@@ -60,6 +60,7 @@ namespace Discord | |||||
internal string BucketId { get; set; } | internal string BucketId { get; set; } | ||||
internal bool IsClientBucket { get; set; } | internal bool IsClientBucket { get; set; } | ||||
internal bool IsReactionBucket { get; set; } | internal bool IsReactionBucket { get; set; } | ||||
internal bool IsGatewayBucket { get; set; } | |||||
internal static RequestOptions CreateOrClone(RequestOptions options) | internal static RequestOptions CreateOrClone(RequestOptions options) | ||||
{ | { | ||||
@@ -0,0 +1,50 @@ | |||||
using System.Collections.Immutable; | |||||
namespace Discord.Net.Queue | |||||
{ | |||||
public enum GatewayBucketType | |||||
{ | |||||
Unbucketed = 0, | |||||
Identify = 1 | |||||
} | |||||
internal struct GatewayBucket | |||||
{ | |||||
private static readonly ImmutableDictionary<GatewayBucketType, GatewayBucket> DefsByType; | |||||
private static readonly ImmutableDictionary<string, GatewayBucket> DefsById; | |||||
static GatewayBucket() | |||||
{ | |||||
var buckets = new[] | |||||
{ | |||||
new GatewayBucket(GatewayBucketType.Unbucketed, "<unbucketed>", 120, 60), | |||||
new GatewayBucket(GatewayBucketType.Identify, "<identify>", 1, 5) | |||||
}; | |||||
var builder = ImmutableDictionary.CreateBuilder<GatewayBucketType, GatewayBucket>(); | |||||
foreach (var bucket in buckets) | |||||
builder.Add(bucket.Type, bucket); | |||||
DefsByType = builder.ToImmutable(); | |||||
var builder2 = ImmutableDictionary.CreateBuilder<string, GatewayBucket>(); | |||||
foreach (var bucket in buckets) | |||||
builder2.Add(bucket.Id, bucket); | |||||
DefsById = builder2.ToImmutable(); | |||||
} | |||||
public static GatewayBucket Get(GatewayBucketType type) => DefsByType[type]; | |||||
public static GatewayBucket Get(string id) => DefsById[id]; | |||||
public GatewayBucketType Type { get; } | |||||
public string Id { get; } | |||||
public int WindowCount { get; } | |||||
public int WindowSeconds { get; } | |||||
public GatewayBucket(GatewayBucketType type, string id, int count, int seconds) | |||||
{ | |||||
Type = type; | |||||
Id = id; | |||||
WindowCount = count; | |||||
WindowSeconds = seconds; | |||||
} | |||||
} | |||||
} |
@@ -89,12 +89,23 @@ namespace Discord.Net.Queue | |||||
} | } | ||||
public async Task SendAsync(WebSocketRequest request) | public async Task SendAsync(WebSocketRequest request) | ||||
{ | { | ||||
//TODO: Re-impl websocket buckets | |||||
request.CancelToken = _requestCancelToken; | |||||
await request.SendAsync().ConfigureAwait(false); | |||||
CancellationTokenSource createdTokenSource = null; | |||||
if (request.Options.CancelToken.CanBeCanceled) | |||||
{ | |||||
createdTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_requestCancelToken, request.Options.CancelToken); | |||||
request.Options.CancelToken = createdTokenSource.Token; | |||||
} | |||||
else | |||||
request.Options.CancelToken = _requestCancelToken; | |||||
var bucket = GetOrCreateBucket(request.Options.BucketId, request); | |||||
await bucket.SendAsync(request).ConfigureAwait(false); | |||||
createdTokenSource?.Dispose(); | |||||
//request.CancelToken = _requestCancelToken; | |||||
//await request.SendAsync().ConfigureAwait(false); | |||||
} | } | ||||
internal async Task EnterGlobalAsync(int id, RestRequest request) | |||||
internal async Task EnterGlobalAsync(int id, IRequest request) | |||||
{ | { | ||||
int millis = (int)Math.Ceiling((_waitUntil - DateTimeOffset.UtcNow).TotalMilliseconds); | int millis = (int)Math.Ceiling((_waitUntil - DateTimeOffset.UtcNow).TotalMilliseconds); | ||||
if (millis > 0) | if (millis > 0) | ||||
@@ -110,7 +121,7 @@ namespace Discord.Net.Queue | |||||
_waitUntil = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value + (info.Lag?.TotalMilliseconds ?? 0.0)); | _waitUntil = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value + (info.Lag?.TotalMilliseconds ?? 0.0)); | ||||
} | } | ||||
private RequestBucket GetOrCreateBucket(string id, RestRequest request) | |||||
private RequestBucket GetOrCreateBucket(string id, IRequest request) | |||||
{ | { | ||||
return _buckets.GetOrAdd(id, x => new RequestBucket(this, request, x)); | return _buckets.GetOrAdd(id, x => new RequestBucket(this, request, x)); | ||||
} | } | ||||
@@ -22,7 +22,7 @@ namespace Discord.Net.Queue | |||||
public int WindowCount { get; private set; } | public int WindowCount { get; private set; } | ||||
public DateTimeOffset LastAttemptAt { get; private set; } | public DateTimeOffset LastAttemptAt { get; private set; } | ||||
public RequestBucket(RequestQueue queue, RestRequest request, string id) | |||||
public RequestBucket(RequestQueue queue, IRequest request, string id) | |||||
{ | { | ||||
_queue = queue; | _queue = queue; | ||||
Id = id; | Id = id; | ||||
@@ -31,13 +31,15 @@ namespace Discord.Net.Queue | |||||
if (request.Options.IsClientBucket) | if (request.Options.IsClientBucket) | ||||
WindowCount = ClientBucket.Get(request.Options.BucketId).WindowCount; | WindowCount = ClientBucket.Get(request.Options.BucketId).WindowCount; | ||||
else if (request.Options.IsGatewayBucket) | |||||
WindowCount = GatewayBucket.Get(request.Options.BucketId).WindowCount; | |||||
else | else | ||||
WindowCount = 1; //Only allow one request until we get a header back | WindowCount = 1; //Only allow one request until we get a header back | ||||
_semaphore = WindowCount; | _semaphore = WindowCount; | ||||
_resetTick = null; | _resetTick = null; | ||||
LastAttemptAt = DateTimeOffset.UtcNow; | LastAttemptAt = DateTimeOffset.UtcNow; | ||||
} | } | ||||
static int nextId = 0; | static int nextId = 0; | ||||
public async Task<Stream> SendAsync(RestRequest request) | public async Task<Stream> SendAsync(RestRequest request) | ||||
{ | { | ||||
@@ -149,8 +151,59 @@ namespace Discord.Net.Queue | |||||
} | } | ||||
} | } | ||||
} | } | ||||
public async Task SendAsync(WebSocketRequest request) | |||||
{ | |||||
int id = Interlocked.Increment(ref nextId); | |||||
#if DEBUG_LIMITS | |||||
Debug.WriteLine($"[{id}] Start"); | |||||
#endif | |||||
LastAttemptAt = DateTimeOffset.UtcNow; | |||||
while (true) | |||||
{ | |||||
await _queue.EnterGlobalAsync(id, request).ConfigureAwait(false); | |||||
await EnterAsync(id, request).ConfigureAwait(false); | |||||
private async Task EnterAsync(int id, RestRequest request) | |||||
#if DEBUG_LIMITS | |||||
Debug.WriteLine($"[{id}] Sending..."); | |||||
#endif | |||||
try | |||||
{ | |||||
await request.SendAsync().ConfigureAwait(false); | |||||
return; | |||||
} | |||||
catch (TimeoutException) | |||||
{ | |||||
#if DEBUG_LIMITS | |||||
Debug.WriteLine($"[{id}] Timeout"); | |||||
#endif | |||||
if ((request.Options.RetryMode & RetryMode.RetryTimeouts) == 0) | |||||
throw; | |||||
await Task.Delay(500).ConfigureAwait(false); | |||||
continue; //Retry | |||||
} | |||||
/*catch (Exception) | |||||
{ | |||||
#if DEBUG_LIMITS | |||||
Debug.WriteLine($"[{id}] Error"); | |||||
#endif | |||||
if ((request.Options.RetryMode & RetryMode.RetryErrors) == 0) | |||||
throw; | |||||
await Task.Delay(500); | |||||
continue; //Retry | |||||
}*/ | |||||
finally | |||||
{ | |||||
UpdateRateLimit(id, request, default(RateLimitInfo), false); | |||||
#if DEBUG_LIMITS | |||||
Debug.WriteLine($"[{id}] Stop"); | |||||
#endif | |||||
} | |||||
} | |||||
} | |||||
private async Task EnterAsync(int id, IRequest request) | |||||
{ | { | ||||
int windowCount; | int windowCount; | ||||
DateTimeOffset? resetAt; | DateTimeOffset? resetAt; | ||||
@@ -213,7 +266,7 @@ namespace Discord.Net.Queue | |||||
} | } | ||||
} | } | ||||
private void UpdateRateLimit(int id, RestRequest request, RateLimitInfo info, bool is429) | |||||
private void UpdateRateLimit(int id, IRequest request, RateLimitInfo info, bool is429) | |||||
{ | { | ||||
if (WindowCount == 0) | if (WindowCount == 0) | ||||
return; | return; | ||||
@@ -273,6 +326,13 @@ namespace Discord.Net.Queue | |||||
Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(request.Options.BucketId).WindowSeconds * 1000} ms)"); | Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(request.Options.BucketId).WindowSeconds * 1000} ms)"); | ||||
#endif | #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 (resetTick == null) | if (resetTick == null) | ||||
{ | { | ||||
@@ -320,7 +380,7 @@ namespace Discord.Net.Queue | |||||
} | } | ||||
} | } | ||||
private void ThrowRetryLimit(RestRequest request) | |||||
private void ThrowRetryLimit(IRequest request) | |||||
{ | { | ||||
if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0) | if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0) | ||||
throw new RateLimitedException(request); | throw new RateLimitedException(request); | ||||
@@ -9,7 +9,6 @@ namespace Discord.Net.Queue | |||||
public class WebSocketRequest : IRequest | public class WebSocketRequest : IRequest | ||||
{ | { | ||||
public IWebSocketClient Client { get; } | public IWebSocketClient Client { get; } | ||||
public string BucketId { get; } | |||||
public byte[] Data { get; } | public byte[] Data { get; } | ||||
public bool IsText { get; } | public bool IsText { get; } | ||||
public DateTimeOffset? TimeoutAt { get; } | public DateTimeOffset? TimeoutAt { get; } | ||||
@@ -17,12 +16,11 @@ namespace Discord.Net.Queue | |||||
public RequestOptions Options { get; } | public RequestOptions Options { get; } | ||||
public CancellationToken CancelToken { get; internal set; } | public CancellationToken CancelToken { get; internal set; } | ||||
public WebSocketRequest(IWebSocketClient client, string bucketId, byte[] data, bool isText, RequestOptions options) | |||||
public WebSocketRequest(IWebSocketClient client, byte[] data, bool isText, RequestOptions options) | |||||
{ | { | ||||
Preconditions.NotNull(options, nameof(options)); | Preconditions.NotNull(options, nameof(options)); | ||||
Client = client; | Client = client; | ||||
BucketId = bucketId; | |||||
Data = data; | Data = data; | ||||
IsText = isText; | IsText = isText; | ||||
Options = options; | Options = options; | ||||
@@ -205,7 +205,10 @@ namespace Discord.API | |||||
payload = new SocketFrame { Operation = (int)opCode, Payload = payload }; | payload = new SocketFrame { Operation = (int)opCode, Payload = payload }; | ||||
if (payload != null) | if (payload != null) | ||||
bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); | bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); | ||||
await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, null, bytes, true, options)).ConfigureAwait(false); | |||||
options.IsGatewayBucket = true; | |||||
options.BucketId = GatewayBucket.Get(opCode == GatewayOpCode.Identify ? GatewayBucketType.Identify : GatewayBucketType.Unbucketed).Id; | |||||
await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, bytes, true, options)).ConfigureAwait(false); | |||||
await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); | await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); | ||||
} | } | ||||