feat: update DebugStreamBridgeActor to use gRPC for streaming events

After receiving the initial snapshot via ClusterClient, the bridge actor
now opens a gRPC server-streaming subscription via SiteStreamGrpcClient
for ongoing AttributeValueChanged/AlarmStateChanged events. Adds NodeA/
NodeB failover with max 3 retries, retry count reset on successful event,
and IWithTimers-based reconnect scheduling.

- DebugStreamBridgeActor: gRPC stream after snapshot, reconnect state machine
- DebugStreamService: inject SiteStreamGrpcClientFactory, resolve gRPC addresses
- ServiceCollectionExtensions: register SiteStreamGrpcClientFactory singleton
- SiteStreamGrpcClient: make SubscribeAsync/Unsubscribe virtual for testability
- SiteStreamGrpcClientFactory: make GetOrCreate virtual for testability
- New test suite: DebugStreamBridgeActorTests (8 tests)
This commit is contained in:
Joseph Doherty
2026-03-21 12:14:24 -04:00
parent 25a6022f7b
commit 2cd43b6992
6 changed files with 503 additions and 16 deletions

View File

@@ -36,9 +36,10 @@ public class SiteStreamGrpcClient : IAsyncDisposable
}
/// <summary>
/// Private constructor for unit testing without a real gRPC channel.
/// Protected constructor for unit testing without a real gRPC channel.
/// Allows subclassing for mock implementations.
/// </summary>
private SiteStreamGrpcClient()
protected SiteStreamGrpcClient()
{
}
@@ -62,7 +63,7 @@ public class SiteStreamGrpcClient : IAsyncDisposable
/// The <paramref name="onEvent"/> callback delivers domain events, and
/// <paramref name="onError"/> lets the caller handle reconnection.
/// </summary>
public async Task SubscribeAsync(
public virtual async Task SubscribeAsync(
string correlationId,
string instanceUniqueName,
Action<object> onEvent,
@@ -109,7 +110,7 @@ public class SiteStreamGrpcClient : IAsyncDisposable
/// <summary>
/// Cancels an active subscription by correlation ID.
/// </summary>
public void Unsubscribe(string correlationId)
public virtual void Unsubscribe(string correlationId)
{
if (_subscriptions.TryRemove(correlationId, out var cts))
{

View File

@@ -21,7 +21,7 @@ public class SiteStreamGrpcClientFactory : IAsyncDisposable, IDisposable
/// <summary>
/// Returns an existing client for the site or creates a new one.
/// </summary>
public SiteStreamGrpcClient GetOrCreate(string siteIdentifier, string grpcEndpoint)
public virtual SiteStreamGrpcClient GetOrCreate(string siteIdentifier, string grpcEndpoint)
{
return _clients.GetOrAdd(siteIdentifier, _ =>
{