From d7d7e88d91b0bc255f890e360259005cee8c8c51 Mon Sep 17 00:00:00 2001 From: JT Date: Sun, 10 Feb 2019 12:31:50 +0800 Subject: [PATCH] v1.3.0 - Reliable shutdown (#6) * Rework shutdown handling so client deadlocks don't prevent exit * Update csprojs * Update README.md * Update README.md * Cleanup * Typo * More cleanup --- .../Discord.Addons.Hosting.csproj | 7 ++-- .../DiscordHostBuilderExtensions.cs | 6 ++-- .../DiscordHostedService.cs | 11 +++--- Discord.Addons.Hosting/LogAdapter.cs | 9 +++-- .../Reliability/ReliableDiscordHost.cs | 36 +++++++------------ .../Reliability/ReliableHostExtensions.cs | 10 +++--- README.md | 8 ++++- Samples/SampleBotSerilog/Program.cs | 2 +- .../SampleBotSerilog/SampleBotSerilog.csproj | 2 +- Samples/SampleBotSimple/Program.cs | 8 ++--- 10 files changed, 45 insertions(+), 54 deletions(-) diff --git a/Discord.Addons.Hosting/Discord.Addons.Hosting.csproj b/Discord.Addons.Hosting/Discord.Addons.Hosting.csproj index 243f99c..37c7f94 100644 --- a/Discord.Addons.Hosting/Discord.Addons.Hosting.csproj +++ b/Discord.Addons.Hosting/Discord.Addons.Hosting.csproj @@ -3,17 +3,18 @@ netstandard2.0 Discord.Addons.Hosting - 1.2.1 + 1.3.0 Hawxy Simplifying Discord.Net hosting with .NET Generic Host (Microsoft.Extensions.Hosting) true - https://github.com/Hawxy/Discord.Addons.Hosting/blob/master/LICENSE + Apache-2.0 https://github.com/Hawxy/Discord.Addons.Hosting https://github.com/Hawxy/Discord.Addons.Hosting git https://i.imgur.com/ofsSSut.png - Hawxy 2018 + Hawxy 2018-2019 discord,discord.net,addon,hosting,microsoft.extensions.hosting + 7.3 diff --git a/Discord.Addons.Hosting/DiscordHostBuilderExtensions.cs b/Discord.Addons.Hosting/DiscordHostBuilderExtensions.cs index 871e3a4..cf41478 100644 --- a/Discord.Addons.Hosting/DiscordHostBuilderExtensions.cs +++ b/Discord.Addons.Hosting/DiscordHostBuilderExtensions.cs @@ -82,7 +82,7 @@ public static IHostBuilder UseCommandService(this IHostBuilder builder) builder.ConfigureServices((context, collection) => { if (collection.Any(x => x.ServiceType == typeof(CommandService))) - throw new InvalidOperationException($"Cannot add more than one CommandService to host"); + throw new InvalidOperationException("Cannot add more than one CommandService to host"); collection.AddSingleton(); }); return builder; @@ -107,7 +107,7 @@ public static IHostBuilder UseCommandService(this IHostBuilder builder, Action { if (collection.Any(x => x.ServiceType == typeof(CommandService))) - throw new InvalidOperationException($"Cannot add more than one CommandService to host"); + throw new InvalidOperationException("Cannot add more than one CommandService to host"); var csc = new CommandServiceConfig(); config(context, csc); @@ -137,7 +137,7 @@ public static IHostBuilder ConfigureDiscordLogFormat(this IHostBuilder builder, builder.ConfigureServices((context, collection) => { if (collection.Any(x => x.ServiceType == typeof(Func))) - throw new InvalidOperationException($"Cannot add more than one formatter to host"); + throw new InvalidOperationException("Cannot add more than one formatter to host"); collection.AddSingleton(formatter); }); diff --git a/Discord.Addons.Hosting/DiscordHostedService.cs b/Discord.Addons.Hosting/DiscordHostedService.cs index 8caaf8d..460e262 100644 --- a/Discord.Addons.Hosting/DiscordHostedService.cs +++ b/Discord.Addons.Hosting/DiscordHostedService.cs @@ -38,16 +38,11 @@ public DiscordHostedService(ILogger logger, IConfiguration _config = config; _client = client; - //workaround for correct logging category - adapter.UseLogger(logger); - client.Log += adapter.Log; if (commandService != null) - { commandService.Log += adapter.Log; - } - + } public async Task StartAsync(CancellationToken cancellationToken) { @@ -59,7 +54,9 @@ public async Task StartAsync(CancellationToken cancellationToken) public async Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Discord.Net hosted service is stopping"); - await _client.StopAsync(); + var task = _client.StopAsync(); + await Task.WhenAny(task, Task.Delay(-1, cancellationToken)); + if (cancellationToken.IsCancellationRequested) _logger.LogCritical("Discord.NET client could not be stopped within the given timeout and may have permanently deadlocked"); } public void Dispose() diff --git a/Discord.Addons.Hosting/LogAdapter.cs b/Discord.Addons.Hosting/LogAdapter.cs index 8b4d2af..2cd8de7 100644 --- a/Discord.Addons.Hosting/LogAdapter.cs +++ b/Discord.Addons.Hosting/LogAdapter.cs @@ -6,19 +6,18 @@ namespace Discord.Addons.Hosting { internal class LogAdapter { - private ILogger _logger; + private readonly ILogger _logger; private readonly Func _formatter; - public LogAdapter(Func formatter = null) + public LogAdapter(ILoggerFactory loggerFactory, Func formatter = null) { + _logger = loggerFactory.CreateLogger("Discord.Client"); _formatter = formatter ?? DefaultFormatter; } - - public void UseLogger(ILogger logger) => _logger = logger; public Task Log(LogMessage message) { - _logger.Log(GetLogLevel(message.Severity), default(EventId), message, message.Exception, _formatter); + _logger.Log(GetLogLevel(message.Severity), default, message, message.Exception, _formatter); return Task.CompletedTask; } diff --git a/Discord.Addons.Hosting/Reliability/ReliableDiscordHost.cs b/Discord.Addons.Hosting/Reliability/ReliableDiscordHost.cs index 22896f4..6df6eb9 100644 --- a/Discord.Addons.Hosting/Reliability/ReliableDiscordHost.cs +++ b/Discord.Addons.Hosting/Reliability/ReliableDiscordHost.cs @@ -31,7 +31,6 @@ public ReliableDiscordHost(DiscordSocketClient discord, ILogger logger, IHost ho private Task ConnectedAsync() { - _logger.LogDebug("Discord client reconnected, resetting cancel token..."); _cts.Cancel(); _cts = new CancellationTokenSource(); _logger.LogDebug("Discord client reconnected, cancel token reset."); @@ -60,32 +59,21 @@ private async Task CheckStateAsync() return; } - _logger.LogCritical("Client did not reconnect in time, restarting host"); - await _host.StopAsync(); - await _host.StartAsync(); - } - - private void Dispose(bool disposing) - { - if (disposing) + _logger.LogCritical("Client did not reconnect in time, attempting to restart host..."); + await _host.StopAsync().ContinueWith(async _ => { - _logger.LogInformation("Disposing Reliability Service"); - _discord.Connected -= ConnectedAsync; - _discord.Disconnected -= DisconnectedAsync; - _cts?.Cancel(); - _cts?.Dispose(); - } - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); + await _host.StartAsync(); + }); + } - ~ReliableDiscordHost() - { - Dispose(false); + public void Dispose() + { + _logger.LogInformation("Disposing Reliability Service"); + _discord.Connected -= ConnectedAsync; + _discord.Disconnected -= DisconnectedAsync; + _cts?.Cancel(); + _cts?.Dispose(); } } } \ No newline at end of file diff --git a/Discord.Addons.Hosting/Reliability/ReliableHostExtensions.cs b/Discord.Addons.Hosting/Reliability/ReliableHostExtensions.cs index 0d36f83..27dcfb1 100644 --- a/Discord.Addons.Hosting/Reliability/ReliableHostExtensions.cs +++ b/Discord.Addons.Hosting/Reliability/ReliableHostExtensions.cs @@ -17,7 +17,7 @@ public static class ReliableHostExtensions private static CancellationTokenSource _cts; /// - /// Adds the Reliability Service and Runs the host. This function will only return if is called elsewhere. Do not use in combination with + /// Adds the Reliability Service and Runs the host. This function will only return if is called elsewhere. /// /// The host to configure. public static async Task RunReliablyAsync(this IHost host) @@ -64,9 +64,11 @@ public static async Task StopReliablyAsync(this IHost host) throw new InvalidOperationException("Reliable host is null. Shutdown the host normally with StopAsync instead."); _reliable.Dispose(); _reliable = null; - await host.StopAsync(); - _cts.Cancel(); - _cts.Dispose(); + await host.StopAsync().ContinueWith(_ => + { + _cts.Cancel(); + _cts.Dispose(); + }); } } } diff --git a/README.md b/README.md index e20ab0d..e7c54ad 100644 --- a/README.md +++ b/README.md @@ -62,9 +62,15 @@ Serilog should be added to the host with ```Serilog.Extensions.Hosting```. See the Serilog [example](https://github.com/Hawxy/Discord.Addons.Hosting/tree/master/Samples/SampleBotSerilog) for usage +### Shutdown + +When shutdown is requested, the host will wait a maximum of 5 seconds for services to stop before timing out. + +If you're finding that this isn't enough time, you can modify the shutdown timeout via the [ShutdownTimeout host setting](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-2.2#shutdown-timeout). + ### Reliability -Discord.Net can occasionally fail to reconnect after an extended outage. This library provides a basic solution that will automatically attempt to restart the host on a failure. Please note that this functionality is experimental and does not guarantee that the client will *always* recover. +Discord.Net can occasionally fail to reconnect after an extended outage. This library provides a basic solution that will automatically attempt to restart the host on a failure. Please note that this functionality is experimental and does not guarantee that the client will *always* recover. Please note that this feature is also affected by the shutdown timeout set above. To use the reliability extensions, start the host with ```await host.RunReliablyAsync()```. diff --git a/Samples/SampleBotSerilog/Program.cs b/Samples/SampleBotSerilog/Program.cs index cab68c1..f07c9f3 100644 --- a/Samples/SampleBotSerilog/Program.cs +++ b/Samples/SampleBotSerilog/Program.cs @@ -68,7 +68,7 @@ static async Task Main() .UseConsoleLifetime(); //Start and stop just by hitting enter - //See https://github.com/aspnet/Hosting/tree/master/samples/GenericHostSample for other control patterns + //See https://github.com/aspnet/Extensions/tree/master/src/Hosting/samples/GenericHostSample for other control patterns var host = builder.Build(); using (host) { diff --git a/Samples/SampleBotSerilog/SampleBotSerilog.csproj b/Samples/SampleBotSerilog/SampleBotSerilog.csproj index aec093a..a2b5d1a 100644 --- a/Samples/SampleBotSerilog/SampleBotSerilog.csproj +++ b/Samples/SampleBotSerilog/SampleBotSerilog.csproj @@ -12,7 +12,7 @@ - + diff --git a/Samples/SampleBotSimple/Program.cs b/Samples/SampleBotSimple/Program.cs index cd8d2a1..2ff6cd8 100644 --- a/Samples/SampleBotSimple/Program.cs +++ b/Samples/SampleBotSimple/Program.cs @@ -1,5 +1,4 @@ -using System; -using System.IO; +using System.IO; using System.Threading.Tasks; using Discord.Addons.Hosting; using Discord.Addons.Hosting.Reliability; @@ -32,7 +31,7 @@ static async Task Main() { x.SetMinimumLevel(LogLevel.Information); //This works but isn't very pretty. I would highly suggest using Serilog or some other third-party logger - //See https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-2.1#built-in-logging-providers for more logging options + //See https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-2.2#built-in-logging-providers for more logging options x.AddConsole(); //Inject ILogger in any services/modules that require logging @@ -66,10 +65,9 @@ static async Task Main() await host.Services.GetRequiredService().InitializeAsync(); //Fire and forget. Will run until console is closed or the service is stopped. Basically the same as normally running the bot. await host.RunAsync(); - //If you want the host to attempt a restart due to a client reconnect deadlock, use the Reliability extension. + //If you want the host to attempt a restart when the client fails to reconnect, use the Reliability extension. //await host.RunReliablyAsync(); } - } } }