Browse Source

Added request retry modes

tags/1.0-rc
RogueException 8 years ago
parent
commit
1efcd3daf6
14 changed files with 96 additions and 37 deletions
  1. +13
    -8
      src/Discord.Net.Core/API/DiscordRestApiClient.cs
  2. +3
    -0
      src/Discord.Net.Core/DiscordConfig.cs
  3. +1
    -0
      src/Discord.Net.Core/Entities/Users/IGuildUser.cs
  4. +7
    -1
      src/Discord.Net.Core/Net/Queue/RequestQueue.cs
  5. +30
    -9
      src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs
  6. +1
    -1
      src/Discord.Net.Core/Net/Queue/Requests/JsonRestRequest.cs
  7. +1
    -1
      src/Discord.Net.Core/Net/Queue/Requests/MultipartRestRequest.cs
  8. +1
    -4
      src/Discord.Net.Core/Net/Queue/Requests/RestRequest.cs
  9. +10
    -3
      src/Discord.Net.Core/RequestOptions.cs
  10. +22
    -0
      src/Discord.Net.Core/RetryMode.cs
  11. +2
    -4
      src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs
  12. +4
    -3
      src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs
  13. +0
    -2
      src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs
  14. +1
    -1
      src/Discord.Net.WebSocket/DiscordSocketClient.cs

+ 13
- 8
src/Discord.Net.Core/API/DiscordRestApiClient.cs View File

@@ -32,26 +32,29 @@ namespace Discord.API
protected readonly JsonSerializer _serializer; protected readonly JsonSerializer _serializer;
protected readonly SemaphoreSlim _stateLock; protected readonly SemaphoreSlim _stateLock;
private readonly RestClientProvider _restClientProvider; private readonly RestClientProvider _restClientProvider;
private readonly string _userAgent;


protected string _authToken; protected string _authToken;
protected bool _isDisposed; protected bool _isDisposed;
private CancellationTokenSource _loginCancelToken; private CancellationTokenSource _loginCancelToken;
private IRestClient _restClient; private IRestClient _restClient;
private bool _fetchCurrentUser;

public RetryMode DefaultRetryMode { get; }
public string UserAgent { get; }


public LoginState LoginState { get; private set; } public LoginState LoginState { get; private set; }
public TokenType AuthTokenType { get; private set; } public TokenType AuthTokenType { get; private set; }
public User CurrentUser { get; private set; } public User CurrentUser { get; private set; }
public RequestQueue RequestQueue { get; private set; } public RequestQueue RequestQueue { get; private set; }
internal bool FetchCurrentUser { get; set; }


public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, JsonSerializer serializer = null, RequestQueue requestQueue = null)
public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry,
JsonSerializer serializer = null, RequestQueue requestQueue = null, bool fetchCurrentUser = true)
{ {
_restClientProvider = restClientProvider; _restClientProvider = restClientProvider;
_userAgent = userAgent;
UserAgent = userAgent;
_serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() }; _serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() };
RequestQueue = requestQueue; RequestQueue = requestQueue;
FetchCurrentUser = true;
_fetchCurrentUser = fetchCurrentUser;


_stateLock = new SemaphoreSlim(1, 1); _stateLock = new SemaphoreSlim(1, 1);


@@ -61,7 +64,7 @@ namespace Discord.API
{ {
_restClient = _restClientProvider(baseUrl); _restClient = _restClientProvider(baseUrl);
_restClient.SetHeader("accept", "*/*"); _restClient.SetHeader("accept", "*/*");
_restClient.SetHeader("user-agent", _userAgent);
_restClient.SetHeader("user-agent", UserAgent);
_restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken)); _restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken));
} }
internal static string GetPrefixedToken(TokenType tokenType, string token) internal static string GetPrefixedToken(TokenType tokenType, string token)
@@ -120,8 +123,8 @@ namespace Discord.API
_authToken = token; _authToken = token;
_restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken)); _restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken));


