Moved (re)connection handling to ConnectionManager
This commit is contained in:
@@ -18,10 +18,9 @@ namespace Discord.Rest
|
||||
public event Func<Task> LoggedOut { add { _loggedOutEvent.Add(value); } remove { _loggedOutEvent.Remove(value); } }
|
||||
private readonly AsyncEvent<Func<Task>> _loggedOutEvent = new AsyncEvent<Func<Task>>();
|
||||
|
||||
internal readonly Logger _restLogger, _queueLogger;
|
||||
internal readonly SemaphoreSlim _connectionLock;
|
||||
private bool _isFirstLogin;
|
||||
private bool _isDisposed;
|
||||
internal readonly Logger _restLogger;
|
||||
private readonly SemaphoreSlim _stateLock;
|
||||
private bool _isFirstLogin, _isDisposed;
|
||||
|
||||
internal API.DiscordRestApiClient ApiClient { get; }
|
||||
internal LogManager LogManager { get; }
|
||||
@@ -35,17 +34,16 @@ namespace Discord.Rest
|
||||
LogManager = new LogManager(config.LogLevel);
|
||||
LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false);
|
||||
|
||||
_connectionLock = new SemaphoreSlim(1, 1);
|
||||
_stateLock = new SemaphoreSlim(1, 1);
|
||||
_restLogger = LogManager.CreateLogger("Rest");
|
||||
_queueLogger = LogManager.CreateLogger("Queue");
|
||||
_isFirstLogin = config.DisplayInitialLog;
|
||||
|
||||
ApiClient.RequestQueue.RateLimitTriggered += async (id, info) =>
|
||||
{
|
||||
if (info == null)
|
||||
await _queueLogger.WarningAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
|
||||
await _restLogger.WarningAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
|
||||
else
|
||||
await _queueLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
|
||||
await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
|
||||
};
|
||||
ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false);
|
||||
}
|
||||
@@ -53,12 +51,12 @@ namespace Discord.Rest
|
||||
/// <inheritdoc />
|
||||
public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true)
|
||||
{
|
||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
||||
await _stateLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await LoginInternalAsync(tokenType, token).ConfigureAwait(false);
|
||||
}
|
||||
finally { _connectionLock.Release(); }
|
||||
finally { _stateLock.Release(); }
|
||||
}
|
||||
private async Task LoginInternalAsync(TokenType tokenType, string token)
|
||||
{
|
||||
@@ -86,17 +84,17 @@ namespace Discord.Rest
|
||||
|
||||
await _loggedInEvent.InvokeAsync().ConfigureAwait(false);
|
||||
}
|
||||
protected virtual Task OnLoginAsync(TokenType tokenType, string token) { return Task.Delay(0); }
|
||||
internal virtual Task OnLoginAsync(TokenType tokenType, string token) { return Task.Delay(0); }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task LogoutAsync()
|
||||
{
|
||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
||||
await _stateLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await LogoutInternalAsync().ConfigureAwait(false);
|
||||
}
|
||||
finally { _connectionLock.Release(); }
|
||||
finally { _stateLock.Release(); }
|
||||
}
|
||||
private async Task LogoutInternalAsync()
|
||||
{
|
||||
@@ -111,7 +109,7 @@ namespace Discord.Rest
|
||||
|
||||
await _loggedOutEvent.InvokeAsync().ConfigureAwait(false);
|
||||
}
|
||||
protected virtual Task OnLogoutAsync() { return Task.Delay(0); }
|
||||
internal virtual Task OnLogoutAsync() { return Task.Delay(0); }
|
||||
|
||||
internal virtual void Dispose(bool disposing)
|
||||
{
|
||||
@@ -161,8 +159,9 @@ namespace Discord.Rest
|
||||
Task<IVoiceRegion> IDiscordClient.GetVoiceRegionAsync(string id)
|
||||
=> Task.FromResult<IVoiceRegion>(null);
|
||||
|
||||
Task IDiscordClient.ConnectAsync() { throw new NotSupportedException(); }
|
||||
Task IDiscordClient.DisconnectAsync() { throw new NotSupportedException(); }
|
||||
|
||||
Task IDiscordClient.StartAsync()
|
||||
=> Task.Delay(0);
|
||||
Task IDiscordClient.StopAsync()
|
||||
=> Task.Delay(0);
|
||||
}
|
||||
}
|
||||
|
||||
199
src/Discord.Net.Rest/ConnectionManager.cs
Normal file
199
src/Discord.Net.Rest/ConnectionManager.cs
Normal file
@@ -0,0 +1,199 @@
|
||||
using Discord.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord
|
||||
{
|
||||
internal class ConnectionManager
|
||||
{
|
||||
public event Func<Task> Connected { add { _connectedEvent.Add(value); } remove { _connectedEvent.Remove(value); } }
|
||||
private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>();
|
||||
public event Func<Exception, bool, Task> Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } }
|
||||
private readonly AsyncEvent<Func<Exception, bool, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, bool, Task>>();
|
||||
|
||||
private readonly SemaphoreSlim _stateLock;
|
||||
private readonly Logger _logger;
|
||||
private readonly int _connectionTimeout;
|
||||
private readonly Func<Task> _onConnecting;
|
||||
private readonly Func<Exception, Task> _onDisconnecting;
|
||||
|
||||
private TaskCompletionSource<bool> _connectionPromise, _readyPromise;
|
||||
private CancellationTokenSource _combinedCancelToken, _reconnectCancelToken, _connectionCancelToken;
|
||||
private Task _task;
|
||||
|
||||
public ConnectionState State { get; private set; }
|
||||
public CancellationToken CancelToken { get; private set; }
|
||||
|
||||
public bool IsCompleted => _readyPromise.Task.IsCompleted;
|
||||
|
||||
internal ConnectionManager(SemaphoreSlim stateLock, Logger logger, int connectionTimeout,
|
||||
Func<Task> onConnecting, Func<Exception, Task> onDisconnecting, Action<Func<Exception, Task>> clientDisconnectHandler)
|
||||
{
|
||||
_stateLock = stateLock;
|
||||
_logger = logger;
|
||||
_connectionTimeout = connectionTimeout;
|
||||
_onConnecting = onConnecting;
|
||||
_onDisconnecting = onDisconnecting;
|
||||
|
||||
clientDisconnectHandler(ex =>
|
||||
{
|
||||
if (ex != null)
|
||||
Error(new Exception("WebSocket connection was closed", ex));
|
||||
else
|
||||
Error(new Exception("WebSocket connection was closed"));
|
||||
return Task.Delay(0);
|
||||
});
|
||||
}
|
||||
|
||||
public virtual async Task StartAsync()
|
||||
{
|
||||
await AcquireConnectionLock().ConfigureAwait(false);
|
||||
var reconnectCancelToken = new CancellationTokenSource();
|
||||
_reconnectCancelToken = new CancellationTokenSource();
|
||||
_task = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Random jitter = new Random();
|
||||
int nextReconnectDelay = 1000;
|
||||
while (!reconnectCancelToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ConnectAsync(reconnectCancelToken).ConfigureAwait(false);
|
||||
nextReconnectDelay = 1000; //Reset delay
|
||||
await _connectionPromise.Task.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
Cancel(); //In case this exception didn't come from another Error call
|
||||
await DisconnectAsync(ex, !reconnectCancelToken.IsCancellationRequested).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error(ex); //In case this exception didn't come from another Error call
|
||||
if (!reconnectCancelToken.IsCancellationRequested)
|
||||
{
|
||||
await _logger.WarningAsync(ex).ConfigureAwait(false);
|
||||
await DisconnectAsync(ex, true).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _logger.ErrorAsync(ex).ConfigureAwait(false);
|
||||
await DisconnectAsync(ex, false).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!reconnectCancelToken.IsCancellationRequested)
|
||||
{
|
||||
//Wait before reconnecting
|
||||
await Task.Delay(nextReconnectDelay, reconnectCancelToken.Token).ConfigureAwait(false);
|
||||
nextReconnectDelay = (nextReconnectDelay * 2) + jitter.Next(-250, 250);
|
||||
if (nextReconnectDelay > 60000)
|
||||
nextReconnectDelay = 60000;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally { _stateLock.Release(); }
|
||||
});
|
||||
}
|
||||
public virtual async Task StopAsync()
|
||||
{
|
||||
Cancel();
|
||||
var task = _task;
|
||||
if (task != null)
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ConnectAsync(CancellationTokenSource reconnectCancelToken)
|
||||
{
|
||||
_connectionCancelToken = new CancellationTokenSource();
|
||||
_combinedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(_connectionCancelToken.Token, reconnectCancelToken.Token);
|
||||
CancelToken = _combinedCancelToken.Token;
|
||||
|
||||
_connectionPromise = new TaskCompletionSource<bool>();
|
||||
State = ConnectionState.Connecting;
|
||||
await _logger.InfoAsync("Connecting").ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
var readyPromise = new TaskCompletionSource<bool>();
|
||||
_readyPromise = readyPromise;
|
||||
|
||||
//Abort connection on timeout
|
||||
var cancelToken = CancelToken;
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(_connectionTimeout, cancelToken).ConfigureAwait(false);
|
||||
readyPromise.TrySetException(new TimeoutException());
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
});
|
||||
|
||||
await _onConnecting().ConfigureAwait(false);
|
||||
|
||||
await _logger.InfoAsync("Connected").ConfigureAwait(false);
|
||||
State = ConnectionState.Connected;
|
||||
await _logger.DebugAsync("Raising Event").ConfigureAwait(false);
|
||||
await _connectedEvent.InvokeAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
private async Task DisconnectAsync(Exception ex, bool isReconnecting)
|
||||
{
|
||||
if (State == ConnectionState.Disconnected) return;
|
||||
State = ConnectionState.Disconnecting;
|
||||
await _logger.InfoAsync("Disconnecting").ConfigureAwait(false);
|
||||
|
||||
await _onDisconnecting(ex).ConfigureAwait(false);
|
||||
|
||||
await _logger.InfoAsync("Disconnected").ConfigureAwait(false);
|
||||
State = ConnectionState.Disconnected;
|
||||
await _disconnectedEvent.InvokeAsync(ex, isReconnecting).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task CompleteAsync()
|
||||
{
|
||||
await _readyPromise.TrySetResultAsync(true).ConfigureAwait(false);
|
||||
}
|
||||
public async Task WaitAsync()
|
||||
{
|
||||
await _readyPromise.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
_readyPromise?.TrySetCanceled();
|
||||
_connectionPromise?.TrySetCanceled();
|
||||
_reconnectCancelToken?.Cancel();
|
||||
_connectionCancelToken?.Cancel();
|
||||
}
|
||||
public void Error(Exception ex)
|
||||
{
|
||||
_readyPromise.TrySetException(ex);
|
||||
_connectionPromise.TrySetException(ex);
|
||||
_connectionCancelToken?.Cancel();
|
||||
}
|
||||
public void CriticalError(Exception ex)
|
||||
{
|
||||
_reconnectCancelToken?.Cancel();
|
||||
Error(ex);
|
||||
}
|
||||
private async Task AcquireConnectionLock()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
await StopAsync().ConfigureAwait(false);
|
||||
if (await _stateLock.WaitAsync(0).ConfigureAwait(false))
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,19 @@ namespace Discord.Rest
|
||||
|
||||
private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config)
|
||||
=> new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent);
|
||||
internal override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
ApiClient.Dispose();
|
||||
}
|
||||
|
||||
protected override async Task OnLoginAsync(TokenType tokenType, string token)
|
||||
internal override async Task OnLoginAsync(TokenType tokenType, string token)
|
||||
{
|
||||
var user = await ApiClient.GetMyUserAsync(new RequestOptions { RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false);
|
||||
ApiClient.CurrentUserId = user.Id;
|
||||
base.CurrentUser = RestSelfUser.Create(this, user);
|
||||
}
|
||||
protected override Task OnLogoutAsync()
|
||||
internal override Task OnLogoutAsync()
|
||||
{
|
||||
_applicationInfo = null;
|
||||
return Task.Delay(0);
|
||||
|
||||
Reference in New Issue
Block a user