fix(ui): auto-reconnect worker connection with retry backoff
Add IndefiniteRetryPolicy for WithAutomaticReconnect, wrap initial
StartAsync in a retry loop so the UI keeps retrying when Worker is
offline at startup, and expose IsReconnecting to StatusBar
("Connecting..." / "Online" / "Offline").
This commit is contained in:
@@ -7,13 +7,32 @@ namespace ClaudeDo.Ui.Services;
|
||||
|
||||
public record ActiveTask(string Slot, string TaskId, DateTime StartedAt);
|
||||
|
||||
sealed class IndefiniteRetryPolicy : IRetryPolicy
|
||||
{
|
||||
private static readonly TimeSpan[] _delays =
|
||||
[
|
||||
TimeSpan.Zero,
|
||||
TimeSpan.FromSeconds(2),
|
||||
TimeSpan.FromSeconds(5),
|
||||
TimeSpan.FromSeconds(10),
|
||||
TimeSpan.FromSeconds(30),
|
||||
];
|
||||
|
||||
public TimeSpan? NextRetryDelay(RetryContext retryContext) =>
|
||||
_delays[Math.Min(retryContext.PreviousRetryCount, _delays.Length - 1)];
|
||||
}
|
||||
|
||||
public partial class WorkerClient : ObservableObject, IAsyncDisposable
|
||||
{
|
||||
private readonly HubConnection _hub;
|
||||
private CancellationTokenSource? _startCts;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isConnected;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isReconnecting;
|
||||
|
||||
public ObservableCollection<ActiveTask> ActiveTasks { get; } = new();
|
||||
|
||||
public event Action<string, string, DateTime>? TaskStartedEvent;
|
||||
@@ -26,18 +45,18 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable
|
||||
{
|
||||
_hub = new HubConnectionBuilder()
|
||||
.WithUrl(signalRUrl)
|
||||
.WithAutomaticReconnect()
|
||||
.WithAutomaticReconnect(new IndefiniteRetryPolicy())
|
||||
.Build();
|
||||
|
||||
_hub.Reconnected += async _ =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => IsConnected = true);
|
||||
Dispatcher.UIThread.Post(() => { IsConnected = true; IsReconnecting = false; });
|
||||
await SeedActiveTasksAsync();
|
||||
};
|
||||
|
||||
_hub.Reconnecting += _ =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => IsConnected = false);
|
||||
Dispatcher.UIThread.Post(() => { IsConnected = false; IsReconnecting = true; });
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
@@ -46,6 +65,7 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
IsConnected = false;
|
||||
IsReconnecting = false;
|
||||
ActiveTasks.Clear();
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
@@ -87,22 +107,43 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable
|
||||
});
|
||||
}
|
||||
|
||||
public async Task StartAsync()
|
||||
public Task StartAsync()
|
||||
{
|
||||
try
|
||||
_startCts = new CancellationTokenSource();
|
||||
_ = ConnectWithRetryAsync(_startCts.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task ConnectWithRetryAsync(CancellationToken ct)
|
||||
{
|
||||
var delays = new[] { 0, 2, 5, 10, 30 };
|
||||
int attempt = 0;
|
||||
Dispatcher.UIThread.Post(() => IsReconnecting = true);
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
await _hub.StartAsync();
|
||||
Dispatcher.UIThread.Post(() => IsConnected = true);
|
||||
await SeedActiveTasksAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => IsConnected = false);
|
||||
try
|
||||
{
|
||||
await _hub.StartAsync(ct);
|
||||
Dispatcher.UIThread.Post(() => { IsConnected = true; IsReconnecting = false; });
|
||||
await SeedActiveTasksAsync();
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
var delay = delays[Math.Min(attempt++, delays.Length - 1)];
|
||||
try { await Task.Delay(TimeSpan.FromSeconds(delay), ct); }
|
||||
catch (OperationCanceledException) { return; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync()
|
||||
{
|
||||
_startCts?.Cancel();
|
||||
try { await _hub.StopAsync(); } catch { /* swallow */ }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user