if (FetchCurrentUser)
CurrentUser = await GetMyUserAsync(new RequestOptions { IgnoreState = true }).ConfigureAwait(false);
if (_fetchCurrentUser)
CurrentUser = await GetMyUserAsync(new RequestOptions { IgnoreState = true, RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false);


LoginState = LoginState.LoggedIn; LoginState = LoginState.LoggedIn;
} }
@@ -257,6 +260,8 @@ namespace Discord.API
{ {
if (!request.Options.IgnoreState) if (!request.Options.IgnoreState)
CheckState(); CheckState();
if (request.Options.RetryMode == null)
request.Options.RetryMode = DefaultRetryMode;


var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
var responseStream = await RequestQueue.SendAsync(request).ConfigureAwait(false); var responseStream = await RequestQueue.SendAsync(request).ConfigureAwait(false);


+ 3
- 0
src/Discord.Net.Core/DiscordConfig.cs View File

@@ -19,6 +19,9 @@ namespace Discord
public const int MaxMessagesPerBatch = 100; public const int MaxMessagesPerBatch = 100;
public const int MaxUsersPerBatch = 1000; public const int MaxUsersPerBatch = 1000;


/// <summary> Gets or sets how a request should act in the case of an error, by default. </summary>
public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry;
/// <summary> Gets or sets the minimum log level severity that will be sent to the LogMessage event. </summary> /// <summary> Gets or sets the minimum log level severity that will be sent to the LogMessage event. </summary>
public LogSeverity LogLevel { get; set; } = LogSeverity.Info; public LogSeverity LogLevel { get; set; } = LogSeverity.Info;
} }


+ 1
- 0
src/Discord.Net.Core/Entities/Users/IGuildUser.cs View File

@@ -12,6 +12,7 @@ namespace Discord
DateTimeOffset? JoinedAt { get; } DateTimeOffset? JoinedAt { get; }
/// <summary> Gets the nickname for this user. </summary> /// <summary> Gets the nickname for this user. </summary>
string Nickname { get; } string Nickname { get; }
/// <summary> Gets the guild-level permissions for this user. </summary>
GuildPermissions GuildPermissions { get; } GuildPermissions GuildPermissions { get; }


/// <summary> Gets the guild for this user. </summary> /// <summary> Gets the guild for this user. </summary>


+ 7
- 1
src/Discord.Net.Core/Net/Queue/RequestQueue.cs View File

@@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
#if DEBUG_LIMITS
using System.Diagnostics; using System.Diagnostics;
#endif
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -63,7 +65,11 @@ namespace Discord.Net.Queue


public async Task<Stream> SendAsync(RestRequest request) public async Task<Stream> SendAsync(RestRequest request)
{ {
request.CancelToken = _requestCancelToken;
if (request.Options.CancelToken.CanBeCanceled)
request.Options.CancelToken = CancellationTokenSource.CreateLinkedTokenSource(_requestCancelToken, request.Options.CancelToken).Token;
else
request.Options.CancelToken = _requestCancelToken;

var bucket = GetOrCreateBucket(request.Options.BucketId, request); var bucket = GetOrCreateBucket(request.Options.BucketId, request);
return await bucket.SendAsync(request).ConfigureAwait(false); return await bucket.SendAsync(request).ConfigureAwait(false);
} }


+ 30
- 9
src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs View File

@@ -1,5 +1,4 @@
using Discord.Net.Rest;
using Newtonsoft.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using System; using System;
#if DEBUG_LIMITS #if DEBUG_LIMITS
@@ -88,7 +87,10 @@ namespace Discord.Net.Queue
#if DEBUG_LIMITS #if DEBUG_LIMITS
Debug.WriteLine($"[{id}] (!) 502"); Debug.WriteLine($"[{id}] (!) 502");
#endif #endif
continue; //Continue
if ((request.Options.RetryMode & RetryMode.Retry502) == 0)
throw new HttpException(HttpStatusCode.BadGateway, null);

