.net RateGate类到Polly策略

qfe3c7zg  于 2023-04-07  发布在  .NET
关注(0)|答案(1)|浏览(123)

我试图用Polly策略替换RateGate逻辑。然而,没有状态码或任何东西,我不确定是否有可能实现同样的想法,但使用Polly。

用法

// Binance allows 5 messages per second, but we still get rate limited if we send a lot of messages at that rate
// By sending 3 messages per second, evenly spaced out, we can keep sending messages without being limited
private readonly RateGate _webSocketRateLimiter = new RateGate(1, TimeSpan.FromMilliseconds(330));

private void Send(IWebSocket webSocket, object obj)
{
    var json = JsonConvert.SerializeObject(obj);

    _webSocketRateLimiter.WaitToProceed();

    Log.Trace("Send: " + json);

    webSocket.Send(json);
}

RateGate类

public class RateGate : IDisposable
{
    // Semaphore used to count and limit the number of occurrences per
    // unit time.
    private readonly SemaphoreSlim _semaphore;

    // Times (in millisecond ticks) at which the semaphore should be exited.
    private readonly ConcurrentQueue<int> _exitTimes;

    // Timer used to trigger exiting the semaphore.
    private readonly Timer _exitTimer;

    // Whether this instance is disposed.
    private bool _isDisposed;

    /// <summary>
    /// Number of occurrences allowed per unit of time.
    /// </summary>
    public int Occurrences
    {
        get; private set;
    }

    /// <summary>
    /// The length of the time unit, in milliseconds.
    /// </summary>
    public int TimeUnitMilliseconds
    {
        get; private set;
    }

    /// <summary>
    /// Flag indicating we are currently being rate limited
    /// </summary>
    public bool IsRateLimited
    {
        get { return !WaitToProceed(0); }
    }

    /// <summary>
    /// Initializes a <see cref="RateGate"/> with a rate of <paramref name="occurrences"/>
    /// per <paramref name="timeUnit"/>.
    /// </summary>
    /// <param name="occurrences">Number of occurrences allowed per unit of time.</param>
    /// <param name="timeUnit">Length of the time unit.</param>
    /// <exception cref="ArgumentOutOfRangeException">
    /// If <paramref name="occurrences"/> or <paramref name="timeUnit"/> is negative.
    /// </exception>
    public RateGate(int occurrences, TimeSpan timeUnit)
    {
        // Check the arguments.
        if (occurrences <= 0)
            throw new ArgumentOutOfRangeException(nameof(occurrences), "Number of occurrences must be a positive integer");
        if (timeUnit != timeUnit.Duration())
            throw new ArgumentOutOfRangeException(nameof(timeUnit), "Time unit must be a positive span of time");
        if (timeUnit >= TimeSpan.FromMilliseconds(UInt32.MaxValue))
            throw new ArgumentOutOfRangeException(nameof(timeUnit), "Time unit must be less than 2^32 milliseconds");

        Occurrences = occurrences;
        TimeUnitMilliseconds = (int)timeUnit.TotalMilliseconds;

        // Create the semaphore, with the number of occurrences as the maximum count.
        _semaphore = new SemaphoreSlim(Occurrences, Occurrences);

        // Create a queue to hold the semaphore exit times.
        _exitTimes = new ConcurrentQueue<int>();

        // Create a timer to exit the semaphore. Use the time unit as the original
        // interval length because that's the earliest we will need to exit the semaphore.
        _exitTimer = new Timer(ExitTimerCallback, null, TimeUnitMilliseconds, -1);
    }

    // Callback for the exit timer that exits the semaphore based on exit times
    // in the queue and then sets the timer for the nextexit time.
    // Credit to Jim: http://www.jackleitch.net/2010/10/better-rate-limiting-with-dot-net/#comment-3620
    // for providing the code below, fixing issue #3499 - https://github.com/QuantConnect/Lean/issues/3499
    private void ExitTimerCallback(object state)
    {
        try
        {
            // While there are exit times that are passed due still in the queue,
            // exit the semaphore and dequeue the exit time.
            var exitTime = 0;
            var exitTimeValid = _exitTimes.TryPeek(out exitTime);
            while (exitTimeValid)
            {
                if (unchecked(exitTime - Environment.TickCount) > 0)
                {
                    break;
                }
                _semaphore.Release();
                _exitTimes.TryDequeue(out exitTime);
                exitTimeValid = _exitTimes.TryPeek(out exitTime);
            }
            // we are already holding the next item from the queue, do not peek again
            // although this exit time may have already pass by this stmt.
            var timeUntilNextCheck = exitTimeValid
                ? Math.Min(TimeUnitMilliseconds, Math.Max(0, exitTime - Environment.TickCount))
                : TimeUnitMilliseconds;

            _exitTimer.Change(timeUntilNextCheck, -1);
        }
        catch (Exception)
        {
            // can throw if called when disposing
        }
    }

