feat: Release v0.2.2 with shutdown improvements and connection handling fixes

This commit is contained in:
HueByte
2026-02-19 13:01:51 +01:00
parent 4c192717de
commit 552fe6afa3
6 changed files with 72 additions and 35 deletions
+1
View File
@@ -4,6 +4,7 @@ Release history for EchoHub.
## Releases ## Releases
- [v0.2.2](v0.2.2.md) - Shutdown Fix
- [v0.2.1](v0.2.1.md) - Shutdown & CI Fixes - [v0.2.1](v0.2.1.md) - Shutdown & CI Fixes
- [v0.2.0](v0.2.0.md) - IRC Gateway - [v0.2.0](v0.2.0.md) - IRC Gateway
- [v0.1.1](v0.1.1.md) - Directory Connection Self-Healing - [v0.1.1](v0.1.1.md) - Directory Connection Self-Healing
+2
View File
@@ -1,5 +1,7 @@
- name: Overview - name: Overview
href: index.md href: index.md
- name: v0.2.2
href: v0.2.2.md
- name: v0.2.1 - name: v0.2.1
href: v0.2.1.md href: v0.2.1.md
- name: v0.2.0 - name: v0.2.0
+7
View File
@@ -0,0 +1,7 @@
# v0.2.2 - Shutdown Fix
## Fixes
- Actually fixed server hanging on Ctrl+C — replaced `await using` with explicit dispose bounded to 3 seconds, so a stuck `HubConnection` can no longer block shutdown
- Reduced host shutdown timeout from 30s (default) to 5s
- Caught `OperationCanceledException` in the directory service reconnect loop so cancellation exits immediately instead of propagating through dispose
+1 -1
View File
@@ -1,6 +1,6 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>0.2.1</Version> <Version>0.2.2</Version>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn> <NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup> </PropertyGroup>
+4
View File
@@ -35,6 +35,10 @@ while (true)
{ {
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// ── Host options ────────────────────────────────────────────────────
builder.Services.Configure<HostOptions>(options =>
options.ShutdownTimeout = TimeSpan.FromSeconds(5));
// ── Serilog ────────────────────────────────────────────────────────── // ── Serilog ──────────────────────────────────────────────────────────
builder.Host.UseSerilog((context, config) => builder.Host.UseSerilog((context, config) =>
config.ReadFrom.Configuration(context.Configuration)); config.ReadFrom.Configuration(context.Configuration));
@@ -43,46 +43,57 @@ public sealed class ServerDirectoryService(
// Outer loop: rebuilds the connection if automatic reconnect permanently fails // Outer loop: rebuilds the connection if automatic reconnect permanently fails
while (!stoppingToken.IsCancellationRequested) while (!stoppingToken.IsCancellationRequested)
{ {
await using var connection = BuildConnection(); var connection = BuildConnection();
_connection = connection; _connection = connection;
var connectionPermanentlyClosed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); try
connection.Reconnected += async _ =>
{ {
logger.LogInformation("Reconnected to directory — re-registering server"); var connectionPermanentlyClosed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
_lastReportedUserCount = -1;
connection.Reconnected += async _ =>
{
logger.LogInformation("Reconnected to directory — re-registering server");
_lastReportedUserCount = -1;
await RegisterAsync(serverName, description, host);
};
connection.Closed += ex =>
{
if (ex is not null)
logger.LogWarning(ex, "Directory connection permanently closed — will rebuild");
else
logger.LogWarning("Directory connection permanently closed — will rebuild");
connectionPermanentlyClosed.TrySetResult();
return Task.CompletedTask;
};
// Connect with retry
if (!await ConnectWithRetryAsync(connection, stoppingToken))
return;
logger.LogInformation("Successfully connected to EchoHubSpace API at {Url}", DirectoryHubUrl);
await RegisterAsync(serverName, description, host); await RegisterAsync(serverName, description, host);
};
connection.Closed += ex => // Poll user count until the connection is permanently closed or cancellation
await PollUserCountAsync(connection, connectionPermanentlyClosed.Task, stoppingToken);
if (stoppingToken.IsCancellationRequested)
return;
// Connection was permanently closed — wait briefly then rebuild
logger.LogInformation("Rebuilding directory connection...");
await Task.Delay(ReconnectBaseDelay, stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{ {
if (ex is not null)
logger.LogWarning(ex, "Directory connection permanently closed — will rebuild");
else
logger.LogWarning("Directory connection permanently closed — will rebuild");
connectionPermanentlyClosed.TrySetResult();
return Task.CompletedTask;
};
// Connect with retry
if (!await ConnectWithRetryAsync(connection, stoppingToken))
return; return;
}
logger.LogInformation("Successfully connected to EchoHubSpace API at {Url}", DirectoryHubUrl); finally
await RegisterAsync(serverName, description, host); {
_connection = null;
// Poll user count until the connection is permanently closed or cancellation await DisposeConnectionAsync(connection);
await PollUserCountAsync(connection, connectionPermanentlyClosed.Task, stoppingToken); }
if (stoppingToken.IsCancellationRequested)
return;
// Connection was permanently closed — wait briefly then rebuild
_connection = null;
logger.LogInformation("Rebuilding directory connection...");
await Task.Delay(ReconnectBaseDelay, stoppingToken);
} }
} }
@@ -175,9 +186,21 @@ public sealed class ServerDirectoryService(
} }
} }
private static async Task DisposeConnectionAsync(HubConnection connection)
{
try
{
await connection.DisposeAsync()
.AsTask().WaitAsync(TimeSpan.FromSeconds(3));
}
catch
{
// Don't let a slow dispose block shutdown
}
}
public override async Task StopAsync(CancellationToken cancellationToken) public override async Task StopAsync(CancellationToken cancellationToken)
{ {
// Cancel ExecuteAsync first — it disposes the connection via await using
await base.StopAsync(cancellationToken); await base.StopAsync(cancellationToken);
_connection = null; _connection = null;
} }