File: System\Threading\RateLimiting\ChainedRateLimiterShared.cs
Web Access
Project: src\src\libraries\System.Threading.RateLimiting\src\System.Threading.RateLimiting.csproj (System.Threading.RateLimiting)
// 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.Generic;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
 
namespace System.Threading.RateLimiting
{
    /// <summary>
    /// Shared static methods used by <see cref="ChainedRateLimiter"/>,
    /// <see cref="ChainedReplenishingRateLimiter"/>, and <see cref="ChainedPartitionedRateLimiter{TResource}"/>.
    /// </summary>
    internal static class ChainedRateLimiterShared
    {
        internal static RateLimiterStatistics GetStatisticsCore(RateLimiter[] limiters)
        {
            long lowestAvailablePermits = long.MaxValue;
            long currentQueuedCount = 0;
            long totalFailedLeases = 0;
            long innerMostSuccessfulLeases = 0;
 
            foreach (RateLimiter limiter in limiters)
            {
                if (limiter.GetStatistics() is { } statistics)
                {
                    if (statistics.CurrentAvailablePermits < lowestAvailablePermits)
                    {
                        lowestAvailablePermits = statistics.CurrentAvailablePermits;
                    }
 
                    currentQueuedCount += statistics.CurrentQueuedCount;
                    totalFailedLeases += statistics.TotalFailedLeases;
                    innerMostSuccessfulLeases = statistics.TotalSuccessfulLeases;
                }
            }
 
            return new RateLimiterStatistics()
            {
                CurrentAvailablePermits = lowestAvailablePermits,
                CurrentQueuedCount = currentQueuedCount,
                TotalFailedLeases = totalFailedLeases,
                TotalSuccessfulLeases = innerMostSuccessfulLeases,
            };
        }
 
        internal static TimeSpan? GetIdleDurationCore(RateLimiter[] limiters)
        {
            TimeSpan? lowestIdleDuration = null;
 
            foreach (RateLimiter limiter in limiters)
            {
                TimeSpan? idleDuration = limiter.IdleDuration;
                if (idleDuration is null)
                {
                    // The chain should not be considered idle if any of its children is not idle.
                    return null;
                }
 
                if (lowestIdleDuration is null || idleDuration < lowestIdleDuration)
                {
                    lowestIdleDuration = idleDuration;
                }
            }
 
            return lowestIdleDuration;
        }
 
        internal static RateLimitLease AttemptAcquireChained(RateLimiter[] limiters, int permitCount)
        {
            RateLimitLease[]? leases = null;
 
            for (int i = 0; i < limiters.Length; i++)
            {
                RateLimitLease? lease = null;
                Exception? exception = null;
 
                try
                {
                    lease = limiters[i].AttemptAcquire(permitCount);
                }
                catch (Exception ex)
                {
                    exception = ex;
                }
 
                RateLimitLease? notAcquiredLease = CommonAcquireLogic(exception, lease, ref leases, i, limiters.Length);
 
                if (notAcquiredLease is not null)
                {
                    return notAcquiredLease;
                }
            }
 
            return new CombinedRateLimitLease(leases!);
        }
 
        internal static async ValueTask<RateLimitLease> AcquireAsyncChained(RateLimiter[] limiters, int permitCount, CancellationToken cancellationToken)
        {
            RateLimitLease[]? leases = null;
 
            for (int i = 0; i < limiters.Length; i++)
            {
                RateLimitLease? lease = null;
                Exception? exception = null;
 
                try
                {
                    lease = await limiters[i].AcquireAsync(permitCount, cancellationToken).ConfigureAwait(false);
                }
                catch (Exception ex)
                {
                    exception = ex;
                }
 
                RateLimitLease? notAcquiredLease = CommonAcquireLogic(exception, lease, ref leases, i, limiters.Length);
 
                if (notAcquiredLease is not null)
                {
                    return notAcquiredLease;
                }
            }
 
            return new CombinedRateLimitLease(leases!);
        }
 
        internal static RateLimitLease? CommonAcquireLogic(Exception? ex, RateLimitLease? lease, ref RateLimitLease[]? leases, int index, int length)
        {
            if (ex is not null)
            {
                AggregateException? innerEx = CommonDispose(leases, index);
 
                if (innerEx is not null)
                {
                    Exception[] exceptions = new Exception[innerEx.InnerExceptions.Count + 1];
                    innerEx.InnerExceptions.CopyTo(exceptions, 0);
                    exceptions[exceptions.Length - 1] = ex;
                    throw new AggregateException(exceptions);
                }
 
                ExceptionDispatchInfo.Capture(ex).Throw();
            }
 
            if (!lease!.IsAcquired)
            {
                AggregateException? innerEx = CommonDispose(leases, index);
                return innerEx is not null ? throw innerEx : lease;
            }
 
            leases ??= new RateLimitLease[length];
            leases[index] = lease;
            return null;
        }
 
        private static AggregateException? CommonDispose(RateLimitLease[]? leases, int i)
        {
            List<Exception>? exceptions = null;
 
            while (i > 0)
            {
                i--;
 
                try
                {
                    leases![i].Dispose();
                }
                catch (Exception ex)
                {
                    exceptions ??= [];
                    exceptions.Add(ex);
                }
            }
 
            if (exceptions is not null)
            {
                return new AggregateException(exceptions);
            }
 
            return null;
        }
 
    }
}