docs: fill missing XML docs and strip tracking IDs from src comments
Clears CommentChecker findings in src/ (3,280 -> 209, the remainder being false positives like PLC-5/IEEE-754/M0.0) by adding missing summaries/params/returns, normalising inheritdoc usage, and removing Gitea/review tracking IDs while keeping the rationale they annotated. Comment-only: comment-stripped sources are identical to the parent commit.
This commit is contained in:
@@ -85,7 +85,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
public static readonly TimeSpan DefaultApplyWedgeTimeout = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #601 — how long an Applied ack waits for <c>OpcUaPublishActor</c>'s address-space apply
|
||||
/// How long an Applied ack waits for <c>OpcUaPublishActor</c>'s address-space apply
|
||||
/// outcome (<see cref="OpcUa.OpcUaPublishActor.RebuildCompleted"/>) before it is sent anyway.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
@@ -99,7 +99,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// </remarks>
|
||||
public static readonly TimeSpan DefaultApplyOutcomeTimeout = DeployTimings.ApplyOutcomeTimeout;
|
||||
|
||||
/// <summary>Gitea #601 — the reason prefix of a degraded ack sent because the outcome TIMED OUT, so an
|
||||
/// <summary>The reason prefix of a degraded ack sent because the outcome TIMED OUT, so an
|
||||
/// operator can tell "not reported in time" from a real sink failure.</summary>
|
||||
public const string OutcomeNotReportedReasonPrefix = "address-space apply outcome not reported in time";
|
||||
|
||||
@@ -174,7 +174,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// <see langword="null"/> when none was wired (per-cluster mesh Phase 4). On a driver-role node
|
||||
/// this is the replicated LocalDb store (<c>LocalDbAlarmConditionStateStore</c>) so condition
|
||||
/// state survives without central SQL; when null the actor skips spawning the alarm host (the
|
||||
/// ConfigDb-backed EF fallback was retired in Phase 4 Task 9).
|
||||
/// ConfigDb-backed EF fallback was retired).
|
||||
/// </summary>
|
||||
private readonly IAlarmStateStore? _alarmStateStore;
|
||||
|
||||
@@ -265,7 +265,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
private RevisionHash? _currentRevision;
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #601 — the last address-space apply of <see cref="_currentRevision"/> reported DEGRADED (or no
|
||||
/// The last address-space apply of <see cref="_currentRevision"/> reported DEGRADED (or no
|
||||
/// outcome in time). A re-dispatch of that same revision then RE-APPLIES instead of taking the
|
||||
/// idempotent "already at this revision" short-circuit: the short-circuit would ack a clean Applied
|
||||
/// for an address space known to be partial, and the re-apply is what repairs it (a degraded
|
||||
@@ -274,7 +274,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// </summary>
|
||||
private bool _currentRevisionDegraded;
|
||||
|
||||
/// <summary>Gitea #601 — Applied acks held until the address-space apply reports its outcome, keyed by
|
||||
/// <summary>Applied acks held until the address-space apply reports its outcome, keyed by
|
||||
/// the apply's correlation id (echoed back in <see cref="OpcUa.OpcUaPublishActor.RebuildCompleted"/>).</summary>
|
||||
private readonly Dictionary<CorrelationId, PendingAck> _pendingAcks = new();
|
||||
|
||||
@@ -330,8 +330,8 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
private readonly Dictionary<(AddressSpaceRealm Realm, string NodeId), (string DriverInstanceId, string RawPath)> _driverRefByNodeId = new();
|
||||
|
||||
/// <summary>(DriverInstanceId, RawPath = alarm ConditionId) → materialised condition NodeId(s) (realm-tagged).
|
||||
/// v3 Batch 4: a native alarm is a single condition at the RawPath (Raw realm); WP4 adds the referencing
|
||||
/// equipment notifier NodeIds. Resolves a native alarm transition to the materialised Part 9 condition
|
||||
/// v3 Batch 4: a native alarm is a single condition at the RawPath (Raw realm); the referencing
|
||||
/// equipment notifier NodeIds are added on top. Resolves a native alarm transition to the materialised Part 9 condition
|
||||
/// node(s). Alarm tags are conditions, not value variables, so they are kept OUT of the value maps +
|
||||
/// value-subscription set.</summary>
|
||||
private readonly Dictionary<(string DriverInstanceId, string RawPath), HashSet<NodeRealmRef>> _alarmNodeIdByDriverRef = new();
|
||||
@@ -408,7 +408,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// first apply — a child spawned before any apply is handed nothing, which is what it should have.
|
||||
/// <para>It is only ever rebuilt by a SUCCESSFUL apply, so after a failed one it deliberately lingers on
|
||||
/// the last-good composition — matching the address space, drivers and subscriptions that also keep
|
||||
/// serving through a failed apply (#485/#486). A respawn or reprovision in that window therefore hands
|
||||
/// serving through a failed apply. A respawn or reprovision in that window therefore hands
|
||||
/// the child the last-known-good set, not an empty one.</para></summary>
|
||||
private readonly Dictionary<string, DesiredSubscriptionSet> _desiredByDriver = new(StringComparer.Ordinal);
|
||||
|
||||
@@ -434,7 +434,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
|
||||
/// <summary>
|
||||
/// Where the Primary <b>data-plane</b> verdict is published for drivers that must know it
|
||||
/// directly (Gitea #511). Null on nodes that wire no redundancy manager.
|
||||
/// directly. Null on nodes that wire no redundancy manager.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note the different verdict from <see cref="_redundancyRoleView"/>: that one carries
|
||||
@@ -446,16 +446,16 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
private readonly IDriverPrimaryRoleSource? _primaryRoleSource;
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #537: where the continuous-historization <b>capture</b> verdict is published for the
|
||||
/// Where the continuous-historization <b>capture</b> verdict is published for the
|
||||
/// recorder, which taps the mux fan-out and so needs the decision off its own mailbox. Null on
|
||||
/// nodes that wire no redundancy manager.
|
||||
/// </summary>
|
||||
private readonly IHistorizationRoleView? _historizationRoleView;
|
||||
|
||||
/// <summary>Gitea #674 review: where the HistoryUpdate verdict is published. Null on nodes that wire no redundancy manager.</summary>
|
||||
/// <summary>Where the HistoryUpdate verdict is published. Null on nodes that wire no redundancy manager.</summary>
|
||||
private readonly IHistoryUpdateRoleView? _historyUpdateRoleView;
|
||||
|
||||
/// <summary>Gitea #674 review: whether any redundancy snapshot has reached this host — the HistoryUpdate gate stays
|
||||
/// <summary>Whether any redundancy snapshot has reached this host — the HistoryUpdate gate stays
|
||||
/// closed until one has (see <see cref="PrimaryGatePolicy.ShouldServeHistoryUpdate"/>).</summary>
|
||||
private bool _redundancySnapshotReceived;
|
||||
|
||||
@@ -468,20 +468,20 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// </summary>
|
||||
private readonly Func<CommonsNodeId, bool>? _sharesHistorizationScopeWithLocal;
|
||||
|
||||
/// <summary>Gitea #672: whether an UNREACHABLE recording Primary still stands this node's historization
|
||||
/// <summary>Whether an UNREACHABLE recording Primary still stands this node's historization
|
||||
/// capture down (<c>false</c>) or not (<c>true</c>, the default). See <see cref="ComputeHistorizationVerdict"/>.</summary>
|
||||
private readonly bool _captureWhilePrimaryUnreachable;
|
||||
|
||||
/// <summary>Gitea #672: the last redundancy snapshot's nodes, so a reachability change can re-derive the
|
||||
/// <summary>The last redundancy snapshot's nodes, so a reachability change can re-derive the
|
||||
/// historization capture verdict without waiting for the next snapshot — which, when the recording Primary
|
||||
/// crashed, may not come until auto-down (the redundancy singleton usually lived on that Primary).</summary>
|
||||
private IReadOnlyList<NodeRedundancyState>? _lastRedundancySnapshotNodes;
|
||||
|
||||
/// <summary>Gitea #672: peers the cluster failure detector currently reports unreachable (never this node).
|
||||
/// <summary>Peers the cluster failure detector currently reports unreachable (never this node).
|
||||
/// Fed by <see cref="Akka.Cluster.ClusterEvent.ReachabilityEvent"/>; cleared for an address that comes Up again.</summary>
|
||||
private readonly HashSet<CommonsNodeId> _unreachableNodes = new();
|
||||
|
||||
/// <summary>Gitea #672: the unreachable recording Primary this node is currently capturing in place of, or
|
||||
/// <summary>The unreachable recording Primary this node is currently capturing in place of, or
|
||||
/// null — drives the once-per-transition Information log.</summary>
|
||||
private CommonsNodeId? _capturingForUnreachablePrimary;
|
||||
|
||||
@@ -489,10 +489,10 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// path republishes only a CHANGED verdict.</summary>
|
||||
private bool? _lastPublishedHistorizationVerdict;
|
||||
|
||||
/// <summary>Whether this actor holds its own (#672) cluster reachability subscription.</summary>
|
||||
/// <summary>Whether this actor holds its own cluster reachability subscription.</summary>
|
||||
private bool _subscribedToReachabilityEvents;
|
||||
|
||||
/// <summary>Gitea #601 — how long an Applied ack waits for the address-space outcome (see
|
||||
/// <summary>How long an Applied ack waits for the address-space outcome (see
|
||||
/// <see cref="DefaultApplyOutcomeTimeout"/>).</summary>
|
||||
private readonly TimeSpan _applyOutcomeTimeout;
|
||||
|
||||
@@ -511,7 +511,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// re-publishes, and read once by <see cref="CommsRecoveryAudit"/> after the driver reconnects.
|
||||
/// <para><b>It is a tripwire, not a mechanism.</b> Nothing depends on it being correct for the data
|
||||
/// plane to behave: the host publishes nothing Good on reconnect either way. What it buys is that a
|
||||
/// driver which reconnects and then fails to re-publish — the pre-STAB-18 MQTT shape, and what any
|
||||
/// driver which reconnects and then fails to re-publish — the MQTT shape, and what any
|
||||
/// future push driver without an initial push will do — produces a Warning and a metered count
|
||||
/// instead of a subscription that silently reports Bad forever.</para>
|
||||
/// <para>Keyed by RawPath, so a raw node and its UNS mirrors count once: the operator is being told
|
||||
@@ -538,7 +538,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
private readonly string? _replicationPeerHost;
|
||||
|
||||
/// <summary>
|
||||
/// Review R6 MINOR-4: identifies the alarm-queue-sharing peer when <see cref="_replicationPeerHost"/> is
|
||||
/// Identifies the alarm-queue-sharing peer when <see cref="_replicationPeerHost"/> is
|
||||
/// unknown — the passive (listening) half of a pair, which replicates the same rows but dials nobody. Before
|
||||
/// this the passive half had no peer to defer to, so as Secondary it kept draining alongside its Primary
|
||||
/// partner and every alarm event reached the historian twice. Null ⇒ no peer known ⇒ never stand down.
|
||||
@@ -636,8 +636,8 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// VirtualTag host instead of spawning a real <see cref="VirtualTagHostActor"/> child, so tests
|
||||
/// can intercept the <see cref="VirtualTagHostActor.ApplyVirtualTags"/> message. Null in
|
||||
/// production (the real host is spawned).</param>
|
||||
/// <param name="loggerFactory">Retained for constructor-signature stability; unused since Phase 4
|
||||
/// Task 9 retired the <c>EfAlarmConditionStateStore</c> fallback that consumed it. Defaults to null.</param>
|
||||
/// <param name="loggerFactory">Retained for constructor-signature stability; unused since the
|
||||
/// <c>EfAlarmConditionStateStore</c> fallback that consumed it was retired. Defaults to null.</param>
|
||||
/// <param name="scriptRootLogger">Optional root script logger required to spawn the ScriptedAlarm
|
||||
/// host (the engine + its script logging hang off it). When null the ScriptedAlarm host is left
|
||||
/// unspawned — the graceful dev/None-deployment path.</param>
|
||||
@@ -648,21 +648,47 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// <param name="invokerFactory">Optional Phase 6.1 resilience-invoker factory used to attach an
|
||||
/// <see cref="IDriverCapabilityInvoker"/> to each spawned driver; defaults to
|
||||
/// <see cref="NullDriverCapabilityInvokerFactory"/> (pass-through) when not supplied.</param>
|
||||
/// <param name="driverMemberCountProvider">Test seam: overrides the count of Up <c>driver</c>-role cluster
|
||||
/// members the Primary gate reads while the role is unknown. When null the cluster state is read.</param>
|
||||
/// <param name="deploymentArtifactCache">Optional node-local cache of applied deployment artifacts, so the node
|
||||
/// can boot from its last-known-good configuration while central SQL is unreachable. Null skips caching.</param>
|
||||
/// <param name="redundancyRoleView">Where the node's redundancy role is published and read. Null on nodes
|
||||
/// that wire no redundancy manager.</param>
|
||||
/// <param name="replicationPeerHost">Host of the LocalDb replication peer this node dials, used to identify the
|
||||
/// partner that shares its alarm queue. Null when the node dials nobody.</param>
|
||||
/// <param name="fetchAndCacheMode">When <c>true</c>, the node never reads central SQL for configuration and instead
|
||||
/// fetches artifacts through <paramref name="artifactFetcher"/> and boots from its local cache.</param>
|
||||
/// <param name="artifactFetcher">Fetches deployment artifacts from central over gRPC in
|
||||
/// <paramref name="fetchAndCacheMode"/>. Null when that mode is off.</param>
|
||||
/// <param name="alarmStateStore">Per-cluster mesh Phase 4: scripted-alarm condition-state store. On a
|
||||
/// driver-role node this is the replicated LocalDb store, so condition state persists with no
|
||||
/// ConfigDb; null skips spawning the alarm host (the ConfigDb-backed EF fallback was retired in
|
||||
/// Phase 4 Task 9). Defaults to null.</param>
|
||||
/// <param name="telemetryHub">Per-cluster mesh Phase 5: optional node-local live-telemetry hub each
|
||||
/// ConfigDb; null skips spawning the alarm host (the ConfigDb-backed EF fallback was retired).
|
||||
/// Defaults to null.</param>
|
||||
/// <param name="telemetryHub">Optional node-local live-telemetry hub each
|
||||
/// Primary-gated native-alarm transition is also emitted into, and which is threaded into the spawned
|
||||
/// VirtualTag + ScriptedAlarm hosts. Defaults to null (admin-only nodes / tests skip the emit).</param>
|
||||
/// <param name="primaryRoleSource">Where the Primary data-plane verdict is published, for drivers that assert a
|
||||
/// standing exclusive identity on a shared external system. Null on nodes that wire no redundancy manager.</param>
|
||||
/// <param name="historizationRoleView">Where the continuous-historization capture verdict is published. Null on
|
||||
/// nodes that wire no redundancy manager.</param>
|
||||
/// <param name="sharesHistorizationScopeWithLocal">Tests whether a node named in a redundancy snapshot serves this
|
||||
/// node's address space and therefore records the same historized refs. Null means this node never stands down.</param>
|
||||
/// <param name="applyWedgeTimeout">How long the host may sit in <see cref="Applying"/> before the
|
||||
/// wedge watchdog logs a Warning. Defaults to <see cref="DefaultApplyWedgeTimeout"/>.</param>
|
||||
/// <param name="reprovisionBackoff">Backoff before a failed driver re-provision is retried. Null uses the default.</param>
|
||||
/// <param name="commsRecoveryAuditDelay">Archreview 03/S15: how long after a driver reconnects the
|
||||
/// recovery audit waits before reporting refs still latched <c>BadNoCommunication</c>. Defaults to
|
||||
/// <see cref="DefaultCommsRecoveryAuditDelay"/>; tests shorten it.</param>
|
||||
/// <param name="applyOutcomeTimeout">Gitea #601: how long an Applied ack waits for the address-space
|
||||
/// <param name="sharesAlarmQueueWithLocal">Tests whether a node named in a redundancy snapshot holds this node's
|
||||
/// replicated alarm queue, for a node that knows no <paramref name="replicationPeerHost"/>. Null means no such
|
||||
/// peer is known.</param>
|
||||
/// <param name="applyOutcomeTimeout">How long an Applied ack waits for the address-space
|
||||
/// apply outcome before it is sent anyway, marked degraded ("outcome unknown"). Defaults to
|
||||
/// <see cref="DefaultApplyOutcomeTimeout"/>; tests shorten it.</param>
|
||||
/// <param name="captureWhilePrimaryUnreachable">When <c>true</c> (default), a node stops deferring historization
|
||||
/// capture to a mesh-partner Primary the cluster failure detector reports unreachable.</param>
|
||||
/// <param name="historyUpdateRoleView">Where the OPC UA HistoryUpdate verdict is published for the node manager's
|
||||
/// HistoryUpdate Primary gate. Null on nodes that wire no redundancy manager.</param>
|
||||
/// <returns>The Akka.NET <see cref="Akka.Actor.Props"/> used to spawn this actor.</returns>
|
||||
public static Props Props(
|
||||
IDbContextFactory<OtOpcUaConfigDbContext>? dbFactory,
|
||||
@@ -729,8 +755,8 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// for historized VirtualTag results; defaults to <see cref="NullHistoryWriter"/>.</param>
|
||||
/// <param name="virtualTagHostOverride">Test seam: when supplied, used as the VirtualTag host
|
||||
/// instead of spawning a real <see cref="VirtualTagHostActor"/> child.</param>
|
||||
/// <param name="loggerFactory">Retained for constructor-signature stability; unused since Phase 4
|
||||
/// Task 9 retired the <c>EfAlarmConditionStateStore</c> fallback that consumed it. Defaults to null.</param>
|
||||
/// <param name="loggerFactory">Retained for constructor-signature stability; unused since the
|
||||
/// <c>EfAlarmConditionStateStore</c> fallback that consumed it was retired. Defaults to null.</param>
|
||||
/// <param name="scriptRootLogger">Optional root script logger required to spawn the ScriptedAlarm
|
||||
/// host; when null the host is left unspawned.</param>
|
||||
/// <param name="scriptedAlarmHostOverride">Test seam: when supplied, used as the ScriptedAlarm host
|
||||
@@ -745,19 +771,27 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// When supplied, each successful apply stores its artifact so the node can boot from its
|
||||
/// last-known-good configuration while central SQL is unreachable. Null on admin-only nodes and in
|
||||
/// tests that do not exercise the cache — caching is then simply skipped.</param>
|
||||
/// <param name="redundancyRoleView">Where the node's redundancy role is published and read. Null on nodes
|
||||
/// that wire no redundancy manager.</param>
|
||||
/// <param name="replicationPeerHost">Host of the LocalDb replication peer this node dials, used to identify the
|
||||
/// partner that shares its alarm queue. Null when the node dials nobody.</param>
|
||||
/// <param name="fetchAndCacheMode">When <c>true</c>, the node never reads central SQL for configuration and instead
|
||||
/// fetches artifacts through <paramref name="artifactFetcher"/> and boots from its local cache.</param>
|
||||
/// <param name="artifactFetcher">Fetches deployment artifacts from central over gRPC in
|
||||
/// <paramref name="fetchAndCacheMode"/>. Null when that mode is off.</param>
|
||||
/// <param name="alarmStateStore">Per-cluster mesh Phase 4: scripted-alarm condition-state store. On a
|
||||
/// driver-role node this is the replicated LocalDb store; null skips spawning the alarm host (the
|
||||
/// ConfigDb-backed EF fallback was retired in Phase 4 Task 9).</param>
|
||||
/// <param name="telemetryHub">Per-cluster mesh Phase 5: optional node-local live-telemetry hub each
|
||||
/// ConfigDb-backed EF fallback was retired).</param>
|
||||
/// <param name="telemetryHub">Optional node-local live-telemetry hub each
|
||||
/// Primary-gated native-alarm transition is also emitted into, and which is threaded into the spawned
|
||||
/// VirtualTag + ScriptedAlarm hosts. Null (admin-only nodes / tests) skips the emit and passes null down.</param>
|
||||
/// <param name="primaryRoleSource">Gitea #511: where the Primary <i>data-plane</i> verdict is published
|
||||
/// <param name="primaryRoleSource">Where the Primary <i>data-plane</i> verdict is published
|
||||
/// for drivers that assert a standing exclusive identity on a shared external system — today the MQTT
|
||||
/// driver's Sparkplug primary-host STATE claim. Deliberately a separate parameter from
|
||||
/// <paramref name="redundancyRoleView"/> even though the production object implements both: a test that
|
||||
/// supplies only a role-view double must not silently also satisfy this, or its primary-role assertions
|
||||
/// pass without the publish ever happening. Null on nodes that wire no redundancy manager.</param>
|
||||
/// <param name="historizationRoleView">Gitea #537: where the continuous-historization <i>capture</i>
|
||||
/// <param name="historizationRoleView">Where the continuous-historization <i>capture</i>
|
||||
/// verdict is published, for the recorder that taps the mux fan-out. A third distinct parameter for
|
||||
/// the same reason <paramref name="primaryRoleSource"/> is a second — the production object
|
||||
/// implements all three, and a test double supplied for one must not silently satisfy the others.
|
||||
@@ -769,21 +803,25 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// <param name="applyWedgeTimeout">How long the host may sit in <see cref="Applying"/> before the
|
||||
/// wedge watchdog logs a Warning. Defaults to <see cref="DefaultApplyWedgeTimeout"/>; a test shortens
|
||||
/// it so the watchdog is observable in a test's lifetime.</param>
|
||||
/// <param name="reprovisionBackoff">Backoff before a failed driver re-provision is retried. Null uses the default.</param>
|
||||
/// <param name="commsRecoveryAuditDelay">Archreview 03/S15: how long after a driver reconnects the
|
||||
/// recovery audit waits before reporting refs still latched <c>BadNoCommunication</c>. Defaults to
|
||||
/// <see cref="DefaultCommsRecoveryAuditDelay"/>; a test shortens it for the same reason.</param>
|
||||
/// <param name="sharesAlarmQueueWithLocal">Review R6 MINOR-4: tests whether a node named in a redundancy
|
||||
/// <param name="sharesAlarmQueueWithLocal">Tests whether a node named in a redundancy
|
||||
/// snapshot holds this node's replicated alarm queue, for a node that knows no
|
||||
/// <paramref name="replicationPeerHost"/> — the PASSIVE half of a LocalDb pair (ApiKey + SyncListenPort, no
|
||||
/// PeerAddress), whose partner is its per-cluster mesh partner. Ignored when
|
||||
/// <paramref name="replicationPeerHost"/> is set. Null ⇒ no such peer is known and this node never stands its
|
||||
/// drain down.</param>
|
||||
/// <param name="captureWhilePrimaryUnreachable">Gitea #672 (<c>ContinuousHistorization:CaptureWhilePrimaryUnreachable</c>,
|
||||
/// <param name="applyOutcomeTimeout">How long an Applied ack waits for the address-space apply outcome before it is
|
||||
/// sent anyway, marked degraded ("outcome unknown"). Defaults to <see cref="DefaultApplyOutcomeTimeout"/>; tests
|
||||
/// shorten it.</param>
|
||||
/// <param name="captureWhilePrimaryUnreachable">(<c>ContinuousHistorization:CaptureWhilePrimaryUnreachable</c>,
|
||||
/// default <c>true</c>): when a mesh-partner Primary this node would stand its historization capture down for is
|
||||
/// reported UNREACHABLE by the cluster failure detector, stop deferring to it and capture — rather than waiting
|
||||
/// for auto-down + a fresh redundancy snapshot. Affects ONLY the historization capture verdict; the alarm drain
|
||||
/// and the Primary data-plane verdict are unchanged. <c>false</c> restores the pre-#672 behaviour.</param>
|
||||
/// <param name="historyUpdateRoleView">Gitea #674 review: where the OPC UA HistoryUpdate verdict
|
||||
/// and the Primary data-plane verdict are unchanged. <c>false</c> restores the earlier behaviour.</param>
|
||||
/// <param name="historyUpdateRoleView">Where the OPC UA HistoryUpdate verdict
|
||||
/// (<see cref="PrimaryGatePolicy.ShouldServeHistoryUpdate"/> — the data-plane verdict, but closed until the first
|
||||
/// redundancy snapshot) is published for the node manager's HistoryUpdate Primary gate. Null on nodes that wire no
|
||||
/// redundancy manager.</param>
|
||||
@@ -898,12 +936,12 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review R1 m2 (Gitea #597): the published Primary data-plane verdict (<see cref="IDriverPrimaryRoleSource"/>
|
||||
/// The published Primary data-plane verdict (<see cref="IDriverPrimaryRoleSource"/>
|
||||
/// — the native-ack gate and the Sparkplug primary-host claim) must mirror <see cref="ShouldServiceAsPrimary"/>
|
||||
/// from the moment this host starts, not only from the first redundancy snapshot. Before this, the view sat
|
||||
/// on its single-node seed (allow) through the whole boot window, so on a pair BOTH nodes accepted a native
|
||||
/// ack while this host — which evaluates the member count live — dropped the upstream route on the
|
||||
/// non-Primary: the acked → unacked revert #597 targets. So publish once now, and again whenever cluster
|
||||
/// non-Primary: the acked → unacked revert that the Primary gate prevents. So publish once now, and again whenever cluster
|
||||
/// membership changes (a driver peer coming Up turns an unknown role from allow into deny before any
|
||||
/// snapshot names this node). A non-cluster ActorRefProvider (legacy/test harnesses) just keeps the
|
||||
/// initial publish.
|
||||
@@ -915,7 +953,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// normally seconds, up to <c>Cluster:BootstrapGuard:PartnerProbeSeconds</c> (25 s) under the
|
||||
/// bootstrap guard. In that pre-join window the Sparkplug primary-host claim can be held on both nodes of
|
||||
/// a pair. Native ack is NOT exposed by it: the live route gate reads the same count and also allows, so
|
||||
/// the view and the route still agree and the acked → unacked revert cannot occur (review R6 MINOR-2).
|
||||
/// the view and the route still agree and the acked → unacked revert cannot occur.
|
||||
/// Denying until self is Up was considered and NOT done: it would change the live data-plane gate (the
|
||||
/// two must stay one decision) and so delay writes and acks on a genuinely single-node cluster during its
|
||||
/// own join, and this actor has no reliable way to know that a partner is configured.
|
||||
@@ -944,10 +982,10 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// own subscriber AFTER this event was delivered here — the two are not ordered.</summary>
|
||||
private void OnDriverMembershipChanged(Akka.Cluster.ClusterEvent.IMemberEvent evt)
|
||||
{
|
||||
// Gitea #672: a member that comes Up at an address is reachable — the usual case being a restarted
|
||||
// A member that comes Up at an address is reachable — the usual case being a restarted
|
||||
// node's new incarnation. A REMOVED member deliberately stays in the set: it never answers again, and
|
||||
// dropping it would re-derive the verdict from a cached snapshot that may still name it Primary,
|
||||
// closing the gate on a dead node until the next snapshot (the very hole #672 closes).
|
||||
// closing the gate on a dead node until the next snapshot (the very hole the reachability tracking closes).
|
||||
if (evt is Akka.Cluster.ClusterEvent.MemberUp up
|
||||
&& _unreachableNodes.Remove(ToNodeId(up.Member.Address)))
|
||||
{
|
||||
@@ -979,7 +1017,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
PublishPrimaryVerdict();
|
||||
}
|
||||
|
||||
/// <summary>Gitea #674 review: publishes <see cref="PrimaryGatePolicy.ShouldServeHistoryUpdate"/> (no-op when no view
|
||||
/// <summary>Publishes <see cref="PrimaryGatePolicy.ShouldServeHistoryUpdate"/> (no-op when no view
|
||||
/// is wired). Rides every Primary-verdict publish point — host start, every snapshot, membership changes — so it is
|
||||
/// closed from host start and opens only once a snapshot has arrived and the data-plane verdict allows.</summary>
|
||||
private void PublishHistoryUpdateVerdict() =>
|
||||
@@ -987,7 +1025,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
PrimaryGatePolicy.ShouldServeHistoryUpdate(_redundancySnapshotReceived, _localRole, DriverMemberCount()));
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #672: follow cluster reachability so the historization capture gate stops deferring to a recording
|
||||
/// Follow cluster reachability so the historization capture gate stops deferring to a recording
|
||||
/// Primary the failure detector already considers dead. Its OWN subscription, deliberately not folded into
|
||||
/// <see cref="StartPrimaryVerdictPublishing"/>, which early-returns on nodes with no primary-role source.
|
||||
/// <see cref="Akka.Cluster.ClusterEvent.MemberUp"/> rides along so a re-joined incarnation leaves the set.
|
||||
@@ -1009,7 +1047,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gitea #672: a peer became unreachable or reachable again. Records it and re-derives ONLY the
|
||||
/// <summary>A peer became unreachable or reachable again. Records it and re-derives ONLY the
|
||||
/// historization capture verdict from the cached snapshot — the alarm drain and the Primary data-plane verdict
|
||||
/// are deliberately untouched (a false allow there is a dual claimant, not a duplicate sample).</summary>
|
||||
private void OnReachabilityChanged(Akka.Cluster.ClusterEvent.ReachabilityEvent evt)
|
||||
@@ -1124,7 +1162,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
|
||||
// Per-cluster mesh Phase 4: condition state lives in the wired store — the replicated LocalDb
|
||||
// store on a driver-role node, which persists with no ConfigDb. The ConfigDb-backed EF fallback
|
||||
// was retired (Task 9); when no store was wired there is nowhere to persist condition state, so
|
||||
// was retired; when no store was wired there is nowhere to persist condition state, so
|
||||
// we skip the alarm host outright rather than run the engine against a store it can never save to
|
||||
// (mirrors the _opcUaPublishActor-null skip above).
|
||||
if (_alarmStateStore is null)
|
||||
@@ -1267,7 +1305,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
// cache boot, but this is the steady-state config source, not an outage fallback.
|
||||
if (!ApplyCachedArtifact(deploymentId, cached.Artifact))
|
||||
{
|
||||
// #601 / review R1 m4: unusable cached bytes applied nothing, so they must not become the
|
||||
// Unusable cached bytes applied nothing, so they must not become the
|
||||
// current revision — that would ack a re-dispatch of the same revision as already applied
|
||||
// without ever applying it. Steady-with-no-revision, exactly as for an empty cache.
|
||||
_log.Warning(
|
||||
@@ -1343,7 +1381,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
var deploymentId = DeploymentId.Parse(cached.DeploymentId);
|
||||
var revision = RevisionHash.Parse(cached.RevisionHash);
|
||||
|
||||
// #601 / review R1 m4: unusable cached bytes applied nothing — the node is NOT serving a
|
||||
// Unusable cached bytes applied nothing — the node is NOT serving a
|
||||
// configuration, so it must not claim RUNNING FROM CACHE, adopt the revision (a re-dispatch of
|
||||
// it would then be acked without being applied) or skip Stale's ConfigDb retry loop.
|
||||
if (!ApplyCachedArtifact(deploymentId, cached.Artifact))
|
||||
@@ -1389,12 +1427,12 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// depend on central.
|
||||
/// </remarks>
|
||||
/// <returns><see langword="false"/> when the bytes were empty or malformed and nothing was applied — the
|
||||
/// caller must not adopt the cached revision (#601, review R1 m4).</returns>
|
||||
/// caller must not adopt the cached revision.</returns>
|
||||
private bool ApplyCachedArtifact(DeploymentId deploymentId, byte[] blob)
|
||||
{
|
||||
var correlation = CorrelationId.NewId();
|
||||
|
||||
// #601: null = the cached bytes were empty or malformed — no answer, not an empty configuration.
|
||||
// Null = the cached bytes were empty or malformed — no answer, not an empty configuration.
|
||||
// Keep last-known-good: no address-space rebuild and no subscription push from unusable bytes.
|
||||
if (ReconcileDriversFromBlob(deploymentId, blob) is null)
|
||||
return false;
|
||||
@@ -1447,7 +1485,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
private void Steady()
|
||||
{
|
||||
Receive<DispatchDeployment>(HandleDispatchFromSteady);
|
||||
// #601 — the held Applied ack is released by the address-space outcome (or its timeout); both can
|
||||
// The held Applied ack is released by the address-space outcome (or its timeout); both can
|
||||
// land in any behaviour, so all three register them.
|
||||
Receive<OpcUa.OpcUaPublishActor.RebuildCompleted>(HandleRebuildCompleted);
|
||||
Receive<ApplyOutcomeTimedOut>(HandleApplyOutcomeTimedOut);
|
||||
@@ -1563,7 +1601,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// </summary>
|
||||
private void ForwardBatchToMux(DriverInstanceActor.AttributeValueBatchPublished msg)
|
||||
{
|
||||
// One envelope per burst; the mux filters per item (it registered a batch receive in Task 9).
|
||||
// One envelope per burst; the mux filters per item (it registered a batch receive).
|
||||
_dependencyMux?.Tell(msg);
|
||||
|
||||
// 03/S15 recovery audit: clear whatever this burst re-published from the driver's Bad latch. Guarded
|
||||
@@ -1628,7 +1666,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// #477 — a child driver's connectivity transition. Annotates the source-data Quality of EVERY native
|
||||
/// A child driver's connectivity transition. Annotates the source-data Quality of EVERY native
|
||||
/// alarm condition the driver owns: comms lost → <see cref="OpcUaStatus.BadNoCommunication"/>, restored →
|
||||
/// <see cref="OpcUaStatus.Good"/>. This is the ONLY signal for a comms-lost native source, because a
|
||||
/// disconnected driver emits no alarm transitions — the alarm feed goes silent, so without this a
|
||||
@@ -1642,8 +1680,8 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// (<see cref="IDriverHealthPublisher"/>); a row per condition would be alarm-fatigue.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>#481 (Layer 4) — the same transition also reaches SCRIPTED alarms, via the dependency
|
||||
/// mux.</b> #478 gave a scripted condition the worst quality of its input tags, but only for
|
||||
/// <b>Layer 4 — the same transition also reaches SCRIPTED alarms, via the dependency
|
||||
/// mux.</b> A scripted condition already got the worst quality of its input tags, but only for
|
||||
/// inputs whose driver actually <i>publishes</i> a Bad data change. A poll driver whose device
|
||||
/// goes unreachable publishes nothing at all — it emits this signal and goes silent — so the
|
||||
/// engine kept its last Good value, <c>WorstInputStatus</c> stayed Good, and worse,
|
||||
@@ -1664,13 +1702,13 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// </summary>
|
||||
private void OnDriverConnectivityChanged(DriverInstanceActor.ConnectivityChanged msg)
|
||||
{
|
||||
// #481: the mux fan-out is independent of the OPC UA sink — a node with scripted alarms but no
|
||||
// The mux fan-out is independent of the OPC UA sink — a node with scripted alarms but no
|
||||
// publish actor (the mux-only test/dev path) must still see comms loss. Done before the early
|
||||
// return for exactly that reason.
|
||||
if (!msg.Connected)
|
||||
{
|
||||
FanCommsLossToDependencyMux(msg.DriverInstanceId);
|
||||
// 03/S15: and the EXTERNAL data plane, which #481 stopped one seam short of.
|
||||
// And the EXTERNAL data plane, which the dependency-mux fan-out stopped one seam short of.
|
||||
FanCommsLossToValueNodes(msg.DriverInstanceId);
|
||||
}
|
||||
else
|
||||
@@ -1682,7 +1720,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
|
||||
if (_opcUaPublishActor is null) return;
|
||||
|
||||
// #519: name the actual cause. This is a connectivity transition, so the Bad case is
|
||||
// Name the actual cause. This is a connectivity transition, so the Bad case is
|
||||
// BadNoCommunication — the bare OpcUaQuality.Bad it used to send reached clients as 0x80000000,
|
||||
// indistinguishable from any other fault.
|
||||
var statusCode = msg.Connected ? OpcUaStatus.Good : OpcUaStatus.BadNoCommunication;
|
||||
@@ -1718,7 +1756,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Why it was missing.</b> #481 built the per-driver ref index and wired comms loss to the
|
||||
/// <b>Why it was missing.</b> The per-driver ref index was built and wired comms loss to the
|
||||
/// internal consumers (the mux) and to Part 9 conditions, and stopped one seam short of the
|
||||
/// external OPC UA data plane. A client subscribed to a value node therefore saw the last value
|
||||
/// at <b>Good</b> quality for the whole outage, for every driver whose failure shape is "the
|
||||
@@ -1832,7 +1870,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// observed — the very thing this whole path refuses to do. What the operator gets instead is a
|
||||
/// named driver and a count, plus the <c>otopcua.driver.comms_latched_refs</c> counter, which is
|
||||
/// the operator-visible signature of a driver that reconnects without re-publishing (the
|
||||
/// pre-STAB-18 MQTT shape, or any future push driver that forgets its initial push).
|
||||
/// MQTT shape, or any future push driver that forgets its initial push).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The latch is cleared afterwards either way: it has been reported, and leaving it would make
|
||||
@@ -1865,13 +1903,13 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #481 (Layer 4) — push a synthetic <see cref="OpcUaStatus.BadNoCommunication"/> into the dependency
|
||||
/// Layer 4 — push a synthetic <see cref="OpcUaStatus.BadNoCommunication"/> into the dependency
|
||||
/// mux for every value ref the just-disconnected driver owns, so scripted alarms and virtual tags see
|
||||
/// the source die instead of holding its last Good value indefinitely.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>The ref set already existed.</b> #481 asked for a driver → all-tag-refs map to be built;
|
||||
/// <b>The ref set already existed.</b> The fan-out needed a driver → all-tag-refs map to be built;
|
||||
/// <see cref="_nodeIdByDriverRef"/> is exactly that, keyed <c>(DriverInstanceId, RawPath)</c> and
|
||||
/// rebuilt from the composition's <c>RawTags</c> ∪ <c>UnsReferenceVariables</c> on every apply —
|
||||
/// read here through the per-driver <see cref="_valueRefsByDriver"/> index, since that composite
|
||||
@@ -1884,7 +1922,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// <b>Value <c>null</c>, not the last-known value.</b> The honest statement is "there is no
|
||||
/// current value", and the engine's <c>AreInputsReady</c> gate holds the predicate on any Bad
|
||||
/// regardless, so no script observes the null. It does mean the Bad must survive
|
||||
/// <c>WorstInputStatus</c>, which until #481 skipped every null-valued entry as cold start —
|
||||
/// <c>WorstInputStatus</c>, which used to skip every null-valued entry as cold start —
|
||||
/// hence the paired change stamping never-pushed paths <c>BadWaitingForInitialData</c> so the two
|
||||
/// are tellable apart. Without that half this fan-out would be silently inert.
|
||||
/// </para>
|
||||
@@ -1953,8 +1991,8 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
return;
|
||||
}
|
||||
|
||||
// #626 — a QUALITY-ONLY annotation (an MTConnect condition gone Unavailable, or back) is not an alarm
|
||||
// transition. It rides the SAME quality-only node path the #477 connectivity annotation uses
|
||||
// A QUALITY-ONLY annotation (an MTConnect condition gone Unavailable, or back) is not an alarm
|
||||
// transition. It rides the SAME quality-only node path the connectivity annotation uses
|
||||
// (AlarmQualityUpdate → WriteAlarmQuality, which touches Quality and nothing else), never a projected
|
||||
// snapshot: the projector never sees a client's acknowledgement (that is applied to the node by the
|
||||
// SDK) and forgets a condition a deploy removed, so a projected "keep the state" can disagree with
|
||||
@@ -1995,11 +2033,11 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
nodeId, snapshot, msg.Args.SourceTimestampUtc, n.Realm));
|
||||
|
||||
// Only the Primary publishes the cluster-wide alerts transition (see the gate above). The projector
|
||||
// decides WHICH rows the transition warrants: none for a re-statement (Gitea #647 review: a Raise on an
|
||||
// already-active condition from a respawned driver; #656: a flagged snapshot/first-sight Retrigger or
|
||||
// Clear that changes nothing the condition shows), and up to two for an ActiveAcknowledged (#656:
|
||||
// decides WHICH rows the transition warrants: none for a re-statement (a Raise on an
|
||||
// already-active condition from a respawned driver; a flagged snapshot/first-sight Retrigger or
|
||||
// Clear that changes nothing the condition shows), and up to two for an ActiveAcknowledged (
|
||||
// "Activated" only if it was not already active, "Acknowledged" only if it was not already acked) or an
|
||||
// InactiveUnacknowledged (#657: "Activated" + "Cleared" only if it was acked, "Cleared" if it was active).
|
||||
// InactiveUnacknowledged ("Activated" + "Cleared" only if it was acked, "Cleared" if it was active).
|
||||
if (!serviceAlertsAsPrimary) continue;
|
||||
|
||||
var meta = _alarmMetaByNodeId.TryGetValue(nodeId, out var m)
|
||||
@@ -2023,9 +2061,9 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
(string EquipmentId, string Name, string AlarmType, bool? HistorizeToAveva, IReadOnlyList<string> ReferencingEquipmentPaths) meta,
|
||||
AlarmTransitionKind historyKind)
|
||||
{
|
||||
// #656: an ActiveAcknowledged expands to "Activated" + "Acknowledged". The operator comment/user belong to
|
||||
// An ActiveAcknowledged expands to "Activated" + "Acknowledged". The operator comment/user belong to
|
||||
// the acknowledgement, and the activation happened at the original raise time when the source knows it.
|
||||
// #657: an InactiveUnacknowledged's "Activated" row is likewise the occurrence, at its raise time when known.
|
||||
// An InactiveUnacknowledged's "Activated" row is likewise the occurrence, at its raise time when known.
|
||||
var expandedActivation = (args.Kind == AlarmTransitionKind.ActiveAcknowledged
|
||||
&& historyKind != AlarmTransitionKind.Acknowledge)
|
||||
|| (args.Kind == AlarmTransitionKind.InactiveUnacknowledged && historyKind == AlarmTransitionKind.Raise);
|
||||
@@ -2051,7 +2089,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
// true and suppresses the durable AVEVA row only on an explicit false — the same posture as the
|
||||
// scripted-alarm opt-out. null here rides through unchanged (the gate treats it as default-on).
|
||||
HistorizeToAveva: meta.HistorizeToAveva,
|
||||
// WP4: the Area/Line/Equipment UNS paths that reference this raw condition — the /alerts row
|
||||
// The Area/Line/Equipment UNS paths that reference this raw condition — the /alerts row
|
||||
// renders them as the equipment list (empty for a raw condition no equipment references yet).
|
||||
ReferencingEquipmentPaths: meta.ReferencingEquipmentPaths);
|
||||
_mediator.Tell(new Publish(ScriptedAlarmHostActor.AlertsTopic, alertEvent));
|
||||
@@ -2071,9 +2109,9 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
AlarmTransitionKind.Raise or AlarmTransitionKind.Retrigger => "Activated",
|
||||
AlarmTransitionKind.Clear => "Cleared",
|
||||
AlarmTransitionKind.Acknowledge => "Acknowledged",
|
||||
// #656: never reached in practice — the projector expands ActiveAcknowledged into Raise + Acknowledge rows.
|
||||
// Never reached in practice — the projector expands ActiveAcknowledged into Raise + Acknowledge rows.
|
||||
AlarmTransitionKind.ActiveAcknowledged => "Acknowledged",
|
||||
// #657: likewise never reached — expanded into Raise + Clear (or Clear alone) rows.
|
||||
// Likewise never reached — expanded into Raise + Clear (or Clear alone) rows.
|
||||
AlarmTransitionKind.InactiveUnacknowledged => "Cleared",
|
||||
_ => "Activated",
|
||||
};
|
||||
@@ -2215,7 +2253,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
return;
|
||||
}
|
||||
|
||||
// Gitea #641 review — the node is already acknowledged (the SDK applies the client's ack); tell the
|
||||
// The node is already acknowledged (the SDK applies the client's ack); tell the
|
||||
// projector too, or the next driver Clear projects (Active=false, Acked=prev=false) and un-acknowledges it.
|
||||
// Most sources (MTConnect, FOCAS) send no Acknowledge transition back, and the projector now survives a
|
||||
// deploy rather than resetting to its acked default.
|
||||
@@ -2250,8 +2288,8 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
if (_driverMemberCountProvider is not null) return _driverMemberCountProvider();
|
||||
try
|
||||
{
|
||||
// R3-04 (01/C-9): GossipedRolesHave, not ordinal Contains — a peer gossiping a miscased
|
||||
// 'Driver' (rolling-upgrade window) must still count toward the R2-04 primary-gate member
|
||||
// GossipedRolesHave, not ordinal Contains — a peer gossiping a miscased
|
||||
// 'Driver' (rolling-upgrade window) must still count toward the primary-gate member
|
||||
// count, or a genuine 2-node pair silently takes the single-node allow branch.
|
||||
return Akka.Cluster.Cluster.Get(Context.System).State.Members
|
||||
.Count(m => m.Status == Akka.Cluster.MemberStatus.Up
|
||||
@@ -2264,7 +2302,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
}
|
||||
|
||||
/// <summary>Whether <paramref name="nodeId"/> holds this node's replicated alarm queue: the PeerAddress host when
|
||||
/// this node dials, else the passive-half predicate (review R6 MINOR-4). This node itself never counts.</summary>
|
||||
/// this node dials, else the passive-half predicate. This node itself never counts.</summary>
|
||||
private bool SharesAlarmQueue(CommonsNodeId nodeId) =>
|
||||
nodeId != _localNode
|
||||
&& (_replicationPeerHost is not null
|
||||
@@ -2322,15 +2360,15 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
// cluster-wide and the queue is pair-local, so "somebody is Primary" is not a licence to stop
|
||||
// draining — that Primary is usually in another pair entirely, and cannot deliver our events.
|
||||
//
|
||||
// The dialling half knows its peer by PeerAddress host. The passive half (review R6 MINOR-4) knows it
|
||||
// The dialling half knows its peer by PeerAddress host. The passive half knows it
|
||||
// only as its mesh partner — the predicate — and must be gated too, or both halves drain the same rows.
|
||||
var peerIsPrimary = msg.Nodes.Any(n => n.Role == RedundancyRole.Primary && SharesAlarmQueue(n.NodeId));
|
||||
|
||||
_redundancyRoleView?.Publish(
|
||||
PrimaryGatePolicy.ShouldDrainAlarmHistory(_localRole, queueSharingPeerIsPrimary: peerIsPrimary));
|
||||
|
||||
// Gitea #537: the continuous-historization CAPTURE verdict. Same role rule as the drain gate
|
||||
// above, but a different peer question — see ComputeHistorizationVerdict. Cached (#672) so a
|
||||
// The continuous-historization CAPTURE verdict. Same role rule as the drain gate
|
||||
// above, but a different peer question — see ComputeHistorizationVerdict. Cached so a
|
||||
// reachability change can re-derive it before the next snapshot arrives.
|
||||
_lastRedundancySnapshotNodes = msg.Nodes;
|
||||
if (_historizationRoleView is not null)
|
||||
@@ -2342,13 +2380,13 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
|
||||
// The DATA-PLANE verdict, deliberately — the same one HandleRouteNodeWrite gates on, not the
|
||||
// drain verdict published immediately above. A driver reading this claims an exclusive identity
|
||||
// on a shared external system (Gitea #511: the Sparkplug host id), where two claimants is the
|
||||
// on a shared external system (the Sparkplug host id), where two claimants is the
|
||||
// harm, so an unknown role with a driver peer present must deny.
|
||||
PublishPrimaryVerdict();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The continuous-historization capture verdict for a snapshot (Gitea #537, #672).
|
||||
/// The continuous-historization capture verdict for a snapshot.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Historization is not pair-local, it is fleet-wide by historian tag name, so the only node worth
|
||||
@@ -2356,12 +2394,12 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// no way to establish that (a legacy node carrying no cluster-scoped role, or a test harness), the
|
||||
/// predicate is null and this node keeps capturing. Duplicate samples are recoverable; a hole in the trend
|
||||
/// is not.</para>
|
||||
/// <para><b>#672 — an UNREACHABLE recording Primary does not count.</b> A crashed (SIGKILLed) Primary stays a
|
||||
/// <para><b>An UNREACHABLE recording Primary does not count.</b> A crashed (SIGKILLed) Primary stays a
|
||||
/// cluster member until auto-down, and the redundancy singleton usually died with it, so the cached snapshot
|
||||
/// keeps naming it Primary for the whole failure-detector + auto-down window (~25 s) while nobody captures.
|
||||
/// Excluding unreachable peers shrinks that hole to the failure-detector window alone. The cost — a short
|
||||
/// duplicate window, and both nodes capturing for the duration of a genuine partition — is the same
|
||||
/// asymmetric trade #537 already makes. Gated by <c>ContinuousHistorization:CaptureWhilePrimaryUnreachable</c>.</para>
|
||||
/// asymmetric trade the capture gate already makes. Gated by <c>ContinuousHistorization:CaptureWhilePrimaryUnreachable</c>.</para>
|
||||
/// </remarks>
|
||||
private bool ComputeHistorizationVerdict(IReadOnlyList<NodeRedundancyState> nodes)
|
||||
{
|
||||
@@ -2434,7 +2472,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
// The redundancy-state topic also carries OpcUaProbeResult (OpcUaPublishActor peer-probes).
|
||||
// We don't consume it here — drop it so it doesn't dead-letter (matches PeerProbeSupervisor).
|
||||
Receive<ZB.MOM.WW.OtOpcUa.Runtime.Health.PeerOpcUaProbeActor.OpcUaProbeResult>(_ => { });
|
||||
// #601 — an apply that completed just before the host went Stale still owes its ack; the
|
||||
// An apply that completed just before the host went Stale still owes its ack; the
|
||||
// coordinator is not behind the ConfigDb, so release it exactly as in Steady.
|
||||
Receive<OpcUa.OpcUaPublishActor.RebuildCompleted>(HandleRebuildCompleted);
|
||||
Receive<ApplyOutcomeTimedOut>(HandleApplyOutcomeTimedOut);
|
||||
@@ -2480,7 +2518,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
|
||||
private void HandleDispatchFromSteady(DispatchDeployment msg)
|
||||
{
|
||||
// #601: the short-circuit claims a CLEAN Applied, so it is only honest while the current revision's
|
||||
// The short-circuit claims a CLEAN Applied, so it is only honest while the current revision's
|
||||
// address space is known-good — not after a degraded apply, and not while its outcome is still
|
||||
// pending. Either way the dispatch re-applies instead, which is also what repairs a degraded apply.
|
||||
var currentRevisionUnconfirmed = _currentRevisionDegraded
|
||||
@@ -2547,7 +2585,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
_log.Debug("DriverHost {Node}: fetching artifact for {Id} (rev {Rev}) from central over gRPC",
|
||||
_localNode, deploymentId, revision);
|
||||
|
||||
// A faulted fetch task maps to null bytes — the #485 apply-failure path, exactly like an
|
||||
// A faulted fetch task maps to null bytes — the apply-failure path, exactly like an
|
||||
// all-endpoints-down fetch — so a fault can never escape as an unhandled actor message.
|
||||
_artifactFetcher.FetchAsync(deploymentId.ToString(), revision.Value, CancellationToken.None)
|
||||
.PipeTo(
|
||||
@@ -2559,7 +2597,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perf roadmap Task 26 (<c>Direct</c>). Kicks off the central-SQL artifact read on a pool
|
||||
/// Direct apply path. Kicks off the central-SQL artifact read on a pool
|
||||
/// thread and pipes the bytes back to <see cref="Self"/> as an <see cref="ArtifactForApply"/>,
|
||||
/// the same shape <see cref="BeginFetchAndCacheApply"/> has always had for its gRPC fetch. The
|
||||
/// apply itself completes on the actor thread in <see cref="HandleArtifactForApply"/>.
|
||||
@@ -2573,7 +2611,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// source ever should — everything downstream of "bytes in hand" is one continuation.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The bytes come back as a MESSAGE, never as an escaping fault (#491): the task closes over
|
||||
/// The bytes come back as a MESSAGE, never as an escaping fault: the task closes over
|
||||
/// <see cref="_dbFactory"/>, <see cref="_localNode"/> and <see cref="_log"/> and touches
|
||||
/// neither <see cref="Self"/> nor <c>Context</c>, <see cref="LoadArtifactBlob"/> already
|
||||
/// swallows its own DB failures into a null return, and the failure mapper turns anything it
|
||||
@@ -2607,7 +2645,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// <summary>
|
||||
/// Completes an apply once the artifact bytes are in hand — the ONE continuation both modes
|
||||
/// share, whether the bytes were read from the ConfigDb or fetched from central. Null bytes are
|
||||
/// an apply FAILURE (keep last-known-good, do not advance the revision — #485/#486); non-null
|
||||
/// an apply FAILURE (keep last-known-good, do not advance the revision); non-null
|
||||
/// bytes are applied from hand and cached.
|
||||
/// </summary>
|
||||
private void HandleArtifactForApply(ArtifactForApply msg)
|
||||
@@ -2631,7 +2669,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
ParsedDeploymentArtifact? parsed = null;
|
||||
try
|
||||
{
|
||||
// #485/#486: null bytes = "no answer" — the ConfigDb was unreachable or the row is
|
||||
// Null bytes = "no answer" — the ConfigDb was unreachable or the row is
|
||||
// missing/empty (Direct), or every endpoint was unreachable / NotFound / SHA mismatch
|
||||
// (FetchAndCache). Either way NOTHING was applied, so fail the apply and stay on the
|
||||
// current revision: HandleDispatchFromSteady short-circuits on a revision match, so
|
||||
@@ -2658,7 +2696,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
|
||||
_currentRevision = msg.Revision;
|
||||
UpsertNodeDeploymentState(msg.DeploymentId, NodeDeploymentStatus.Applied, failureReason: null);
|
||||
// Gitea #601: the Applied ack is NOT sent here any more. It used to go out right now, before the
|
||||
// The Applied ack is NOT sent here any more. It used to go out right now, before the
|
||||
// address-space apply below had even been dequeued, so a degraded apply (swallowed sink faults,
|
||||
// _lastApplied not advanced) still sealed the deployment green fleet-wide. The ack is now held
|
||||
// until OpcUaPublishActor replies RebuildCompleted (HandleRebuildCompleted) or the outcome
|
||||
@@ -2700,7 +2738,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// #601: a throw after the Applied ack was put on hold — the Failed ack below is this apply's one
|
||||
// A throw after the Applied ack was put on hold — the Failed ack below is this apply's one
|
||||
// answer, so the held Applied must never follow it when the address-space outcome lands.
|
||||
DropPendingAck(msg.Correlation);
|
||||
UpsertNodeDeploymentState(msg.DeploymentId, NodeDeploymentStatus.Failed, ex.Message);
|
||||
@@ -2723,19 +2761,19 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gitea #601 — an Applied ack held until the address-space apply reports its outcome.</summary>
|
||||
/// <summary>An Applied ack held until the address-space apply reports its outcome.</summary>
|
||||
private sealed record PendingAck(DeploymentId DeploymentId, RevisionHash Revision, CorrelationId Correlation);
|
||||
|
||||
/// <summary>Gitea #601 — self-timer: the address-space outcome for this correlation did not arrive in time.</summary>
|
||||
/// <summary>Self-timer: the address-space outcome for this correlation did not arrive in time.</summary>
|
||||
private sealed record ApplyOutcomeTimedOut(CorrelationId Correlation);
|
||||
|
||||
private static string ApplyOutcomeTimerKey(CorrelationId correlation) => $"apply-outcome-{correlation}";
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #601 — holds the Applied ack for an apply whose revision has just advanced until the
|
||||
/// Holds the Applied ack for an apply whose revision has just advanced until the
|
||||
/// address-space apply outcome is known, arming <see cref="_applyOutcomeTimeout"/> as the backstop.
|
||||
/// With no publish actor wired (tests, a node with no OPC UA data plane) there is no outcome to wait
|
||||
/// for, so the ack is sent clean at once — byte-for-byte the pre-#601 behaviour.
|
||||
/// for, so the ack is sent clean at once — byte-for-byte the earlier behaviour.
|
||||
/// </summary>
|
||||
private void AwaitAddressSpaceOutcomeThenAck(DeploymentId deploymentId, RevisionHash revision, CorrelationId correlation)
|
||||
{
|
||||
@@ -2754,7 +2792,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
Timers.StartSingleTimer(ApplyOutcomeTimerKey(correlation), new ApplyOutcomeTimedOut(correlation), _applyOutcomeTimeout);
|
||||
}
|
||||
|
||||
/// <summary>Gitea #601 — forgets a held ack (its apply failed after all, and acks Failed instead).</summary>
|
||||
/// <summary>Forgets a held ack (its apply failed after all, and acks Failed instead).</summary>
|
||||
private void DropPendingAck(CorrelationId correlation)
|
||||
{
|
||||
if (_pendingAcks.Remove(correlation))
|
||||
@@ -2762,7 +2800,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #601 — the address-space apply reported its outcome: release the held ack, clean or
|
||||
/// The address-space apply reported its outcome: release the held ack, clean or
|
||||
/// degraded. Registered in every behaviour (Steady, Applying, Stale) because the reply can land in
|
||||
/// any of them — the host returns to Steady before the publish actor has even dequeued the rebuild.
|
||||
/// </summary>
|
||||
@@ -2780,7 +2818,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #601 — the address-space outcome did not arrive within <see cref="_applyOutcomeTimeout"/>.
|
||||
/// The address-space outcome did not arrive within <see cref="_applyOutcomeTimeout"/>.
|
||||
/// The ack is sent anyway so the coordinator does not time the deployment out on a node that DID
|
||||
/// advance its revision — but degraded, never clean: nobody has confirmed the address space.
|
||||
/// </summary>
|
||||
@@ -2801,7 +2839,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
|
||||
if (degraded)
|
||||
{
|
||||
// #601 review — the apply wrote this node's own row Applied/null before the outcome was known. Central
|
||||
// The apply wrote this node's own row Applied/null before the outcome was known. Central
|
||||
// adds the reason from the ack only when it ACCEPTS the ack (a timed-out or superseded deployment's ack
|
||||
// is discarded), so a node with a ConfigDb records it itself; a driver-only node no-ops here.
|
||||
UpsertNodeDeploymentState(pending.DeploymentId, NodeDeploymentStatus.Applied,
|
||||
@@ -2872,7 +2910,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// Read the deployment artifact + reconcile the set of running <see cref="DriverInstanceActor"/>
|
||||
/// children. Spawn missing, respawn on config change, stop removed/disabled drivers.
|
||||
/// When the artifact blob is empty (legacy ControlPlane tests, smoke fixtures) the reconcile is
|
||||
/// SKIPPED outright — see the issue #485 guard below — and when the configured
|
||||
/// SKIPPED outright — see the empty-artifact guard below — and when the configured
|
||||
/// <see cref="IDriverFactory"/> can't materialise any of the requested types this is a no-op.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
@@ -2907,7 +2945,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// <remarks>
|
||||
/// Called on the actor thread by the PreStart restore path and on a POOL thread by
|
||||
/// <see cref="BeginDirectApply"/>, so it must stay free of <see cref="Self"/> / <c>Context</c>
|
||||
/// (#491). It touches only <see cref="_dbFactory"/>, <see cref="_localNode"/> and
|
||||
/// It touches only <see cref="_dbFactory"/>, <see cref="_localNode"/> and
|
||||
/// <see cref="_log"/>, all of which are safe off the actor thread — the logger publishes to the
|
||||
/// EventStream and captures nothing from the cell.
|
||||
/// </remarks>
|
||||
@@ -2917,7 +2955,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
{
|
||||
// Phase 4 belt-and-suspenders: this ConfigDb read is Direct-only (the FetchAndCache apply uses
|
||||
// ReconcileDriversFromParsed with bytes in hand). A null factory can't read, so return null — the
|
||||
// caller treats that as "artifact could not be read" (#486) and keeps last-known-good.
|
||||
// caller treats that as "artifact could not be read" and keeps last-known-good.
|
||||
if (_dbFactory is null)
|
||||
{
|
||||
_log.Warning("DriverHost {Node}: no ConfigDb to load artifact for {Id}; skipping reconcile", _localNode, deploymentId);
|
||||
@@ -2948,7 +2986,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
// below async, and the only caller-side token is actor lifetime — the timeout gives the same
|
||||
// bound without touching the actor seam. A timeout surfaces as an exception through this
|
||||
// method's existing catch-to-null, so the apply FAILS and the revision does not advance,
|
||||
// keeping last-known-good served (the #485-family "no answer, never tear down" semantics —
|
||||
// keeping last-known-good served (the "no answer, never tear down" semantics —
|
||||
// already correct, unchanged here).
|
||||
//
|
||||
// Guarded on IsRelational: the timeout is a relational-only concept and the extension throws
|
||||
@@ -2975,7 +3013,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// <summary>
|
||||
/// An apply's artifact: the bytes that were reconciled plus the ONE
|
||||
/// <see cref="ParsedDeploymentArtifact"/> every later consumer of that apply reads. Both are
|
||||
/// <see langword="null"/> when nothing was applied (#485/#486).
|
||||
/// <see langword="null"/> when nothing was applied.
|
||||
/// </summary>
|
||||
/// <param name="Blob">The reconciled artifact bytes, or null when there was no usable artifact.</param>
|
||||
/// <param name="Parsed">The parsed artifact, owned by the apply handler — dispose before it returns.</param>
|
||||
@@ -2989,7 +3027,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// apply builds exactly one <c>JsonDocument</c> in this actor.
|
||||
/// </summary>
|
||||
/// <returns>The reconciled blob + its parsed artifact, or a default (both null) when the blob was
|
||||
/// empty or absent (#485).</returns>
|
||||
/// empty or absent.</returns>
|
||||
/// <exception cref="System.Text.Json.JsonException">The artifact bytes are not valid JSON. Unlike the
|
||||
/// lenient blob wrappers (malformed ⇒ no specs ⇒ every running child planned for StopChild) this
|
||||
/// surfaces, and <see cref="HandleArtifactForApply"/>'s catch turns it into a Failed ACK with the
|
||||
@@ -3027,7 +3065,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// "malformed ⇒ no specs" parse those paths have always had. The apply paths use
|
||||
/// <see cref="ReconcileDriversFromParsed"/> instead, so they parse once and keep the parse.
|
||||
/// </summary>
|
||||
/// <returns>The reconciled blob, or <see langword="null"/> when it was empty (#485).</returns>
|
||||
/// <returns>The reconciled blob, or <see langword="null"/> when it was empty.</returns>
|
||||
private byte[]? ReconcileDriversFromBlob(DeploymentId deploymentId, byte[]? blob)
|
||||
{
|
||||
blob ??= Array.Empty<byte>();
|
||||
@@ -3037,7 +3075,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
return null;
|
||||
}
|
||||
|
||||
// #601 (S-10): non-empty but malformed bytes are "no answer" too. The lenient static
|
||||
// Non-empty but malformed bytes are "no answer" too. The lenient static
|
||||
// ParseDriverInstances swallows the JsonException into an empty list, and an empty list plans every
|
||||
// running child for StopChild. Open throws instead, so a corrupt cache row / restored blob leaves
|
||||
// the running drivers alone, exactly like the zero-length case above.
|
||||
@@ -3060,7 +3098,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Issue #485 — no bytes is no answer, not "a configuration with no drivers". Parsing zero bytes
|
||||
/// No bytes is no answer, not "a configuration with no drivers". Parsing zero bytes
|
||||
/// yields zero specs, and <c>DriverSpawnPlanner</c> then plans EVERY running child for StopChild: a
|
||||
/// missing deployment row (or a half-written one) silently takes this node's entire field I/O down
|
||||
/// while the apply still ACKs Applied. Nothing legitimate produces a zero-length blob — deleting the
|
||||
@@ -3085,9 +3123,9 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
foreach (var id in plan.ToStop) StopChild(id);
|
||||
foreach (var spec in plan.ToSpawn) SpawnChild(spec);
|
||||
|
||||
// v3 (WP7): a dependency-consumer driver's declared dependency refs change when its tags/scripts are
|
||||
// edited. That edit changes its DriverConfig, which respawns it (#516), and the respawned driver's
|
||||
// adapter registers the NEW set in PreStart — no separate re-register is needed (#665).
|
||||
// v3: a dependency-consumer driver's declared dependency refs change when its tags/scripts are
|
||||
// edited. That edit changes its DriverConfig, which respawns it, and the respawned driver's
|
||||
// adapter registers the NEW set in PreStart — no separate re-register is needed.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -3327,7 +3365,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// </remarks>
|
||||
private void PushDesiredSubscriptionsFromArtifact(DeploymentId deploymentId, byte[] blob)
|
||||
{
|
||||
// Issue #485 — the same "no bytes is no answer" rule as ReconcileDrivers. An empty artifact parses
|
||||
// The same "no bytes is no answer" rule as ReconcileDrivers. An empty artifact parses
|
||||
// to a composition with no raw tags, which hands every child an EMPTY desired set — and an empty set
|
||||
// drops the live subscription handle rather than re-subscribing. Reachable independently of the
|
||||
// reconcile guard: this method does its OWN ConfigDb read, so the row can go missing between the two.
|
||||
@@ -3339,7 +3377,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
return;
|
||||
}
|
||||
|
||||
// #601 (S-10): Open, not the lenient static ParseComposition — that wrapper swallows malformed JSON
|
||||
// Open, not the lenient static ParseComposition — that wrapper swallows malformed JSON
|
||||
// into an EMPTY composition, which would hand every child an empty desired set and drop its live
|
||||
// subscription. A malformed artifact is no answer: leave the subscriptions as they are.
|
||||
AddressSpaceComposition composition;
|
||||
@@ -3449,7 +3487,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
_alarmNodeIdByDriverRef.Clear();
|
||||
_driverRefByAlarmNodeId.Clear();
|
||||
_alarmMetaByNodeId.Clear();
|
||||
// NOT _nativeAlarmProjector.Clear() (Gitea #641): it is pruned to the still-deployed conditions after the
|
||||
// NOT _nativeAlarmProjector.Clear(): it is pruned to the still-deployed conditions after the
|
||||
// maps are rebuilt below, so a redeploy that keeps an alarm keeps its projected prior state.
|
||||
// The per-driver secondary indexes are rebuilt WHOLE alongside their primaries below — same pass,
|
||||
// same clear — so no incremental maintenance exists to get out of step with them.
|
||||
@@ -3460,8 +3498,8 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
var key = (t.DriverInstanceId, t.NodeId); // (DriverInstanceId, RawPath) — RawPath IS the wire-ref
|
||||
if (t.Alarm is not null)
|
||||
{
|
||||
// Native alarm → a single Part 9 condition at the RawPath (Raw realm). WP4 adds the referencing
|
||||
// equipment notifier NodeIds; WP3 wires the single raw condition.
|
||||
// Native alarm → a single Part 9 condition at the RawPath (Raw realm). The referencing
|
||||
// equipment notifier NodeIds are added on top of the single raw condition.
|
||||
if (!_alarmNodeIdByDriverRef.TryGetValue(key, out var aset))
|
||||
{
|
||||
_alarmNodeIdByDriverRef[key] = aset = new HashSet<NodeRealmRef>();
|
||||
@@ -3473,8 +3511,8 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
aset.Add(new NodeRealmRef(t.NodeId, AddressSpaceRealm.Raw));
|
||||
// Inverse 1:1 map for the inbound native-condition ack path (keyed by BARE condition NodeId).
|
||||
_driverRefByAlarmNodeId[t.NodeId] = key;
|
||||
// Per-condition metadata for the alerts fan-out. WP4 refines /alerts identity to the referencing
|
||||
// equipment paths; WP3 keys the display off the RawPath. The referencing-equipment paths (the
|
||||
// Per-condition metadata for the alerts fan-out. The /alerts identity is refined to the referencing
|
||||
// equipment paths; the display is keyed off the RawPath. The referencing-equipment paths (the
|
||||
// Area/Line/Equipment UNS folder paths carried on the RawTagPlan) ride onto every transition so
|
||||
// the /alerts row lists which equipment reference this raw condition.
|
||||
_alarmMetaByNodeId[t.NodeId] = (t.NodeId, t.Name, t.Alarm.AlarmType, t.Alarm.HistorizeToAveva, t.ReferencingEquipmentPaths);
|
||||
@@ -3504,7 +3542,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
}
|
||||
}
|
||||
|
||||
// Gitea #641 — forget the projected prior state of conditions this composition no longer carries, and keep
|
||||
// Forget the projected prior state of conditions this composition no longer carries, and keep
|
||||
// the rest. Clearing it whole on every apply made the next driver Acknowledge of a still-active alarm
|
||||
// project Active=false (the default prior), and the next Clear of an unacknowledged one project
|
||||
// Acked=true — on any deploy, rebuild or not. The node manager carries the NODE's state across a rebuild,
|
||||
@@ -3596,7 +3634,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
|
||||
private void SpawnChild(DriverInstanceSpec spec)
|
||||
{
|
||||
// #609: an instance id removed by an earlier deployment (StopChild ⇒ Evict, which also holds back late
|
||||
// An instance id removed by an earlier deployment (StopChild ⇒ Evict, which also holds back late
|
||||
// items) may come back; lift the hold BEFORE the child exists, since it publishes health as it starts.
|
||||
// The resilience tracker's equivalent is lifted by _invokerFactory.Create below.
|
||||
_telemetryHub?.Readmit(spec.DriverInstanceId);
|
||||
@@ -3609,7 +3647,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
{
|
||||
// A factory throw is a CONFIG error, not a connectivity one — TryCreate is pure parsing;
|
||||
// device I/O happens later in InitializeAsync. Logged at Error because the node silently
|
||||
// degrades to a stub that answers nothing, and since #516 routed DriverConfig changes
|
||||
// degrades to a stub that answers nothing, and since DriverConfig changes are now routed
|
||||
// through a respawn this path is now reachable by an ordinary operator edit rather than
|
||||
// only by a brand-new driver. It still does NOT fail the deployment — making it do so is
|
||||
// a deliberate follow-up, since it would let one malformed driver block a fleet deploy.
|
||||
@@ -3673,12 +3711,12 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
// supervision reprovision. Sending it HERE rather than at each call site is the point: a child that
|
||||
// exists without its subscription set is a dead driver, so the two facts are established together.
|
||||
// Before this, the only sender was the apply's bulk push, so an operator's Restart produced a child
|
||||
// that connected and then read nothing until the next deployment — the #488 periodic reconcile cannot
|
||||
// that connected and then read nothing until the next deployment — the periodic reconcile cannot
|
||||
// help, because it early-returns on an empty desired set.
|
||||
//
|
||||
// ORDERING, and its one accepted cost: ApplyDriverSpecs (which spawns) runs BEFORE
|
||||
// ApplyDesiredSubscriptions (which rebuilds _desiredByDriver), so on a deploy that RESPAWNS a child
|
||||
// (#516 — the merged DriverConfig folds in the raw tags, so a tag-set edit changes the spec) the new
|
||||
// (the merged DriverConfig folds in the raw tags, so a tag-set edit changes the spec) the new
|
||||
// child is handed the PREVIOUS apply's set, and nothing at all on the first deploy. That transient is
|
||||
// superseded a moment later by the bulk push, on the same sender→receiver pair, so Akka's per-pair
|
||||
// ordering guarantees the child CONVERGES on the new set — but it is NOT guaranteed to be inert on
|
||||
@@ -3692,7 +3730,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
// Sent after InitializeRequested above, which keeps init-before-subscribe ordering.
|
||||
SendDesiredSubscriptions(child, spec.DriverInstanceId);
|
||||
|
||||
// v3 (WP7): if the (real, non-stub) driver consumes other tags' live values, spawn a mux-adapter
|
||||
// v3: if the (real, non-stub) driver consumes other tags' live values, spawn a mux-adapter
|
||||
// child that registers its dependency refs on the per-node dependency mux and forwards changes
|
||||
// into the driver. Requires a wired mux (dev/None deployments have none — then calc inputs simply
|
||||
// can't flow, matching the VirtualTag dev path).
|
||||
@@ -3724,7 +3762,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
StopDependencyAdapter(driverInstanceId);
|
||||
Context.Stop(entry.Actor);
|
||||
_children.Remove(driverInstanceId);
|
||||
// #609: the instance left the plan (StopChild is only reached via plan.ToStop), so its cached
|
||||
// The instance left the plan (StopChild is only reached via plan.ToStop), so its cached
|
||||
// Health/Resilience snapshots must not replay to new telemetry subscribers. Release the shared
|
||||
// resilience state FIRST: the resilience publisher re-emits the tracker's snapshots every tick, so
|
||||
// evicting the hub alone was undone within one interval. Context.Stop is asynchronous, so the child can
|
||||
@@ -3828,7 +3866,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
// be in flight from an OLD incarnation after HandleRestartDriver stopped that child and spawned a
|
||||
// replacement under the same DriverInstanceId. Delivered to the new child — which is in Connecting,
|
||||
// where InitializeRequested IS handled — it would start a second, concurrent InitializeAsync,
|
||||
// racing the dispose-before-overwrite that #527's teardown exists to serialise
|
||||
// racing the dispose-before-overwrite that the teardown exists to serialise
|
||||
// (ModbusDriver._transport, MqttDriver._connection each leak a transport on that race).
|
||||
if (!Equals(child, entry.Actor))
|
||||
{
|
||||
@@ -3948,7 +3986,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
{
|
||||
// Synchronous by design, and a recorded follow-up rather than an oversight: this is a
|
||||
// bounded per-apply write of ONE row (at most three per deployment), not the multi-MB
|
||||
// artifact read that Task 26 moved off the mailbox. It is also the apply's crash-boundary
|
||||
// artifact read that moved off the mailbox. It is also the apply's crash-boundary
|
||||
// marker, which is easiest to reason about while it is ordered on the actor thread.
|
||||
using var db = _dbFactory.CreateDbContext();
|
||||
var existing = db.NodeDeploymentStates.FirstOrDefault(
|
||||
|
||||
Reference in New Issue
Block a user