public RequestBucket(RequestQueue queue, RestRequest request, string id) { _queue = queue; Id = id; _lock = new object(); if (request.Options.IsClientBucket) { WindowCount = ClientBucket.Get(request.Options.BucketId).WindowCount; } else { WindowCount = 1; //Only allow one request until we get a header back } _semaphore = WindowCount; _resetTick = null; LastAttemptAt = DateTimeOffset.UtcNow; }
private void UpdateRateLimit(int id, IRequest request, RateLimitInfo info, bool is429, bool redirected = false) { if (WindowCount == 0) { return; } lock (_lock) { if (redirected) { Interlocked.Decrement(ref _semaphore); //we might still hit a real ratelimit if all tickets were already taken, can't do much about it since we didn't know they were the same #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Decrease Semaphore"); #endif } bool hasQueuedReset = _resetTick != null; if (info.Bucket != null && !redirected) { (RequestBucket, BucketId)hashBucket = _queue.UpdateBucketHash(Id, info.Bucket); if (!(hashBucket.Item1 is null) && !(hashBucket.Item2 is null)) { if (hashBucket.Item1 == this) //this bucket got promoted to a hash queue { Id = hashBucket.Item2; #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Promoted to Hash Bucket ({hashBucket.Item2})"); #endif } else { _redirectBucket = hashBucket.Item1; //this request should be part of another bucket, this bucket will be disabled, redirect everything _redirectBucket.UpdateRateLimit(id, request, info, is429, redirected: true); //update the hash bucket ratelimit #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Redirected to {_redirectBucket.Id}"); #endif return; } } } if (info.Limit.HasValue && WindowCount != info.Limit.Value) { WindowCount = info.Limit.Value; _semaphore = info.Remaining.Value; #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Upgraded Semaphore to {info.Remaining.Value}/{WindowCount}"); #endif } DateTimeOffset?resetTick = null; //Using X-RateLimit-Remaining causes a race condition /*if (info.Remaining.HasValue) * { * Debug.WriteLine($"[{id}] X-RateLimit-Remaining: " + info.Remaining.Value); * _semaphore = info.Remaining.Value; * }*/ if (info.RetryAfter.HasValue) { //RetryAfter is more accurate than Reset, where available resetTick = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value); #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Retry-After: {info.RetryAfter.Value} ({info.RetryAfter.Value} ms)"); #endif } else if (info.ResetAfter.HasValue && (request.Options.UseSystemClock.HasValue ? !request.Options.UseSystemClock.Value : false)) { resetTick = DateTimeOffset.UtcNow.Add(info.ResetAfter.Value); #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Reset-After: {info.ResetAfter.Value} ({info.ResetAfter?.TotalMilliseconds} ms)"); #endif } else if (info.Reset.HasValue) { resetTick = info.Reset.Value.AddSeconds(info.Lag?.TotalSeconds ?? 1.0); /* millisecond precision makes this unnecessary, retaining in case of regression * if (request.Options.IsReactionBucket) * resetTick = DateTimeOffset.Now.AddMilliseconds(250); */ int diff = (int)(resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds; #if DEBUG_LIMITS Debug.WriteLine($"[{id}] X-RateLimit-Reset: {info.Reset.Value.ToUnixTimeSeconds()} ({diff} ms, {info.Lag?.TotalMilliseconds} ms lag)"); #endif } else if (request.Options.IsClientBucket && Id != null) { resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(Id).WindowSeconds); #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(Id).WindowSeconds * 1000} ms)"); #endif } else if (request.Options.IsGatewayBucket && request.Options.BucketId != null) { resetTick = DateTimeOffset.UtcNow.AddSeconds(GatewayBucket.Get(request.Options.BucketId).WindowSeconds); #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Gateway Bucket ({GatewayBucket.Get(request.Options.BucketId).WindowSeconds * 1000} ms)"); #endif if (!hasQueuedReset) { _resetTick = resetTick; LastAttemptAt = resetTick.Value; #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Reset in {(int)Math.Ceiling((resetTick - DateTimeOffset.UtcNow).Value.TotalMilliseconds)} ms"); #endif var _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds), request); } return; } if (resetTick == null) { WindowCount = 0; //No rate limit info, disable limits on this bucket #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Disabled Semaphore"); #endif return; } if (!hasQueuedReset || resetTick > _resetTick) { _resetTick = resetTick; LastAttemptAt = resetTick.Value; //Make sure we dont destroy this until after its been reset #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Reset in {(int)Math.Ceiling((resetTick - DateTimeOffset.UtcNow).Value.TotalMilliseconds)} ms"); #endif if (!hasQueuedReset) { var _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds), request); } } } }
private void UpdateRateLimit(int id, RestRequest request, RateLimitInfo info, bool is429) { if (WindowCount == 0) { return; } lock (_lock) { bool hasQueuedReset = _resetTick != null; if (info.Limit.HasValue && WindowCount != info.Limit.Value) { WindowCount = info.Limit.Value; _semaphore = info.Remaining.Value; #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Upgraded Semaphore to {info.Remaining.Value}/{WindowCount}"); #endif } var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); DateTimeOffset?resetTick = null; //Using X-RateLimit-Remaining causes a race condition /*if (info.Remaining.HasValue) * { * Debug.WriteLine($"[{id}] X-RateLimit-Remaining: " + info.Remaining.Value); * _semaphore = info.Remaining.Value; * }*/ if (info.RetryAfter.HasValue) { //RetryAfter is more accurate than Reset, where available resetTick = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value); #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Retry-After: {info.RetryAfter.Value} ({info.RetryAfter.Value} ms)"); #endif } else if (info.Reset.HasValue) { resetTick = info.Reset.Value.AddSeconds(info.Lag?.TotalSeconds ?? 1.0); if (request.Options.IsReactionBucket) { resetTick = DateTimeOffset.Now.AddMilliseconds(250); } int diff = (int)(resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds; #if DEBUG_LIMITS Debug.WriteLine($"[{id}] X-RateLimit-Reset: {info.Reset.Value.ToUnixTimeSeconds()} ({diff} ms, {info.Lag?.TotalMilliseconds} ms lag)"); #endif } else if (request.Options.IsClientBucket && request.Options.BucketId != null) { resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(request.Options.BucketId).WindowSeconds); #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(request.Options.BucketId).WindowSeconds * 1000} ms)"); #endif } if (resetTick == null) { WindowCount = 0; //No rate limit info, disable limits on this bucket (should only ever happen with a user token) #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Disabled Semaphore"); #endif return; } if (!hasQueuedReset || resetTick > _resetTick) { _resetTick = resetTick; LastAttemptAt = resetTick.Value; //Make sure we dont destroy this until after its been reset #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Reset in {(int)Math.Ceiling((resetTick - DateTimeOffset.UtcNow).Value.TotalMilliseconds)} ms"); #endif if (!hasQueuedReset) { var _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds)); } } } }