diff --git a/docs/requirements/Component-DataConnectionLayer.md b/docs/requirements/Component-DataConnectionLayer.md index 6760dcd1..6790d300 100644 --- a/docs/requirements/Component-DataConnectionLayer.md +++ b/docs/requirements/Component-DataConnectionLayer.md @@ -398,7 +398,7 @@ The DCL manages connection lifecycle automatically: 1. **Connection drop detection**: When a connection to a data source is lost, the DCL immediately pushes a value update with quality `bad` for **every tag subscribed on that connection**. Instance Actors see the staleness immediately and publish it to the site stream / debug view; **script and alarm actors are deliberately NOT re-notified on a quality-only change** (no value changed — re-evaluating triggers would cause spurious firings). Scripts observe quality on their next attribute read; a computed alarm holds its last-known state until fresh values arrive. 2. **Auto-reconnect with fixed interval**: The DCL retries the connection at a configurable fixed interval (e.g., every 5 seconds). The retry interval is a **shared setting for all data connections** (`ReconnectInterval` in the Shared Settings table). This is consistent with the fixed-interval retry philosophy used throughout the system. Individual gRPC/OPC UA operations (reads, writes) fail immediately to the caller on error; there is no operation-level retry within the adapter. -3. **Connection state transitions**: The DCL tracks each connection's state as `connected`, `disconnected`, or `reconnecting`. All transitions are logged to Site Event Logging. +3. **Connection state transitions**: The DCL tracks each connection's state as `connected`, `disconnected`, or `reconnecting`. All transitions are logged to Site Event Logging (event type `connection`): the initial connect (`Connection established on … endpoint`, Info — added by review 2026-10-21, it previously logged nothing), connection lost / reconfigured (`Warning` / `Info`), restored on reconnect (`Info`) and failover (`Warning`). 4. **Transparent re-subscribe**: On successful reconnection, the DCL automatically re-establishes all previously active subscriptions for that connection. Instance Actors require no action — they simply see quality return to `good` as fresh values arrive from restored subscriptions. 5. **Clean client replacement on (re)connect**: On every (re)connect the adapter detaches and disposes the previous protocol client (session, keep-alive timers, event loop) before creating a new one — a stale client can neither leak nor signal a disconnect against the new session. 6. **Session generation on same-adapter reconnect** (review 2026-10-21, finding C1): a plain drop + reconnect reuses the adapter object, so `SwapAdapter`'s generation bump never runs. On a successful reconnect the actor therefore advances `_adapterGeneration` itself — **before** `ReSubscribeAll` — and re-hooks the adapter's `Disconnected` handler for the new generation (new handler hooked before the old one is removed, so a disconnect in between is never lost). Every subscribe/alarm completion, value callback and seed started against the dead session then takes the existing stale path. That path releases a stale completion's handles against the adapter that created them, **except** when that adapter is still the current one: the handles died with the dead session, and releasing by handle against the adapter's NEW session could tear down a live item. Before the fix a dead-session initial-subscribe completion (in flight up to `SeedOverallTimeout`) was applied against the new session — parking a dead handle that made the live re-subscribe row look redundant and get released (the tag read resolved and stayed silent), or releasing a handle number the new MxGateway session had reused, and a connection-level failure row in it knocked the fresh link back into Reconnecting. diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs index 0537c948..87f9dfa2 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs @@ -1088,6 +1088,14 @@ public class DataConnectionActor : UntypedActor, IWithTimers } _log.Info("[{0}] Connection established", _connectionName); + // Every connection transition reaches the site event log — the initial connect + // included, not only restore / loss / failover (review 2026-10-21 02/U4). + if (_siteEventLogger != null) + { + _ = _siteEventLogger.LogEventAsync( + "connection", "Info", null, _connectionName, + $"Connection established on {_activeEndpoint} endpoint", null); + } BecomeConnected(); } else diff --git a/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Actors/DataConnectionHousekeepingTests.cs b/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Actors/DataConnectionHousekeepingTests.cs index 8b862cb9..b36bc97b 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Actors/DataConnectionHousekeepingTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Actors/DataConnectionHousekeepingTests.cs @@ -6,12 +6,14 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Actors; using ZB.MOM.WW.ScadaBridge.HealthMonitoring; +using ZB.MOM.WW.ScadaBridge.SiteEventLogging; namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests.Actors; /// -/// Low-severity housekeeping findings from review 2026-10-21 (02/L2, 02/L4): bookkeeping that -/// outlived the subscription or the session it belonged to. +/// Low-severity housekeeping findings from review 2026-10-21 (02/L2, 02/L4, 02/U4): bookkeeping +/// that outlived the subscription or the session it belonged to, and the connection-transition +/// event-log contract. /// [Collection("DataConnectionManagerActor")] public class DataConnectionHousekeepingTests : TestKit @@ -104,4 +106,24 @@ public class DataConnectionHousekeepingTests : TestKit AwaitCondition(() => reseedToken.IsCancellationRequested, TimeSpan.FromSeconds(5)); } + + /// + /// 02/U4(b): the spec says every connection transition is logged to Site Event Logging — + /// the INITIAL connect included, not just reconnect-restored / lost / failover. + /// + [Fact] + public void InitialConnect_IsLoggedToSiteEventLog() + { + const string name = "initial-connect-logged"; + var eventLog = Substitute.For(); + var adapter = new FakeBatchDataConnection(); + Sys.ActorOf(Props.Create(() => new DataConnectionActor( + name, adapter, Options(), _health, _factory, "OpcUa", null, null, 3, eventLog)), name); + + AwaitCondition(() => eventLog.ReceivedCalls().Any(c => + c.GetMethodInfo().Name == nameof(ISiteEventLogger.LogEventAsync) + && c.GetArguments() is ["connection", "Info", null, name, string message, ..] + && message.Contains("established", StringComparison.OrdinalIgnoreCase)), + TimeSpan.FromSeconds(5)); + } }