// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; /// /// A per-run registry of named instances keyed by resource string (ordinal, /// case-sensitive). It lets the parallel scheduler serialize only the chunks that declare conflicting /// [ResourceLock] keys while unrelated chunks continue to run concurrently. Scope is the test host process. /// internal sealed class ResourceLockManager { private readonly ConcurrentDictionary _locks = new(StringComparer.Ordinal); /// /// Acquires every lock in (which the caller MUST have sorted ordinally by resource /// to guarantee a global, deadlock-free acquisition order), runs , then releases the /// locks in reverse order. If cancellation occurs while acquiring, any already-acquired locks are released. /// /// The distinct, ordinally-sorted locks to hold for the duration of . /// The work to perform while holding the locks. /// Token used to abandon a blocked acquisition. public async Task ExecuteWithLocksAsync(IReadOnlyList locks, Func action, CancellationToken cancellationToken) { var releasers = new List(locks.Count); try { foreach (ResourceLockInfo resourceLock in locks) { AsyncReaderWriterLock namedLock = _locks.GetOrAdd(resourceLock.Resource, static _ => new AsyncReaderWriterLock()); IDisposable releaser = resourceLock.Mode == ResourceAccessMode.Read ? await namedLock.AcquireReaderAsync(cancellationToken).ConfigureAwait(false) : await namedLock.AcquireWriterAsync(cancellationToken).ConfigureAwait(false); releasers.Add(releaser); } await action().ConfigureAwait(false); } finally { for (int i = releasers.Count - 1; i >= 0; i--) { releasers[i].Dispose(); } } } /// /// Merges the resource locks declared across all elements of a scheduling chunk into a distinct set, keeping /// the strongest mode per key ( wins over /// ), sorted ordinally by resource so every chunk acquires locks in the /// same order. /// /// The elements that make up the chunk. /// The distinct, ordinally-sorted locks for the chunk, or an empty list when none are declared. public static IReadOnlyList GetChunkLocks(IEnumerable testSet) { Dictionary? map = null; foreach (UnitTestElement element in testSet) { if (element.ResourceLocks is not { Length: > 0 } elementLocks) { continue; } // StringComparer.Ordinal keeps key comparison consistent with the rest of the lock machinery. #pragma warning disable IDE0028 // Collection initialization can be simplified - a comparer argument is required here. map ??= new Dictionary(StringComparer.Ordinal); #pragma warning restore IDE0028 foreach (ResourceLockInfo resourceLock in elementLocks) { if (map.TryGetValue(resourceLock.Resource, out ResourceAccessMode existingMode)) { if (existingMode != ResourceAccessMode.ReadWrite && resourceLock.Mode == ResourceAccessMode.ReadWrite) { map[resourceLock.Resource] = ResourceAccessMode.ReadWrite; } } else { map[resourceLock.Resource] = resourceLock.Mode; } } } if (map is null) { return []; } var result = new List(map.Count); foreach (KeyValuePair entry in map) { result.Add(new ResourceLockInfo(entry.Key, entry.Value)); } result.Sort(static (left, right) => string.CompareOrdinal(left.Resource, right.Resource)); return result; } }