From b7f8632db81d06f4855ae1a86c036910d3de4802 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 13 Jul 2026 10:19:51 -0400 Subject: [PATCH] chore(comm): ctor-inject aggregator reconnect/stability tuning, remove process-global statics (plan R2-02 T12) --- .../Actors/SiteAlarmAggregatorActor.cs | 24 ++++++++++++------- .../SiteAlarmLiveCacheService.cs | 4 +++- .../Grpc/SiteAlarmAggregatorActorTests.cs | 10 +++++--- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteAlarmAggregatorActor.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteAlarmAggregatorActor.cs index 869d9886..c1c53432 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteAlarmAggregatorActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteAlarmAggregatorActor.cs @@ -63,15 +63,15 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers /// True while a coalesced publish is armed (dirty deltas awaiting one tick). Actor-thread only. private bool _publishPending; - /// Delay between gRPC reconnection attempts. Settable for tests. - internal static TimeSpan ReconnectDelay { get; set; } = TimeSpan.FromSeconds(5); + /// Delay between gRPC reconnection attempts (ctor-injected; production default 5s). + private readonly TimeSpan _reconnectDelay; /// /// How long a freshly-opened gRPC stream must stay up before its retry budget is - /// considered recovered (mirrors ). - /// Settable for tests. + /// considered recovered (mirrors ); + /// ctor-injected (production default 60s). /// - internal static TimeSpan StabilityWindow { get; set; } = TimeSpan.FromSeconds(60); + private readonly TimeSpan _stabilityWindow; private int _retryCount; private bool _useNodeA = true; @@ -143,6 +143,10 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers /// into one publish per window (review 02 round 2, N6); /// restores per-delta publishing (legacy). Seed/reconcile publishes stay immediate. /// + /// Delay between gRPC reconnection attempts (production 5s). + /// + /// How long a fresh gRPC stream must stay up before its retry budget recovers (production 60s). + /// public SiteAlarmAggregatorActor( string siteIdentifier, string correlationId, @@ -152,7 +156,9 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers string grpcNodeAAddress, string grpcNodeBAddress, TimeSpan reconcileInterval, - TimeSpan publishCoalesce) + TimeSpan publishCoalesce, + TimeSpan reconnectDelay, + TimeSpan stabilityWindow) { _siteIdentifier = siteIdentifier; _correlationId = correlationId; @@ -163,6 +169,8 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers _grpcNodeBAddress = grpcNodeBAddress; _reconcileInterval = reconcileInterval; _publishCoalesce = publishCoalesce; + _reconnectDelay = reconnectDelay; + _stabilityWindow = stabilityWindow; // Live delta from the site-wide alarm stream (marshalled in via Self.Tell). // A received delta must NOT reset the retry budget (a flapping stream that @@ -499,7 +507,7 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers _grpcCts?.Dispose(); _grpcCts = new CancellationTokenSource(); - Timers.StartSingleTimer(StabilityTimerKey, new GrpcAlarmStreamStable(), StabilityWindow); + Timers.StartSingleTimer(StabilityTimerKey, new GrpcAlarmStreamStable(), _stabilityWindow); var generation = ++_streamGeneration; var client = _grpcFactory.GetOrCreate(_siteIdentifier, endpoint); @@ -558,7 +566,7 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers if (_retryCount == 1) Self.Tell(new ReconnectAlarmStream()); else - Timers.StartSingleTimer(ReconnectTimerKey, new ReconnectAlarmStream(), ReconnectDelay); + Timers.StartSingleTimer(ReconnectTimerKey, new ReconnectAlarmStream(), _reconnectDelay); } private void CleanupGrpc() diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/SiteAlarmLiveCacheService.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/SiteAlarmLiveCacheService.cs index 568dc538..13ec0c1b 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/SiteAlarmLiveCacheService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/SiteAlarmLiveCacheService.cs @@ -234,7 +234,9 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache grpcA, grpcB, _options.LiveAlarmCacheReconcileInterval, - _options.LiveAlarmCachePublishCoalesce)); + _options.LiveAlarmCachePublishCoalesce, + TimeSpan.FromSeconds(5), // reconnect delay — former ReconnectDelay static default + TimeSpan.FromSeconds(60))); // stability window — former StabilityWindow static default entry.Actor = system.ActorOf(props, $"site-alarm-aggregator-{entry.SiteId}-{Guid.NewGuid():N}"); entry.StartRetryTimer?.Dispose(); diff --git a/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/SiteAlarmAggregatorActorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/SiteAlarmAggregatorActorTests.cs index e4e7b21d..35c09662 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/SiteAlarmAggregatorActorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/SiteAlarmAggregatorActorTests.cs @@ -24,10 +24,12 @@ public class SiteAlarmAggregatorActorTests : TestKit public SiteAlarmAggregatorActorTests() : base(@"akka.loglevel = WARNING") { - SiteAlarmAggregatorActor.ReconnectDelay = TimeSpan.FromMilliseconds(50); - SiteAlarmAggregatorActor.StabilityWindow = TimeSpan.FromSeconds(30); } + // Former process-global static test seams, now ctor-injected per actor (R2 T12). + private static readonly TimeSpan TestReconnectDelay = TimeSpan.FromMilliseconds(50); + private static readonly TimeSpan TestStabilityWindow = TimeSpan.FromSeconds(30); + // ── Test doubles ──────────────────────────────────────────────────────────── /// @@ -146,7 +148,9 @@ public class SiteAlarmAggregatorActorTests : TestKit var props = Props.Create(() => new SiteAlarmAggregatorActor( SiteId, "corr-1", seed.Seed, sink.Publish, factory, GrpcNodeA, GrpcNodeB, reconcileInterval ?? TimeSpan.FromMinutes(10), - publishCoalesce ?? TimeSpan.Zero)); + publishCoalesce ?? TimeSpan.Zero, + TestReconnectDelay, + TestStabilityWindow)); var actor = Sys.ActorOf(props); return (actor, seed, sink, factory);