    /// <summary>
    /// Blocks the current thread until allowed to proceed or until the
    /// specified timeout elapses.
    /// </summary>
    /// <param name="millisecondsTimeout">Number of milliseconds to wait, or -1 to wait indefinitely.</param>
    /// <returns>true if the thread is allowed to proceed, or false if timed out</returns>
    public bool WaitToProceed(int millisecondsTimeout)
    {
        // Check the arguments.
        if (millisecondsTimeout < -1)
            throw new ArgumentOutOfRangeException(nameof(millisecondsTimeout));

        CheckDisposed();

        // Block until we can enter the semaphore or until the timeout expires.
        var entered = _semaphore.Wait(millisecondsTimeout);

        // If we entered the semaphore, compute the corresponding exit time
        // and add it to the queue.
        if (entered)
        {
            var timeToExit = unchecked(Environment.TickCount + TimeUnitMilliseconds);
            _exitTimes.Enqueue(timeToExit);
        }

        return entered;
    }

    /// <summary>
    /// Blocks the current thread until allowed to proceed or until the
    /// specified timeout elapses.
    /// </summary>
    /// <param name="timeout"></param>
    /// <returns>true if the thread is allowed to proceed, or false if timed out</returns>
    public bool WaitToProceed(TimeSpan timeout)
    {
        return WaitToProceed((int)timeout.TotalMilliseconds);
    }

    /// <summary>
    /// Blocks the current thread indefinitely until allowed to proceed.
    /// </summary>
    public void WaitToProceed()
    {
        WaitToProceed(Timeout.Infinite);
    }

    // Throws an ObjectDisposedException if this object is disposed.
    private void CheckDisposed()
    {
        if (_isDisposed)
            throw new ObjectDisposedException("RateGate is already disposed");
    }

    /// <summary>
    /// Releases unmanaged resources held by an instance of this class.
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    /// Releases unmanaged resources held by an instance of this class.
    /// </summary>
    /// <param name="isDisposing">Whether this object is being disposed.</param>
    protected virtual void Dispose(bool isDisposing)
    {
        if (!_isDisposed)
        {
            if (isDisposing)
            {
                // The semaphore and timer both implement IDisposable and
                // therefore must be disposed.
                _semaphore.Dispose();
                _exitTimer.Dispose();

                _isDisposed = true;
            }
        }
    }
}

GitHub源码

mgdq6dx1

mgdq6dx11#

速率门

免责声明:我没有使用过这个组件,所以我在这里描述的是我从代码中理解的内容。

这是一种侵入性策略,意味着它修改/改变执行/数据流,以减慢快速生产者或平滑突发。它阻塞流,以避免资源滥用。
在这里,您可以指定后续调用之间的“睡眠持续时间”,这由门本身强制执行。

Polly的速率限制器

这个策略也是为了避免资源滥用而设计的。这意味着如果消费者在预定义的时间内对资源发出了太多的请求,那么它只需通过抛出RateLimitRejectedException来缩短执行。
因此,如果您希望在1分钟内执行20次

RateLimitPolicy rateLimiter = Policy
    .RateLimit(20, TimeSpan.FromSeconds(1));

你不想超过你必须自己等待的极限

rateLimiter.Execute(() =>
{
    //Your Action delegate which runs <1ms
    Thread.Sleep(50);
});

因此,执行应该在允许的时间段内均匀分布。如果手动注入的延迟较短,比如10 ms,那么它将抛出异常。

总结

根据我的理解,两者都像代理对象一样工作,它们位于消费者和生产者之间,以控制消费率。
速率门通过注入人工延迟来实现,而速率限制器在检测到滥用时缩短执行。

相关问题