| @@ -16,8 +16,8 @@ namespace Discord.Net.Rest | |||||
| private readonly ConcurrentQueue<RestRequest> _queue; | private readonly ConcurrentQueue<RestRequest> _queue; | ||||
| private readonly SemaphoreSlim _lock; | private readonly SemaphoreSlim _lock; | ||||
| private Task _resetTask; | private Task _resetTask; | ||||
| private DateTime? _retryAfter; | |||||
| private bool _waitingToProcess; | |||||
| private bool _waitingToProcess, _destroyed; //TODO: Remove _destroyed | |||||
| private int _id; | |||||
| public int WindowMaxCount { get; } | public int WindowMaxCount { get; } | ||||
| public int WindowSeconds { get; } | public int WindowSeconds { get; } | ||||
| @@ -44,10 +44,12 @@ namespace Discord.Net.Rest | |||||
| WindowSeconds = windowSeconds; | WindowSeconds = windowSeconds; | ||||
| _queue = new ConcurrentQueue<RestRequest>(); | _queue = new ConcurrentQueue<RestRequest>(); | ||||
| _lock = new SemaphoreSlim(1, 1); | _lock = new SemaphoreSlim(1, 1); | ||||
| _id = new System.Random().Next(0, int.MaxValue); | |||||
| } | } | ||||
| public void Queue(RestRequest request) | public void Queue(RestRequest request) | ||||
| { | { | ||||
| if (_destroyed) throw new Exception(); | |||||
| //Assume this obj's parent is under lock | //Assume this obj's parent is under lock | ||||
| _queue.Enqueue(request); | _queue.Enqueue(request); | ||||
| @@ -75,7 +77,7 @@ namespace Discord.Net.Rest | |||||
| //If we're waiting to reset (due to a rate limit exception, or preemptive check), abort | //If we're waiting to reset (due to a rate limit exception, or preemptive check), abort | ||||
| if (WindowCount == WindowMaxCount) return; | if (WindowCount == WindowMaxCount) return; | ||||
| //Get next request, return if queue is empty | //Get next request, return if queue is empty | ||||
| if (!_queue.TryPeek(out request)) return; | |||||
| if (!_queue.TryPeek(out request)) break; | |||||
| try | try | ||||
| { | { | ||||
| @@ -88,17 +90,20 @@ namespace Discord.Net.Rest | |||||
| } | } | ||||
| catch (HttpRateLimitException ex) //Preemptive check failed, use Discord's time instead of our own | catch (HttpRateLimitException ex) //Preemptive check failed, use Discord's time instead of our own | ||||
| { | { | ||||
| if (_resetTask == null) | |||||
| WindowCount = WindowMaxCount; | |||||
| var task = _resetTask; | |||||
| if (task != null) | |||||
| { | { | ||||
| //No reset has been queued yet, lets create one as if this *was* preemptive | |||||
| _resetTask = ResetAfter(ex.RetryAfterMilliseconds); | |||||
| Debug($"External rate limit: Reset in {ex.RetryAfterMilliseconds} ms"); | |||||
| Debug($"External rate limit: Extended to {ex.RetryAfterMilliseconds} ms"); | |||||
| var retryAfter = DateTime.UtcNow.AddMilliseconds(ex.RetryAfterMilliseconds); | |||||
| await task.ConfigureAwait(false); | |||||
| int millis = (int)Math.Ceiling((DateTime.UtcNow - retryAfter).TotalMilliseconds); | |||||
| _resetTask = ResetAfter(millis); | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| //A preemptive reset is already queued, set RetryAfter to extend it | |||||
| _retryAfter = DateTime.UtcNow.AddMilliseconds(ex.RetryAfterMilliseconds); | |||||
| Debug($"External rate limit: Extended to {ex.RetryAfterMilliseconds} ms"); | |||||
| Debug($"External rate limit: Reset in {ex.RetryAfterMilliseconds} ms"); | |||||
| _resetTask = ResetAfter(ex.RetryAfterMilliseconds); | |||||
| } | } | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -132,6 +137,25 @@ namespace Discord.Net.Rest | |||||
| Debug($"Internal rate limit: Reset in {WindowSeconds * 1000} ms"); | Debug($"Internal rate limit: Reset in {WindowSeconds * 1000} ms"); | ||||
| } | } | ||||
| } | } | ||||
| //If queue is empty, non-global, and there is no active rate limit, remove this bucket | |||||
| if (_resetTask == null && _bucketGroup == BucketGroup.Guild) | |||||
| { | |||||
| try | |||||
| { | |||||
| await _parent.Lock().ConfigureAwait(false); | |||||
| if (_queue.IsEmpty) //Double check, in case a request was queued before we got both locks | |||||
| { | |||||
| Debug($"Destroy"); | |||||
| _parent.DestroyGuildBucket((GuildBucket)_bucketId, _guildId); | |||||
| _destroyed = true; | |||||
| } | |||||
| } | |||||
| finally | |||||
| { | |||||
| _parent.Unlock(); | |||||
| } | |||||
| } | |||||
| } | } | ||||
| finally | finally | ||||
| { | { | ||||
| @@ -155,36 +179,14 @@ namespace Discord.Net.Rest | |||||
| { | { | ||||
| await Lock().ConfigureAwait(false); | await Lock().ConfigureAwait(false); | ||||
| //If an extension has been planned, start a new wait task | |||||
| if (_retryAfter != null) | |||||
| { | |||||
| _resetTask = ResetAfter((int)(_retryAfter.Value - DateTime.UtcNow).TotalMilliseconds); | |||||
| _retryAfter = null; | |||||
| return; | |||||
| } | |||||
| Debug($"Reset"); | Debug($"Reset"); | ||||
| //Reset the current window count and set our state back to normal | //Reset the current window count and set our state back to normal | ||||
| WindowCount = 0; | WindowCount = 0; | ||||
| _resetTask = null; | _resetTask = null; | ||||
| //Wait is over, work through the current queue | //Wait is over, work through the current queue | ||||
| await ProcessQueue().ConfigureAwait(false); | await ProcessQueue().ConfigureAwait(false); | ||||
| //If queue is empty and non-global, remove this bucket | |||||
| if (_bucketGroup == BucketGroup.Guild && _queue.IsEmpty) | |||||
| { | |||||
| try | |||||
| { | |||||
| await _parent.Lock().ConfigureAwait(false); | |||||
| if (_queue.IsEmpty) //Double check, in case a request was queued before we got both locks | |||||
| _parent.DestroyGuildBucket((GuildBucket)_bucketId, _guildId); | |||||
| } | |||||
| finally | |||||
| { | |||||
| _parent.Unlock(); | |||||
| } | |||||
| } | |||||
| } | } | ||||
| finally | finally | ||||
| { | { | ||||
| @@ -217,7 +219,7 @@ namespace Discord.Net.Rest | |||||
| name = "Unknown"; | name = "Unknown"; | ||||
| break; | break; | ||||
| } | } | ||||
| System.Diagnostics.Debug.WriteLine($"[{name}] {text}"); | |||||
| System.Diagnostics.Debug.WriteLine($"[{name} {_id}] {text}"); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||