diff --git a/src/Discord.Net.Core/API/DiscordRestApiClient.cs b/src/Discord.Net.Core/API/DiscordRestApiClient.cs index 0d0b7914b..61e56ca13 100644 --- a/src/Discord.Net.Core/API/DiscordRestApiClient.cs +++ b/src/Discord.Net.Core/API/DiscordRestApiClient.cs @@ -32,26 +32,29 @@ namespace Discord.API protected readonly JsonSerializer _serializer; protected readonly SemaphoreSlim _stateLock; private readonly RestClientProvider _restClientProvider; - private readonly string _userAgent; protected string _authToken; protected bool _isDisposed; private CancellationTokenSource _loginCancelToken; private IRestClient _restClient; + private bool _fetchCurrentUser; + + public RetryMode DefaultRetryMode { get; } + public string UserAgent { get; } public LoginState LoginState { get; private set; } public TokenType AuthTokenType { get; private set; } public User CurrentUser { 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; - _userAgent = userAgent; + UserAgent = userAgent; _serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() }; RequestQueue = requestQueue; - FetchCurrentUser = true; + _fetchCurrentUser = fetchCurrentUser; _stateLock = new SemaphoreSlim(1, 1); @@ -61,7 +64,7 @@ namespace Discord.API { _restClient = _restClientProvider(baseUrl); _restClient.SetHeader("accept", "*/*"); - _restClient.SetHeader("user-agent", _userAgent); + _restClient.SetHeader("user-agent", UserAgent); _restClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, _authToken)); } internal static string GetPrefixedToken(TokenType tokenType, string token) @@ -120,8 +123,8 @@ namespace Discord.API _authToken = token; _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; } @@ -257,6 +260,8 @@ namespace Discord.API { if (!request.Options.IgnoreState) CheckState(); + if (request.Options.RetryMode == null) + request.Options.RetryMode = DefaultRetryMode; var stopwatch = Stopwatch.StartNew(); var responseStream = await RequestQueue.SendAsync(request).ConfigureAwait(false); diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index b35f0d745..bb7077472 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -19,6 +19,9 @@ namespace Discord public const int MaxMessagesPerBatch = 100; public const int MaxUsersPerBatch = 1000; + /// Gets or sets how a request should act in the case of an error, by default. + public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry; + /// Gets or sets the minimum log level severity that will be sent to the LogMessage event. public LogSeverity LogLevel { get; set; } = LogSeverity.Info; } diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index b48c76a37..ab447e520 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -12,6 +12,7 @@ namespace Discord DateTimeOffset? JoinedAt { get; } /// Gets the nickname for this user. string Nickname { get; } + /// Gets the guild-level permissions for this user. GuildPermissions GuildPermissions { get; } /// Gets the guild for this user. diff --git a/src/Discord.Net.Core/Net/Queue/RequestQueue.cs b/src/Discord.Net.Core/Net/Queue/RequestQueue.cs index 1ea586481..52ad90f11 100644 --- a/src/Discord.Net.Core/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net.Core/Net/Queue/RequestQueue.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Concurrent; +#if DEBUG_LIMITS using System.Diagnostics; +#endif using System.IO; using System.Linq; using System.Threading; @@ -63,7 +65,11 @@ namespace Discord.Net.Queue public async Task 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); return await bucket.SendAsync(request).ConfigureAwait(false); } diff --git a/src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs index 8ee52171c..332177de8 100644 --- a/src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs @@ -1,5 +1,4 @@ -using Discord.Net.Rest; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; #if DEBUG_LIMITS @@ -88,7 +87,10 @@ namespace Discord.Net.Queue #if DEBUG_LIMITS Debug.WriteLine($"[{id}] (!) 502"); #endif - continue; //Continue + if ((request.Options.RetryMode & RetryMode.Retry502) == 0) + throw new HttpException(HttpStatusCode.BadGateway, null); + + continue; //Retry default: string reason = null; if (response.Stream != null) @@ -115,13 +117,28 @@ namespace Discord.Net.Queue return response.Stream; } } + catch (TimeoutException) + { #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"); - throw; - } #endif + if ((request.Options.RetryMode & RetryMode.RetryErrors) == 0) + throw; + + await Task.Delay(500); + continue; //Retry + } finally { UpdateRateLimit(id, request, info, lag, false); @@ -140,7 +157,7 @@ namespace Discord.Net.Queue while (true) { - if (DateTimeOffset.UtcNow > request.TimeoutAt || request.CancelToken.IsCancellationRequested) + if (DateTimeOffset.UtcNow > request.TimeoutAt || request.Options.CancelToken.IsCancellationRequested) { if (!isRateLimited) throw new TimeoutException(); @@ -162,6 +179,10 @@ namespace Discord.Net.Queue isRateLimited = true; await _queue.RaiseRateLimitTriggered(Id, null).ConfigureAwait(false); } + + if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0) + throw new RateLimitedException(); + if (resetAt.HasValue) { if (resetAt > timeoutAt) @@ -171,7 +192,7 @@ namespace Discord.Net.Queue Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)"); #endif if (millis > 0) - await Task.Delay(millis, request.CancelToken).ConfigureAwait(false); + await Task.Delay(millis, request.Options.CancelToken).ConfigureAwait(false); } else { @@ -180,7 +201,7 @@ namespace Discord.Net.Queue #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)"); #endif - await Task.Delay(500, request.CancelToken).ConfigureAwait(false); + await Task.Delay(500, request.Options.CancelToken).ConfigureAwait(false); } continue; } diff --git a/src/Discord.Net.Core/Net/Queue/Requests/JsonRestRequest.cs b/src/Discord.Net.Core/Net/Queue/Requests/JsonRestRequest.cs index 75869d52a..83c5e0eb5 100644 --- a/src/Discord.Net.Core/Net/Queue/Requests/JsonRestRequest.cs +++ b/src/Discord.Net.Core/Net/Queue/Requests/JsonRestRequest.cs @@ -15,7 +15,7 @@ namespace Discord.Net.Queue public override async Task 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); } } } diff --git a/src/Discord.Net.Core/Net/Queue/Requests/MultipartRestRequest.cs b/src/Discord.Net.Core/Net/Queue/Requests/MultipartRestRequest.cs index d132ef395..424a5325e 100644 --- a/src/Discord.Net.Core/Net/Queue/Requests/MultipartRestRequest.cs +++ b/src/Discord.Net.Core/Net/Queue/Requests/MultipartRestRequest.cs @@ -16,7 +16,7 @@ namespace Discord.Net.Queue public override async Task 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); } } } diff --git a/src/Discord.Net.Core/Net/Queue/Requests/RestRequest.cs b/src/Discord.Net.Core/Net/Queue/Requests/RestRequest.cs index 5d5bc1e59..7f358e786 100644 --- a/src/Discord.Net.Core/Net/Queue/Requests/RestRequest.cs +++ b/src/Discord.Net.Core/Net/Queue/Requests/RestRequest.cs @@ -1,7 +1,6 @@ using Discord.Net.Rest; using System; using System.IO; -using System.Threading; using System.Threading.Tasks; namespace Discord.Net.Queue @@ -14,7 +13,6 @@ namespace Discord.Net.Queue public DateTimeOffset? TimeoutAt { get; } public TaskCompletionSource Promise { get; } public RequestOptions Options { get; } - public CancellationToken CancelToken { get; internal set; } public RestRequest(IRestClient client, string method, string endpoint, RequestOptions options) { @@ -24,14 +22,13 @@ namespace Discord.Net.Queue Method = method; Endpoint = endpoint; Options = options; - CancelToken = CancellationToken.None; TimeoutAt = options.Timeout.HasValue ? DateTimeOffset.UtcNow.AddMilliseconds(options.Timeout.Value) : (DateTimeOffset?)null; Promise = new TaskCompletionSource(); } public virtual async Task SendAsync() { - return await Client.SendAsync(Method, Endpoint, CancelToken, Options.HeaderOnly).ConfigureAwait(false); + return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index b82ec29c8..4f5910c53 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -1,11 +1,18 @@ -namespace Discord +using System.Threading; + +namespace Discord { public class RequestOptions { public static RequestOptions Default => new RequestOptions(); - /// 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. + /// + /// 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. + /// public int? Timeout { get; set; } + public CancellationToken CancelToken { get; set; } = CancellationToken.None; + public RetryMode? RetryMode { get; set; } public bool HeaderOnly { get; internal set; } internal bool IgnoreState { get; set; } @@ -13,7 +20,7 @@ internal bool IsClientBucket { get; set; } internal static RequestOptions CreateOrClone(RequestOptions options) - { + { if (options == null) return new RequestOptions(); else diff --git a/src/Discord.Net.Core/RetryMode.cs b/src/Discord.Net.Core/RetryMode.cs new file mode 100644 index 000000000..9dccfc313 --- /dev/null +++ b/src/Discord.Net.Core/RetryMode.cs @@ -0,0 +1,22 @@ +using System; + +namespace Discord +{ + /// Specifies how a request should act in the case of an error. + [Flags] + public enum RetryMode + { + /// If a request fails, an exception is thrown immediately. + AlwaysFail = 0x0, + /// Retry if a request timed out. + RetryTimeouts = 0x1, + /// Retry if a request failed due to a network error. + RetryErrors = 0x2, + /// Retry if a request failed due to a ratelimit. + RetryRatelimit = 0x4, + /// Retry if a request failed due to an HTTP error 502. + Retry502 = 0x8, + /// Continuously retry a request until it times out, its cancel token is triggered, or the server responds with a non-502 error. + AlwaysRetry = RetryTimeouts | RetryErrors | RetryRatelimit | Retry502, + } +} diff --git a/src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs b/src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs index 720c975c0..ee6ad84e1 100644 --- a/src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs +++ b/src/Discord.Net.Rpc/API/DiscordRpcApiClient.cs @@ -69,15 +69,13 @@ namespace Discord.API public ConnectionState ConnectionState { get; private set; } 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); _clientId = clientId; _origin = origin; - FetchCurrentUser = false; - _requestQueue = requestQueue ?? new RequestQueue(); _requests = new ConcurrentDictionary(); diff --git a/src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs index f0dd5f852..9592e2a04 100644 --- a/src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/API/DiscordSocketApiClient.cs @@ -32,11 +32,12 @@ namespace Discord.API 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.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) => { using (var compressed = new MemoryStream(data, index + 2, count - 2)) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs index a150a6d15..2f952bdaa 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs @@ -221,7 +221,5 @@ namespace Discord.WebSocket remove { _recipientRemovedEvent.Remove(value); } } private readonly AsyncEvent> _recipientRemovedEvent = new AsyncEvent>(); - - //TODO: Add PresenceUpdated? VoiceStateUpdated?, VoiceConnected, VoiceDisconnected; } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index fde2d100b..0faa3cbbd 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -130,7 +130,7 @@ namespace Discord.WebSocket 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); } protected override async Task OnLogoutAsync()