fix(a2a-client): stopped registry starts no watch loops

This commit is contained in:
EugeneTes
2026-10-08 14:15:05 +00:00
parent ab714a24ce
commit adee225fcb
3 changed files with 47 additions and 1 deletions

View File

@@ -55,6 +55,9 @@ public sealed class A2AWatchRegistry : IAsyncDisposable
private readonly ConcurrentDictionary<string, byte> _flushRequested = new(StringComparer.Ordinal);
private volatile IA2AResultDelivery _delivery;
/// <summary>Set by <see cref="StopAsync"/>: later starts and re-arms still persist their record but run no loop.</summary>
private volatile bool _stopped;
public A2AWatchRegistry(TimeProvider time, HttpMessageHandler handler, ILogger? logger = null)
{
Time = time;
@@ -97,6 +100,8 @@ public sealed class A2AWatchRegistry : IAsyncDisposable
var watch = new Watch(new WatchRecord(watchId, target, agent.Entry.Name, task.Id, task.ContextId, null, Time.GetUtcNow(), source), workspace);
using (await LockAsync(workspace, cancellationToken).ConfigureAwait(false))
{
// A watch that is finishing (left the map, delivering) isn't seen here, so a start racing it may watch the
// task again: a rare duplicate result, within the registry's at-least-once delivery.
if (_watches.Values.FirstOrDefault(w => w.Record.Target == target && w.Record.TaskId == task.Id) is { } existing)
{
watch.DisposeStop();
@@ -138,6 +143,13 @@ public sealed class A2AWatchRegistry : IAsyncDisposable
watch.DisposeStop();
throw;
}
if (_stopped)
{
// Shutting down: the record stays in pending.json for the next restore, no loop outlives the registry.
_watches.TryRemove(new(watchId, watch));
watch.DisposeStop();
return watch.Record;
}
watch.Loop = Task.Run(() => RunAsync(watch, settings));
return watch.Record;
}
@@ -174,6 +186,8 @@ public sealed class A2AWatchRegistry : IAsyncDisposable
var watch = new Watch(record, workspace);
if (!_watches.TryAdd(record.WatchId, watch)) { watch.DisposeStop(); continue; }
if (!ReferenceEquals(record, stored)) PendingFile.Upsert(workspace, record);
// Checked after the watch is in the map: a StopAsync that set the flag later sees (and stops) it.
if (_stopped) { _watches.TryRemove(new(record.WatchId, watch)); watch.DisposeStop(); return count; }
watch.Loop = Task.Run(() => RunAsync(watch, settings));
count++;
}
@@ -255,6 +269,13 @@ public sealed class A2AWatchRegistry : IAsyncDisposable
var watch = new Watch(record with { Undelivered = null }, workspace);
if (!_watches.TryAdd(record.WatchId, watch)) { watch.DisposeStop(); return; }
PendingFile.Upsert(workspace, watch.Record);
if (_stopped)
{
// Kept in pending.json for the next restore; no loop after StopAsync.
_watches.TryRemove(new(record.WatchId, watch));
watch.DisposeStop();
return;
}
watch.Loop = Task.Run(() => RunAsync(watch, settings));
}
}
@@ -513,6 +534,7 @@ public sealed class A2AWatchRegistry : IAsyncDisposable
/// </summary>
public async Task StopAsync()
{
_stopped = true;
var watches = _watches.ToArray();
foreach (var pair in watches) _watches.TryRemove(pair);
await Task.WhenAll(watches.Select(p => p.Value.StopAsync())).ConfigureAwait(false);