continue; //Retry
default: default:
string reason = null; string reason = null;
if (response.Stream != null) if (response.Stream != null)
@@ -115,13 +117,28 @@ namespace Discord.Net.Queue
return response.Stream; return response.Stream;
} }
} }
catch (TimeoutException)
{
#if DEBUG_LIMITS #if DEBUG_LIMITS
catch
Debug.WriteLine($"[{id}] Timeout");
#endif
if ((request.Options.RetryMode & RetryMode.RetryTimeouts) == 0)
throw;

await Task.Delay(500);
continue; //Retry
}
catch (Exception)
{ {
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Error"); Debug.WriteLine($"[{id}] Error");
throw;
}
#endif #endif
if ((request.Options.RetryMode & RetryMode.RetryErrors) == 0)
throw;

await Task.Delay(500);
continue; //Retry
}
finally finally
{ {
UpdateRateLimit(id, request, info, lag, false); UpdateRateLimit(id, request, info, lag, false);
@@ -140,7 +157,7 @@ namespace Discord.Net.Queue


while (true) while (true)
{ {
if (DateTimeOffset.UtcNow > request.TimeoutAt || request.CancelToken.IsCancellationRequested)
if (DateTimeOffset.UtcNow > request.TimeoutAt || request.Options.CancelToken.IsCancellationRequested)
{ {
if (!isRateLimited) if (!isRateLimited)
throw new TimeoutException(); throw new TimeoutException();
@@ -162,6 +179,10 @@ namespace Discord.Net.Queue
isRateLimited = true; isRateLimited = true;
await _queue.RaiseRateLimitTriggered(Id, null).ConfigureAwait(false); await _queue.RaiseRateLimitTriggered(Id, null).ConfigureAwait(false);
} }

if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0)
throw new RateLimitedException();

if (resetAt.HasValue) if (resetAt.HasValue)
{ {
if (resetAt > timeoutAt) if (resetAt > timeoutAt)
@@ -171,7 +192,7 @@ namespace Discord.Net.Queue
Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)"); Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)");
#endif #endif
if (millis > 0) if (millis > 0)
await Task.Delay(millis, request.CancelToken).ConfigureAwait(false);
await Task.Delay(millis, request.Options.CancelToken).ConfigureAwait(false);
} }
else else
{ {
@@ -180,7 +201,7 @@ namespace Discord.Net.Queue
#if DEBUG_LIMITS #if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)"); Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)");
#endif #endif
await Task.Delay(500, request.CancelToken).ConfigureAwait(false);
await Task.Delay(500, request.Options.CancelToken).ConfigureAwait(false);
} }
continue; continue;
} }


+ 1
- 1
src/Discord.Net.Core/Net/Queue/Requests/JsonRestRequest.cs View File

@@ -15,7 +15,7 @@ namespace Discord.Net.Queue


public override async Task<RestResponse> SendAsync() public override async Task<RestResponse> SendAsync()
{ {
return await Client.SendAsync(Method, Endpoint, Json, CancelToken, Options.HeaderOnly).ConfigureAwait(false);
return await Client.SendAsync(Method, Endpoint, Json, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false);
} }
} }
} }

+ 1
- 1
src/Discord.Net.Core/Net/Queue/Requests/MultipartRestRequest.cs View File

@@ -16,7 +16,7 @@ namespace Discord.Net.Queue


public override async Task<RestResponse> SendAsync() public override async Task<RestResponse> SendAsync()
{ {
return await Client.SendAsync(Method, Endpoint, MultipartParams, CancelToken, Options.HeaderOnly).ConfigureAwait(false);
return await Client.SendAsync(Method, Endpoint, MultipartParams, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false);
} }
} }
} }

+ 1
- 4
src/Discord.Net.Core/Net/Queue/Requests/RestRequest.cs View File

@@ -1,7 +1,6 @@
using Discord.Net.Rest; using Discord.Net.Rest;
using System; using System;
using System.IO; using System.IO;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;


