Skip to content

Commit

Permalink
v1.3.0 - Reliable shutdown (#6)
Browse files Browse the repository at this point in the history
* Rework shutdown handling so client deadlocks don't prevent exit

* Update csprojs

* Update README.md

* Update README.md

* Cleanup

* Typo

* More cleanup
  • Loading branch information
Hawxy authored Feb 10, 2019
1 parent 789f6db commit d7d7e88
Show file tree
Hide file tree
Showing 10 changed files with 45 additions and 54 deletions.
7 changes: 4 additions & 3 deletions Discord.Addons.Hosting/Discord.Addons.Hosting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>Discord.Addons.Hosting</PackageId>
<Version>1.2.1</Version>
<Version>1.3.0</Version>
<Authors>Hawxy</Authors>
<Description>Simplifying Discord.Net hosting with .NET Generic Host (Microsoft.Extensions.Hosting)</Description>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageLicenseUrl>https://github.com/Hawxy/Discord.Addons.Hosting/blob/master/LICENSE</PackageLicenseUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/Hawxy/Discord.Addons.Hosting</PackageProjectUrl>
<RepositoryUrl>https://github.com/Hawxy/Discord.Addons.Hosting</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageIconUrl>https://i.imgur.com/ofsSSut.png</PackageIconUrl>
<Copyright>Hawxy 2018</Copyright>
<Copyright>Hawxy 2018-2019</Copyright>
<PackageTags>discord,discord.net,addon,hosting,microsoft.extensions.hosting</PackageTags>
<LangVersion>7.3</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
6 changes: 3 additions & 3 deletions Discord.Addons.Hosting/DiscordHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CommandService>();
});
return builder;
Expand All @@ -107,7 +107,7 @@ public static IHostBuilder UseCommandService(this IHostBuilder builder, Action<H
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");

var csc = new CommandServiceConfig();
config(context, csc);
Expand Down Expand Up @@ -137,7 +137,7 @@ public static IHostBuilder ConfigureDiscordLogFormat(this IHostBuilder builder,
builder.ConfigureServices((context, collection) =>
{
if (collection.Any(x => x.ServiceType == typeof(Func<LogMessage, Exception, string>)))
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);
});

Expand Down
11 changes: 4 additions & 7 deletions Discord.Addons.Hosting/DiscordHostedService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,11 @@ public DiscordHostedService(ILogger<DiscordHostedService> 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)
{
Expand All @@ -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()
Expand Down
9 changes: 4 additions & 5 deletions Discord.Addons.Hosting/LogAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,18 @@ namespace Discord.Addons.Hosting
{
internal class LogAdapter
{
private ILogger _logger;
private readonly ILogger _logger;
private readonly Func<LogMessage, Exception, string> _formatter;

public LogAdapter(Func<LogMessage, Exception, string> formatter = null)
public LogAdapter(ILoggerFactory loggerFactory, Func<LogMessage, Exception, string> 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;
}

Expand Down
36 changes: 12 additions & 24 deletions Discord.Addons.Hosting/Reliability/ReliableDiscordHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down Expand Up @@ -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();
}
}
}
10 changes: 6 additions & 4 deletions Discord.Addons.Hosting/Reliability/ReliableHostExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static class ReliableHostExtensions
private static CancellationTokenSource _cts;

/// <summary>
/// Adds the Reliability Service and Runs the host. This function will only return if <see cref="StopReliablyAsync"/> is called elsewhere. Do not use in combination with <see cref="WithReliability"/>
/// Adds the Reliability Service and Runs the host. This function will only return if <see cref="StopReliablyAsync"/> is called elsewhere.
/// </summary>
/// <param name="host">The host to configure.</param>
public static async Task RunReliablyAsync(this IHost host)
Expand Down Expand Up @@ -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();
});
}
}
}
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()```.

Expand Down
2 changes: 1 addition & 1 deletion Samples/SampleBotSerilog/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
2 changes: 1 addition & 1 deletion Samples/SampleBotSerilog/SampleBotSerilog.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="2.2.0" />
<PackageReference Include="Serilog" Version="2.7.1" />
<PackageReference Include="Serilog" Version="2.8.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
</ItemGroup>
Expand Down
8 changes: 3 additions & 5 deletions Samples/SampleBotSimple/Program.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -66,10 +65,9 @@ static async Task Main()
await host.Services.GetRequiredService<CommandHandler>().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();
}

}
}
}

0 comments on commit d7d7e88

Please sign in to comment.