fix(dcl): log the initial connect to Site Event Logging like every other transition (review 2026-10-21 02/U4)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+24
-2
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[Collection("DataConnectionManagerActor")]
|
||||
public class DataConnectionHousekeepingTests : TestKit
|
||||
@@ -104,4 +106,24 @@ public class DataConnectionHousekeepingTests : TestKit
|
||||
|
||||
AwaitCondition(() => reseedToken.IsCancellationRequested, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void InitialConnect_IsLoggedToSiteEventLog()
|
||||
{
|
||||
const string name = "initial-connect-logged";
|
||||
var eventLog = Substitute.For<ISiteEventLogger>();
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user