File: Utils\ConcurrencyUtils.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
 
namespace Aspire.Hosting.Utils;
 
internal static class ConcurrencyUtils
{
    public static Action Once(Action action)
    {
        ArgumentNullException.ThrowIfNull(action);
 
        var invoked = 0;
 
        return () =>
        {
            if (Interlocked.CompareExchange(ref invoked, 1, 0) != 0)
            {
                return;
            }
 
            action();
        };
    }
 
    public static void AddRange<T>(this ConcurrentBag<T> bag, IEnumerable<T> items)
    {
        ArgumentNullException.ThrowIfNull(bag);
        ArgumentNullException.ThrowIfNull(items);
 
        foreach (var item in items)
        {
            bag.Add(item);
        }
    }
 
    // Allows acquiring a set of SemaphoreSlim instances in a way that allows guaranteed, exception-free release.
    public static async ValueTask<IDisposable> AcquireAllAsync(this IEnumerable<SemaphoreSlim> semaphores, CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(semaphores);
 
        // Ordering forces consistent acquisition order that helps reduce the risk of deadlocks.
        var distinct = semaphores.Distinct().OrderBy(RuntimeHelpers.GetHashCode).ToArray();
        var acquired = new List<SemaphoreSlim>(distinct.Length);
 
        try
        {
            foreach (var semaphore in distinct)
            {
                await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
                acquired.Add(semaphore);
            }
 
            return new SemaphoreSetReleaser(acquired);
        }
        catch
        {
            for (var i = acquired.Count - 1; i >= 0; i--)
            {
                acquired[i].Release();
            }
 
            throw;
        }
    }
 
    private sealed class SemaphoreSetReleaser : IDisposable
    {
        private List<SemaphoreSlim>? _semaphores;
 
        public SemaphoreSetReleaser(List<SemaphoreSlim> semaphores)
        {
            _semaphores = semaphores;
        }
 
        public void Dispose()
        {
            var semaphores = Interlocked.Exchange(ref _semaphores, null);
            if (semaphores is null)
            {
                return;
            }
 
            for (var i = semaphores.Count - 1; i >= 0; i--)
            {
                semaphores[i].Release();
            }
        }
    }
}