Stop log background loops cleanly during shutdown.

Cancel the hosted-service watcher loops before the service provider is disposed so file-change and app-log pushes do not try to create scopes after shutdown begins.
This commit is contained in:
2026-04-11 21:53:45 +10:00
parent 43ba5116ac
commit 350df71b4f
2 changed files with 84 additions and 11 deletions
@@ -17,6 +17,8 @@ public sealed class AppLogsPushTriggeredHostedService : IApplicationHostedServic
private readonly Channel<byte> _channel; private readonly Channel<byte> _channel;
private readonly ILogsService _logsService; private readonly ILogsService _logsService;
private readonly ILogger<AppLogsPushTriggeredHostedService> _logger; private readonly ILogger<AppLogsPushTriggeredHostedService> _logger;
private CancellationTokenSource? _stopCts;
private Task? _runLoopTask;
public AppLogsPushTriggeredHostedService( public AppLogsPushTriggeredHostedService(
Channel<byte> appLogsPushChannel, Channel<byte> appLogsPushChannel,
@@ -30,11 +32,35 @@ public sealed class AppLogsPushTriggeredHostedService : IApplicationHostedServic
public Task StartAsync(CancellationToken ct) public Task StartAsync(CancellationToken ct)
{ {
_ = RunLoopAsync(ct); _stopCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
_runLoopTask = RunLoopAsync(_stopCts.Token);
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task StopAsync(CancellationToken ct) => Task.CompletedTask; public async Task StopAsync(CancellationToken ct)
{
var stopCts = _stopCts;
_stopCts = null;
if (stopCts != null)
{
try { stopCts.Cancel(); }
finally { stopCts.Dispose(); }
}
var runLoopTask = _runLoopTask;
_runLoopTask = null;
if (runLoopTask == null)
return;
try
{
await runLoopTask;
}
catch (OperationCanceledException)
{
// expected during shutdown
}
}
private async Task RunLoopAsync(CancellationToken ct) private async Task RunLoopAsync(CancellationToken ct)
{ {
@@ -21,6 +21,8 @@ public sealed class LogsFileWatcherHostedService : IApplicationHostedService
private FileSystemWatcher? _watcher; private FileSystemWatcher? _watcher;
private string? _currentPath; private string? _currentPath;
private readonly object _lock = new(); private readonly object _lock = new();
private CancellationTokenSource? _stopCts;
private Task? _runLoopTask;
public LogsFileWatcherHostedService( public LogsFileWatcherHostedService(
IDnsmasqConfigSetService configSetService, IDnsmasqConfigSetService configSetService,
@@ -34,11 +36,41 @@ public sealed class LogsFileWatcherHostedService : IApplicationHostedService
public Task StartAsync(CancellationToken ct) public Task StartAsync(CancellationToken ct)
{ {
_ = RunLoopAsync(ct); _stopCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
_runLoopTask = RunLoopAsync(_stopCts.Token);
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task StopAsync(CancellationToken ct) => Task.CompletedTask; public async Task StopAsync(CancellationToken ct)
{
var stopCts = _stopCts;
_stopCts = null;
if (stopCts != null)
{
try { stopCts.Cancel(); }
finally { stopCts.Dispose(); }
}
lock (_lock)
{
DisposeWatcherLocked();
_currentPath = null;
}
var runLoopTask = _runLoopTask;
_runLoopTask = null;
if (runLoopTask == null)
return;
try
{
await runLoopTask;
}
catch (OperationCanceledException)
{
// expected during shutdown
}
}
private async Task RunLoopAsync(CancellationToken ct) private async Task RunLoopAsync(CancellationToken ct)
{ {
@@ -67,8 +99,7 @@ public sealed class LogsFileWatcherHostedService : IApplicationHostedService
lock (_lock) lock (_lock)
{ {
_watcher?.Dispose(); DisposeWatcherLocked();
_watcher = null;
_currentPath = null; _currentPath = null;
} }
} }
@@ -82,8 +113,7 @@ public sealed class LogsFileWatcherHostedService : IApplicationHostedService
if (string.Equals(_currentPath, normalized, StringComparison.Ordinal)) if (string.Equals(_currentPath, normalized, StringComparison.Ordinal))
return; return;
_watcher?.Dispose(); DisposeWatcherLocked();
_watcher = null;
_currentPath = normalized; _currentPath = normalized;
if (string.IsNullOrEmpty(normalized)) if (string.IsNullOrEmpty(normalized))
@@ -126,18 +156,35 @@ public sealed class LogsFileWatcherHostedService : IApplicationHostedService
private void OnFileChanged(object sender, FileSystemEventArgs e) private void OnFileChanged(object sender, FileSystemEventArgs e)
{ {
_ = InvokePushAsync(); var ct = _stopCts?.Token ?? CancellationToken.None;
if (ct.IsCancellationRequested)
return;
_ = InvokePushAsync(ct);
} }
private async Task InvokePushAsync() private async Task InvokePushAsync(CancellationToken ct)
{ {
try try
{ {
await _logsService.RunAndPushDnsmasqLogsAsync(CancellationToken.None); await _logsService.RunAndPushDnsmasqLogsAsync(ct);
}
catch (OperationCanceledException)
{
// expected during shutdown
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogDebug(ex, "Error pushing logs on file change"); _logger.LogDebug(ex, "Error pushing logs on file change");
} }
} }
private void DisposeWatcherLocked()
{
if (_watcher == null)
return;
_watcher.Changed -= OnFileChanged;
_watcher.Dispose();
_watcher = null;
}
} }