diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/NativeAlarmActor.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/NativeAlarmActor.cs index 168e40d5..526e6757 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/NativeAlarmActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/NativeAlarmActor.cs @@ -129,6 +129,15 @@ public class NativeAlarmActor : ReceiveActor SourceReference: _source.SourceReference, ConditionFilter: _source.ConditionFilter, Timestamp: DateTimeOffset.UtcNow)); + + // Arm a response-timeout retry so a LOST subscribe response is re-sent, not + // only a failure reply (S4). Cancelled on a Success reply; a failure reply + // re-arms it at 1× (overriding this 2× response-timeout arm). 2× interval so + // a slow-but-arriving response is not pre-empted by a redundant re-send. + _retryTimer?.Cancel(); + _retryTimer = Context.System.Scheduler.ScheduleTellOnceCancelable( + TimeSpan.FromMilliseconds(_options.NativeAlarmRetryIntervalMs * 2), + Self, RetrySubscribe.Instance, Self); } private void HandleRehydration(RehydrationCompleted msg) @@ -270,6 +279,8 @@ public class NativeAlarmActor : ReceiveActor { if (resp.Success) { + // Success — cancel the response-timeout retry armed in SendSubscribe. + _retryTimer?.Cancel(); _logger.LogInformation("Native alarm subscription established for {Source} on {Instance}", _source.CanonicalName, _instanceName); return; diff --git a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/NativeAlarmActorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/NativeAlarmActorTests.cs index fb37f76f..3fe305a9 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/NativeAlarmActorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/NativeAlarmActorTests.cs @@ -378,4 +378,18 @@ public class NativeAlarmActorTests : TestKit, IDisposable File.Delete(_dbFile); } } + + [Fact] + public void LostSubscribeResponse_ResendsSubscribe() + { + // S4: a LOST subscribe response (no reply at all) must be re-sent, not only + // a failure reply. The response-timeout arm fires at 2× the retry interval. + var options = new SiteRuntimeOptions { NativeAlarmRetryIntervalMs = 200 }; + var dcl = CreateTestProbe(); + Spawn(CreateTestProbe().Ref, dcl.Ref, options); + + dcl.ExpectMsg(); + // Swallow the response entirely — the actor must still re-subscribe. + dcl.ExpectMsg(TimeSpan.FromSeconds(5)); + } }