fix(dcl): advance the session generation on same-adapter reconnect and session-scope MxGateway subscription ids (review 2026-10-21 02/C1, 02/L1)
This commit is contained in:
@@ -710,6 +710,38 @@ public class DataConnectionActor : UntypedActor, IWithTimers
|
||||
HookAdapterDisconnected();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances <see cref="_adapterGeneration"/> for a new SESSION on the SAME adapter — a plain
|
||||
/// drop + reconnect, where <see cref="SwapAdapter"/> never runs (review 2026-10-21 02/C1).
|
||||
///
|
||||
/// <para>
|
||||
/// Without it, an initial subscribe issued against the dead session (in flight for up to
|
||||
/// <see cref="DataConnectionOptions.SeedOverallTimeout"/>) carried the current generation and
|
||||
/// was applied against the new session: landing before the re-subscribe row for its tag it
|
||||
/// parked a dead handle under the tag's key, so the LIVE row looked redundant and was
|
||||
/// released (the tag reads resolved and is silent until the next reconnect); landing after,
|
||||
/// its dead handle was released against the adapter whose new session may have reused that
|
||||
/// handle number for another tag (MxGateway); and a connection-level failure row in it
|
||||
/// knocked the freshly reconnected link back into Reconnecting.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The <c>Disconnected</c> handler stamps the generation it was hooked with, so it is
|
||||
/// re-hooked for the new generation. The new handler is hooked BEFORE the old one is
|
||||
/// removed: a disconnect raised in between is then delivered twice (old stamp dropped as
|
||||
/// stale, new stamp acted on) rather than lost, which would wedge the actor Connected on a
|
||||
/// dead session.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private void AdvanceSessionGeneration()
|
||||
{
|
||||
var previousHandler = _disconnectedHandler;
|
||||
_adapterGeneration++;
|
||||
HookAdapterDisconnected();
|
||||
if (previousHandler is not null)
|
||||
_adapter.Disconnected -= previousHandler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a new configuration to this LIVE connection <b>in place</b> (deferred register
|
||||
/// row 97) — the non-destructive alternative to stopping and re-creating the connection
|
||||
@@ -1076,6 +1108,12 @@ public class DataConnectionActor : UntypedActor, IWithTimers
|
||||
$"Connection restored on {_activeEndpoint} endpoint", null);
|
||||
}
|
||||
|
||||
// The adapter object survived the drop but its SESSION did not. Advance the
|
||||
// generation BEFORE re-subscribing so every completion, value and alarm callback
|
||||
// started against the dead session takes the existing stale path (review
|
||||
// 2026-10-21 02/C1) — exactly as it would after a swap.
|
||||
AdvanceSessionGeneration();
|
||||
|
||||
// Transparent re-subscribe — re-establish all active subscriptions
|
||||
ReSubscribeAll();
|
||||
// Re-establish native alarm feeds (source replays a snapshot).
|
||||
@@ -1405,8 +1443,19 @@ public class DataConnectionActor : UntypedActor, IWithTimers
|
||||
/// The outgoing adapter is disposed fire-and-forget by <see cref="SwapAdapter"/> and may
|
||||
/// already be gone, so a failure here is expected and logged at Debug rather than surfaced.
|
||||
/// </summary>
|
||||
private void ReleaseStaleAgainstOwnAdapter(string description, Func<Task> release)
|
||||
private void ReleaseStaleAgainstOwnAdapter(object owner, string description, Func<Task> release)
|
||||
{
|
||||
// Same-adapter reconnect (review 2026-10-21 02/C1): the generation advanced because the
|
||||
// SESSION changed, not the adapter. The dead session's handles died with it, and the
|
||||
// adapter now speaks for the NEW session — where a session-scoped handle number (an
|
||||
// MxGateway int) may already belong to a live item. Releasing would tear that down.
|
||||
if (ReferenceEquals(owner, _adapter))
|
||||
{
|
||||
_log.Debug("[{0}] Not releasing {1}: it belongs to a session of the current adapter that "
|
||||
+ "has since ended, so it died with that session", _connectionName, description);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
release().ContinueWith(
|
||||
@@ -1618,6 +1667,7 @@ public class DataConnectionActor : UntypedActor, IWithTimers
|
||||
// MxGateway's are plain ints that collide across sessions, so releasing by handle
|
||||
// against the replacement adapter can tear down a live subscription.
|
||||
ReleaseStaleAgainstOwnAdapter(
|
||||
msg.Adapter,
|
||||
$"{staleIds.Count} tag subscription handle(s) from adapter generation {msg.AdapterGeneration}",
|
||||
() => UnsubscribeIdsAsync(msg.Adapter, staleIds));
|
||||
|
||||
@@ -3078,6 +3128,7 @@ public class DataConnectionActor : UntypedActor, IWithTimers
|
||||
// one (see ReleaseStaleAgainstOwnAdapter).
|
||||
if (msg is { Success: true, SubscriptionId: not null })
|
||||
ReleaseStaleAgainstOwnAdapter(
|
||||
msg.Adapter,
|
||||
$"the alarm feed for {msg.SourceReference} from adapter generation {msg.AdapterGeneration}",
|
||||
() => msg.Adapter.UnsubscribeAlarmsAsync(msg.SubscriptionId));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user