fix(communication): cancel-only CTS replacement, deferred channel disposal, single-endpoint debug stream (arch-review 2026-08-19 P3-1 + P3-2 + P3-3)

This commit is contained in:
Joseph Doherty
2026-08-19 14:38:33 -04:00
parent fd4cb30273
commit 412406e7c5
8 changed files with 566 additions and 50 deletions
@@ -120,7 +120,7 @@ public class SiteStreamGrpcClientFactory : IAsyncDisposable, IDisposable
foreach (var key in _clients.Keys.Where(k => k.Site == siteIdentifier).ToList())
{
if (_clients.TryRemove(key, out var client))
await client.DisposeAsync();
await DisposeClientAsync(client, key);
}
// Drop the cached preshared key too, so a site removed and re-added under the same
@@ -129,16 +129,39 @@ public class SiteStreamGrpcClientFactory : IAsyncDisposable, IDisposable
}
/// <summary>
/// Asynchronously disposes all cached clients and clears the cache.
/// Asynchronously disposes all cached clients and clears the cache. Each client's disposal
/// is fault-isolated: one throwing client must not abort the loop and strand every later
/// client's channel undisposed (arch-review 2026-08-19 P3-1).
/// </summary>
/// <returns>A value task that completes when all clients have been disposed.</returns>
public async ValueTask DisposeAsync()
{
foreach (var client in _clients.Values)
foreach (var entry in _clients)
{
await DisposeClientAsync(entry.Value, entry.Key);
}
_clients.Clear();
}
/// <summary>
/// Disposes one client, logging and swallowing any failure so the caller's disposal loop
/// continues to the remaining clients.
/// </summary>
/// <param name="client">The client to dispose.</param>
/// <param name="key">Cache key of the client, for diagnostics.</param>
/// <returns>A task that completes when the client has been disposed (or its failure logged).</returns>
private async Task DisposeClientAsync(SiteStreamGrpcClient client, (string Site, string Endpoint) key)
{
try
{
await client.DisposeAsync();
}
_clients.Clear();
catch (Exception ex)
{
_loggerFactory.CreateLogger<SiteStreamGrpcClientFactory>().LogWarning(
ex, "Disposing the site stream gRPC client for site {SiteId} endpoint {Endpoint} failed",
key.Site, key.Endpoint);
}
}
/// <summary>
@@ -147,12 +170,22 @@ public class SiteStreamGrpcClientFactory : IAsyncDisposable, IDisposable
/// a stall/deadlock risk during host shutdown). Each
/// <see cref="SiteStreamGrpcClient"/> releases all of its resources
/// synchronously, so we dispose them directly with no async path.
/// Fault-isolated per client, exactly as <see cref="DisposeAsync"/> is.
/// </summary>
public void Dispose()
{
foreach (var client in _clients.Values)
foreach (var entry in _clients)
{
client.Dispose();
try
{
entry.Value.Dispose();
}
catch (Exception ex)
{
_loggerFactory.CreateLogger<SiteStreamGrpcClientFactory>().LogWarning(
ex, "Disposing the site stream gRPC client for site {SiteId} endpoint {Endpoint} failed",
entry.Key.Site, entry.Key.Endpoint);
}
}
_clients.Clear();
}