fix(ui): harden worker auto-reconnect lifecycle

- Fix _startCts lifecycle leak: old CTS is cancelled and disposed before replacement
- Guard StartAsync re-entrancy: early-return if retry loop is still running (lock + IsCompleted check)
- Await _retryLoopTask in both StopAsync and DisposeAsync before stopping/disposing the hub

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mika Kuns
2026-04-13 14:50:34 +02:00
parent 36ef624c51
commit fdf357be8a

View File

@@ -26,6 +26,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable
{ {
private readonly HubConnection _hub; private readonly HubConnection _hub;
private CancellationTokenSource? _startCts; private CancellationTokenSource? _startCts;
private Task _retryLoopTask = Task.CompletedTask;
private readonly object _startLock = new();
[ObservableProperty] [ObservableProperty]
private bool _isConnected; private bool _isConnected;
@@ -109,8 +111,18 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable
public Task StartAsync() public Task StartAsync()
{ {
lock (_startLock)
{
if (!_retryLoopTask.IsCompleted)
return Task.CompletedTask;
var old = _startCts;
_startCts = new CancellationTokenSource(); _startCts = new CancellationTokenSource();
_ = ConnectWithRetryAsync(_startCts.Token); old?.Cancel();
old?.Dispose();
_retryLoopTask = ConnectWithRetryAsync(_startCts.Token);
}
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -144,6 +156,7 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable
public async Task StopAsync() public async Task StopAsync()
{ {
_startCts?.Cancel(); _startCts?.Cancel();
try { await _retryLoopTask; } catch (OperationCanceledException) { } catch { /* swallow */ }
try { await _hub.StopAsync(); } catch { /* swallow */ } try { await _hub.StopAsync(); } catch { /* swallow */ }
} }
@@ -183,6 +196,7 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
_startCts?.Cancel(); _startCts?.Cancel();
try { await _retryLoopTask; } catch (OperationCanceledException) { } catch { /* swallow */ }
await _hub.DisposeAsync(); await _hub.DisposeAsync();
} }