namespace Discord.Net.Queue namespace Discord.Net.Queue
@@ -14,7 +13,6 @@ namespace Discord.Net.Queue
public DateTimeOffset? TimeoutAt { get; } public DateTimeOffset? TimeoutAt { get; }
public TaskCompletionSource<Stream> Promise { get; } public TaskCompletionSource<Stream> Promise { get; }
public RequestOptions Options { get; } public RequestOptions Options { get; }
public CancellationToken CancelToken { get; internal set; }


public RestRequest(IRestClient client, string method, string endpoint, RequestOptions options) public RestRequest(IRestClient client, string method, string endpoint, RequestOptions options)
{ {
@@ -24,14 +22,13 @@ namespace Discord.Net.Queue
Method = method; Method = method;
Endpoint = endpoint; Endpoint = endpoint;
Options = options; Options = options;
CancelToken = CancellationToken.None;
TimeoutAt = options.Timeout.HasValue ? DateTimeOffset.UtcNow.AddMilliseconds(options.Timeout.Value) : (DateTimeOffset?)null; TimeoutAt = options.Timeout.HasValue ? DateTimeOffset.UtcNow.AddMilliseconds(options.Timeout.Value) : (DateTimeOffset?)null;
Promise = new TaskCompletionSource<Stream>(); Promise = new TaskCompletionSource<Stream>();
} }


public virtual async Task<RestResponse> SendAsync() public virtual async Task<RestResponse> SendAsync()
{ {
return await Client.SendAsync(Method, Endpoint, CancelToken, Options.HeaderOnly).ConfigureAwait(false);
return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false);
} }
} }
} }

+ 10
- 3
src/Discord.Net.Core/RequestOptions.cs View File

@@ -1,11 +1,18 @@
namespace Discord
using System.Threading;

namespace Discord
{ {
public class RequestOptions public class RequestOptions
{ {
public static RequestOptions Default => new RequestOptions(); public static RequestOptions Default => new RequestOptions();


/// <summary> The max time, in milliseconds, to wait for this request to complete. If null, a request will not time out. If a rate limit has been triggered for this request's bucket and will not be unpaused in time, this request will fail immediately. </summary>
/// <summary>
/// The max time, in milliseconds, to wait for this request to complete. If null, a request will not time out.
/// If a rate limit has been triggered for this request's bucket and will not be unpaused in time, this request will fail immediately.
/// </summary>
public int? Timeout { get; set; } public int? Timeout { get; set; }
public CancellationToken CancelToken { get; set; } = CancellationToken.None;
public RetryMode? RetryMode { get; set; }
public bool HeaderOnly { get; internal set; } public bool HeaderOnly { get; internal set; }


internal bool IgnoreState { get; set; } internal bool IgnoreState { get; set; }
@@ -13,7 +20,7 @@
internal bool IsClientBucket { get; set; } internal bool IsClientBucket { get; set; }


internal static RequestOptions CreateOrClone(RequestOptions options) internal static RequestOptions CreateOrClone(RequestOptions options)
{
{
if (options == null) if (options == null)
return new RequestOptions(); return new RequestOptions();
else else


+ 22
- 0
src/Discord.Net.Core/RetryMode.cs View File

@@ -0,0 +1,22 @@
using System;

namespace Discord
{
/// <summary> Specifies how a request should act in the case of an error. </summary>
[Flags]
public enum RetryMode
{
/// <summary> If a request fails, an exception is thrown immediately. </summary>
AlwaysFail = 0x0,
/// <summary> Retry if a request timed out. </summary>
RetryTimeouts = 0x1,
/// <summary> Retry if a request failed due to a network error. </summary>
RetryErrors = 0x2,
/// <summary> Retry if a request failed due to a ratelimit. </summary>
RetryRatelimit = 0x4,
/// <summary> Retry if a request failed due to an HTTP error 502. </summary>
Retry502 = 0x8,
/// <summary> Continuously retry a request until it times out, its cancel token is triggered, or the server responds with a non-502 error. </summary>
AlwaysRetry = RetryTimeouts | RetryErrors | RetryRatelimit | Retry502,
}
}

+ 2
- 4
src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs View File

@@ -69,15 +69,13 @@ namespace Discord.API
public ConnectionState ConnectionState { get; private set; } public ConnectionState ConnectionState { get; private set; }


public DiscordRpcApiClient(string clientId, string userAgent, string origin, RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, public DiscordRpcApiClient(string clientId, string userAgent, string origin, RestClientProvider restClientProvider, WebSocketProvider webSocketProvider,
JsonSerializer serializer = null, RequestQueue requestQueue = null)
: base(restClientProvider, userAgent, serializer, requestQueue)
RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null, RequestQueue requestQueue = null)
: base(restClientProvider, userAgent, defaultRetryMode, serializer, requestQueue, false)
{ {
_connectionLock = new SemaphoreSlim(1, 1); _connectionLock = new SemaphoreSlim(1, 1);
_clientId = clientId; _clientId = clientId;
_origin = origin; _origin = origin;


FetchCurrentUser = false;

_requestQueue = requestQueue ?? new RequestQueue(); _requestQueue = requestQueue ?? new RequestQueue();
_requests = new ConcurrentDictionary<Guid, RpcRequest>(); _requests = new ConcurrentDictionary<Guid, RpcRequest>();


+ 4
- 3
src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs View File

@@ -32,11 +32,12 @@ namespace Discord.API


public ConnectionState ConnectionState { get; private set; } public ConnectionState ConnectionState { get; private set; }


public DiscordSocketApiClient(RestClientProvider restClientProvider, string userAgent, WebSocketProvider webSocketProvider, JsonSerializer serializer = null, RequestQueue requestQueue = null)
: base(restClientProvider, userAgent, serializer, requestQueue)
public DiscordSocketApiClient(RestClientProvider restClientProvider, string userAgent, WebSocketProvider webSocketProvider,
RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null, RequestQueue requestQueue = null)
: base(restClientProvider, userAgent, defaultRetryMode, serializer, requestQueue, true)
{ {
_gatewayClient = webSocketProvider(); _gatewayClient = webSocketProvider();
//_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .Net 4.6+)
//_gatewayClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .NET Framework 4.6+)
_gatewayClient.BinaryMessage += async (data, index, count) => _gatewayClient.BinaryMessage += async (data, index, count) =>
{ {
using (var compressed = new MemoryStream(data, index + 2, count - 2)) using (var compressed = new MemoryStream(data, index + 2, count - 2))


+ 0
- 2
src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs View File

@@ -221,7 +221,5 @@ namespace Discord.WebSocket
remove { _recipientRemovedEvent.Remove(value); } remove { _recipientRemovedEvent.Remove(value); }
} }
private readonly AsyncEvent<Func<SocketGroupUser, Task>> _recipientRemovedEvent = new AsyncEvent<Func<SocketGroupUser, Task>>(); private readonly AsyncEvent<Func<SocketGroupUser, Task>> _recipientRemovedEvent = new AsyncEvent<Func<SocketGroupUser, Task>>();

//TODO: Add PresenceUpdated? VoiceStateUpdated?, VoiceConnected, VoiceDisconnected;
} }
} }

+ 1
- 1
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -130,7 +130,7 @@ namespace Discord.WebSocket
protected override async Task OnLoginAsync(TokenType tokenType, string token) protected override async Task OnLoginAsync(TokenType tokenType, string token)
{ {
var voiceRegions = await ApiClient.GetVoiceRegionsAsync(new RequestOptions { IgnoreState = true}).ConfigureAwait(false);
var voiceRegions = await ApiClient.GetVoiceRegionsAsync(new RequestOptions { IgnoreState = true, RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false);
_voiceRegions = voiceRegions.Select(x => RestVoiceRegion.Create(this, x)).ToImmutableDictionary(x => x.Id); _voiceRegions = voiceRegions.Select(x => RestVoiceRegion.Create(this, x)).ToImmutableDictionary(x => x.Id);
} }
protected override async Task OnLogoutAsync() protected override async Task OnLogoutAsync()


Loading…
Cancel
Save