docs(comments): strip internal task/milestone/bundle bookkeeping from code comments
Remove project bookkeeping citations from shipped code comments across the solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/ issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels, and C/D/K/S/T phase labels. Comment text only — no code logic, string/log literals, or XML-doc structure changed. Genuine descriptions are preserved (only the citation is stripped), and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365, UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker TaskReferenceInComment / TrackingReferenceInComment checks plus targeted grep passes; full solution builds clean, append-only guard tests pass.
This commit is contained in:
@@ -11,7 +11,7 @@ using ZB.MOM.WW.ScadaBridge.SiteEventLogging;
|
||||
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Actors;
|
||||
|
||||
/// <summary>
|
||||
/// WP-6: Connection actor using Akka.NET Become/Stash pattern for lifecycle state machine.
|
||||
/// Connection actor using Akka.NET Become/Stash pattern for lifecycle state machine.
|
||||
///
|
||||
/// States:
|
||||
/// - Connecting: stash subscribe/write requests; attempts connection
|
||||
@@ -19,12 +19,12 @@ namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Actors;
|
||||
/// - Reconnecting: push bad quality for all subscribed tags, stash new requests,
|
||||
/// fixed-interval reconnect
|
||||
///
|
||||
/// WP-9: Auto-reconnect with bad quality on disconnect.
|
||||
/// WP-10: Transparent re-subscribe after reconnection.
|
||||
/// WP-11: Write-back support (synchronous failure to caller, no S&F).
|
||||
/// WP-12: Tag path resolution with retry.
|
||||
/// WP-13: Health reporting (connection status + tag resolution counts).
|
||||
/// WP-14: Subscription lifecycle (register on create, cleanup on stop).
|
||||
/// Auto-reconnect with bad quality on disconnect.
|
||||
/// Transparent re-subscribe after reconnection.
|
||||
/// Write-back support (synchronous failure to caller, no S&F).
|
||||
/// Tag path resolution with retry.
|
||||
/// Health reporting (connection status + tag resolution counts).
|
||||
/// Subscription lifecycle (register on create, cleanup on stop).
|
||||
/// </summary>
|
||||
public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
{
|
||||
@@ -55,7 +55,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
private readonly Dictionary<string, string> _subscriptionIds = new();
|
||||
|
||||
/// <summary>
|
||||
/// DataConnectionLayer-008: reverse index of how many instances subscribe to each
|
||||
/// Reverse index of how many instances subscribe to each
|
||||
/// tag path. Lets <see cref="HandleUnsubscribe"/> decide whether any other instance
|
||||
/// still needs a tag in O(1) instead of scanning every instance's tag set.
|
||||
/// </summary>
|
||||
@@ -67,14 +67,14 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
private readonly HashSet<string> _unresolvedTags = new();
|
||||
|
||||
/// <summary>
|
||||
/// DataConnectionLayer-010: tags whose retry SubscribeAsync is currently in flight.
|
||||
/// Tags whose retry SubscribeAsync is currently in flight.
|
||||
/// They are excluded from the next retry tick so a slow attempt is not duplicated
|
||||
/// (which would leak monitored items / subscription ids).
|
||||
/// </summary>
|
||||
private readonly HashSet<string> _resolutionInFlight = new();
|
||||
|
||||
/// <summary>
|
||||
/// DataConnectionLayer-018: tags whose initial SubscribeAsync (issued from
|
||||
/// Tags whose initial SubscribeAsync (issued from
|
||||
/// <see cref="HandleSubscribe"/>) is currently in flight. Two parallel
|
||||
/// <c>SubscribeTagsRequest</c> messages for different instances sharing a tag
|
||||
/// path would otherwise both observe "not subscribed" against
|
||||
@@ -94,7 +94,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, IActorRef> _subscribers = new();
|
||||
|
||||
// ── Native alarm subscriptions (Task-10) ──
|
||||
// ── Native alarm subscriptions ──
|
||||
// The connection opens one alarm feed per source reference; transitions are
|
||||
// routed to subscribers (NativeAlarmActors) by source-object reference.
|
||||
/// <summary>sourceReference → set of subscriber actor refs (NativeAlarmActors), for routing + ref-count.</summary>
|
||||
@@ -107,7 +107,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, string?> _alarmSourceFilter = new();
|
||||
/// <summary>
|
||||
/// sourceReference → parsed condition-type predicate (M2.4 / #8). The authoritative
|
||||
/// sourceReference → parsed condition-type predicate. The authoritative
|
||||
/// client-side gate in <see cref="HandleAlarmTransitionReceived"/>; applies uniformly
|
||||
/// across OPC UA and the gateway-wide MxGateway feed.
|
||||
/// </summary>
|
||||
@@ -138,7 +138,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
private DateTimeOffset _lastConnectedAt;
|
||||
|
||||
/// <summary>
|
||||
/// DataConnectionLayer-011: monotonically increasing tag that identifies the
|
||||
/// Monotonically increasing tag that identifies the
|
||||
/// current adapter instance. Subscription callbacks capture the generation in
|
||||
/// effect when they were created; a <see cref="TagValueReceived"/> whose
|
||||
/// generation no longer matches comes from a disposed adapter and is dropped so
|
||||
@@ -318,7 +318,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
break;
|
||||
case SubscribeCompleted sc:
|
||||
// In Connected state, a connection-level subscribe failure must drive
|
||||
// the reconnection state machine (DataConnectionLayer-004).
|
||||
// the reconnection state machine.
|
||||
if (HandleSubscribeCompleted(sc))
|
||||
{
|
||||
_log.Warning("[{0}] Connection-level subscribe failure — entering Reconnecting", _connectionName);
|
||||
@@ -426,7 +426,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
_connectionDetails = newConfig;
|
||||
_adapter.Disconnected += OnAdapterDisconnected;
|
||||
|
||||
// DataConnectionLayer-011: new adapter — bump the generation so callbacks
|
||||
// New adapter — bump the generation so callbacks
|
||||
// from the disposed adapter are recognised as stale and dropped.
|
||||
_adapterGeneration++;
|
||||
|
||||
@@ -452,10 +452,10 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
|
||||
Become(Reconnecting);
|
||||
|
||||
// WP-9: Push bad quality for all subscribed tags on disconnect
|
||||
// Push bad quality for all subscribed tags on disconnect
|
||||
PushBadQualityForAllTags();
|
||||
|
||||
// Task-10: notify native alarm subscribers the source feed is unavailable
|
||||
// Notify native alarm subscribers the source feed is unavailable
|
||||
// (mark mirrored alarms uncertain; the reconnect snapshot reconciles them).
|
||||
PushAlarmSourceUnavailable();
|
||||
|
||||
@@ -555,7 +555,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
}
|
||||
else
|
||||
{
|
||||
// DataConnectionLayer-015: the INITIAL connect must participate in the
|
||||
// The INITIAL connect must participate in the
|
||||
// failover counter exactly like a reconnect. Without this a primary that is
|
||||
// unreachable when the actor first starts (fresh deployment, site restart, or
|
||||
// a primary simply down) is retried forever and the configured backup is
|
||||
@@ -582,9 +582,9 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
$"Connection restored on {_activeEndpoint} endpoint", null);
|
||||
}
|
||||
|
||||
// WP-10: Transparent re-subscribe — re-establish all active subscriptions
|
||||
// Transparent re-subscribe — re-establish all active subscriptions
|
||||
ReSubscribeAll();
|
||||
// Task-10: re-establish native alarm feeds (source replays a snapshot).
|
||||
// Re-establish native alarm feeds (source replays a snapshot).
|
||||
ReSubscribeAllAlarms();
|
||||
|
||||
BecomeConnected();
|
||||
@@ -601,8 +601,8 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
/// Shared connect-failure handling for both the initial connect (Connecting state)
|
||||
/// and reconnect (Reconnecting state). Assumes <see cref="_consecutiveFailures"/> has
|
||||
/// already been incremented for the current failure. Switches to the other endpoint
|
||||
/// once the retry count is exhausted and a backup is configured
|
||||
/// (DataConnectionLayer-015 brought the initial connect onto this path).
|
||||
/// once the retry count is exhausted and a backup is configured; the initial
|
||||
/// connect follows this same path.
|
||||
/// </summary>
|
||||
private void CountFailureAndMaybeFailover(string? error)
|
||||
{
|
||||
@@ -630,7 +630,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
// Wire disconnect handler on new adapter
|
||||
_adapter.Disconnected += OnAdapterDisconnected;
|
||||
|
||||
// DataConnectionLayer-011: new adapter — bump the generation so callbacks
|
||||
// New adapter — bump the generation so callbacks
|
||||
// from the disposed adapter are recognised as stale and dropped.
|
||||
_adapterGeneration++;
|
||||
|
||||
@@ -661,7 +661,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
BecomeReconnecting();
|
||||
}
|
||||
|
||||
// ── Subscription Management (WP-14) ──
|
||||
// ── Subscription Management ──
|
||||
|
||||
private void HandleSubscribe(SubscribeTagsRequest request)
|
||||
{
|
||||
@@ -675,11 +675,11 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
|
||||
var self = Self;
|
||||
var sender = Sender;
|
||||
// DataConnectionLayer-011: capture the current adapter generation so callbacks
|
||||
// Capture the current adapter generation so callbacks
|
||||
// from this adapter can be distinguished from a later (post-failover) adapter.
|
||||
var generation = _adapterGeneration;
|
||||
|
||||
// DataConnectionLayer-018: partition tags on the actor thread into "this
|
||||
// Partition tags on the actor thread into "this
|
||||
// request will issue _adapter.SubscribeAsync" vs. "already subscribed (by us
|
||||
// or by another in-flight SubscribeTagsRequest)". A tag that is already in
|
||||
// _subscriptionIds OR currently in _subscribesInFlight is treated as
|
||||
@@ -727,7 +727,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// DataConnectionLayer-004: distinguish a connection-level fault
|
||||
// Distinguish a connection-level fault
|
||||
// (adapter not connected / transport down) from a genuine
|
||||
// node-not-found. Connection-level faults must drive the
|
||||
// reconnection state machine, not be retried as unresolved tags.
|
||||
@@ -740,7 +740,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
|
||||
// Initial read — capture current values for resolved tags so the Instance
|
||||
// Actor doesn't stay Uncertain until the next data-change notification.
|
||||
// DataConnectionLayer-026: these are NOT delivered here. Emitting a
|
||||
// These are NOT delivered here. Emitting a
|
||||
// TagValueReceived now (inside the background subscribe task) races ahead of
|
||||
// the SubscribeCompleted that registers this instance's tags in
|
||||
// _subscriptionsByInstance, so HandleTagValueReceived's fan-out finds no
|
||||
@@ -749,7 +749,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
// MES field that never changes) the dropped seed is the only value it will
|
||||
// ever produce — leaving the attribute Uncertain forever. So the seeds ride
|
||||
// back on SubscribeCompleted and are delivered after registration.
|
||||
// DataConnectionLayer-027: SeedTagsAsync retries the still-empty reads so a
|
||||
// SeedTagsAsync retries the still-empty reads so a
|
||||
// seed that races the just-created advise (returns VT_EMPTY) is not silently
|
||||
// dropped, and logs any tag that never yields a value.
|
||||
var seedValues = await SeedTagsAsync(_adapter, tagsToSeed);
|
||||
@@ -759,7 +759,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DataConnectionLayer-027: reads the current value of each tag so the Instance Actor
|
||||
/// Reads the current value of each tag so the Instance Actor
|
||||
/// has an initial value, retrying the still-empty subset a bounded number of times. A
|
||||
/// STATIC tag (one that emits no further OnDataChange after the advise) depends
|
||||
/// entirely on this seed; on a cold/fresh advise the read can race the just-created
|
||||
@@ -785,12 +785,12 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
{
|
||||
foreach (var tagPath in pending.ToList())
|
||||
{
|
||||
// DataConnectionLayer-027: bound each per-tag read with SeedReadTimeout so a
|
||||
// Bound each per-tag read with SeedReadTimeout so a
|
||||
// hung device read cannot delay SubscribeCompleted/the ack indefinitely.
|
||||
// On timeout the catch block treats it identically to any other failed seed
|
||||
// read — the tag stays in pending, is retried up to SeedReadMaxAttempts, and
|
||||
// left Uncertain if still empty after the budget. Same CancellationTokenSource
|
||||
// mechanism used by HandleWrite for WriteTimeout (DataConnectionLayer-005).
|
||||
// mechanism used by HandleWrite for WriteTimeout.
|
||||
using var cts = new CancellationTokenSource(_options.SeedReadTimeout);
|
||||
try
|
||||
{
|
||||
@@ -830,7 +830,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
/// Applies the result of an asynchronous subscribe on the actor thread. ALL mutation
|
||||
/// of subscription state and counters happens here — never on the background task —
|
||||
/// so the actor model's single-threaded state guarantee holds.
|
||||
/// Returns <c>true</c> if any tag failed at connection level (DataConnectionLayer-004),
|
||||
/// Returns <c>true</c> if any tag failed at connection level,
|
||||
/// signalling the caller (only the Connected state) to enter Reconnecting.
|
||||
/// </summary>
|
||||
private bool HandleSubscribeCompleted(SubscribeCompleted msg)
|
||||
@@ -838,7 +838,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
var instanceName = msg.Request.InstanceUniqueName;
|
||||
if (!_subscriptionsByInstance.TryGetValue(instanceName, out var instanceTags))
|
||||
{
|
||||
// DataConnectionLayer-021: the instance was unsubscribed while the
|
||||
// The instance was unsubscribed while the
|
||||
// subscribe I/O was in flight. Re-creating the per-instance entry and
|
||||
// applying counter/handle mutations here would permanently leak state
|
||||
// — _subscriptionsByInstance[instanceName] resurrected with no
|
||||
@@ -858,7 +858,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
|
||||
foreach (var result in msg.Results)
|
||||
{
|
||||
// DCL-018: clear in-flight markers we placed in HandleSubscribe.
|
||||
// Clear in-flight markers we placed in HandleSubscribe.
|
||||
if (!result.AlreadySubscribed)
|
||||
_subscribesInFlight.Remove(result.TagPath);
|
||||
|
||||
@@ -879,7 +879,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
return false;
|
||||
}
|
||||
|
||||
// DataConnectionLayer-004: if any tag failed because the adapter is not
|
||||
// If any tag failed because the adapter is not
|
||||
// connected (a connection-level fault), the subscribe needs the reconnection
|
||||
// state machine, not the tag-resolution retry. Drive a disconnect and let the
|
||||
// request be re-stashed/retried after reconnect via ReSubscribeAll.
|
||||
@@ -887,7 +887,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
|
||||
foreach (var result in msg.Results)
|
||||
{
|
||||
// DataConnectionLayer-018: a result with AlreadySubscribed: false means
|
||||
// A result with AlreadySubscribed: false means
|
||||
// this request was responsible for the SubscribeAsync call — the tag
|
||||
// was added to _subscribesInFlight in HandleSubscribe. Clear it now so
|
||||
// a later SubscribeTagsRequest for the same tag isn't forever treated
|
||||
@@ -896,7 +896,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
if (!result.AlreadySubscribed)
|
||||
_subscribesInFlight.Remove(result.TagPath);
|
||||
|
||||
// DataConnectionLayer-008: only a tag newly added to THIS instance's set
|
||||
// Only a tag newly added to THIS instance's set
|
||||
// increments the reference count, so the count stays an accurate "number
|
||||
// of distinct instances subscribed to this tag".
|
||||
if (instanceTags.Add(result.TagPath))
|
||||
@@ -912,7 +912,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
{
|
||||
_subscriptionIds[result.TagPath] = result.SubscriptionId!;
|
||||
|
||||
// DataConnectionLayer-020: distinguish fresh subscribe from
|
||||
// Distinguish fresh subscribe from
|
||||
// unresolved → resolved promotion. If an earlier instance's
|
||||
// subscribe for this tag had failed at the resolution layer
|
||||
// (the tag was already added to _unresolvedTags AND already
|
||||
@@ -943,9 +943,9 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
}
|
||||
else
|
||||
{
|
||||
// WP-12: genuine tag resolution failure — mark unresolved so the
|
||||
// periodic retry timer picks it up. DataConnectionLayer-020:
|
||||
// only increment _totalSubscribed when the tag is genuinely
|
||||
// Genuine tag resolution failure — mark unresolved so the
|
||||
// periodic retry timer picks it up. Only increment _totalSubscribed
|
||||
// when the tag is genuinely
|
||||
// newly-tracked. A second instance failing to resolve a tag the
|
||||
// first instance already added to _unresolvedTags is the same
|
||||
// logical tag, counted once — bumping _totalSubscribed again
|
||||
@@ -958,7 +958,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
_log.Debug("[{0}] Tag resolution failed for {1}: {2}",
|
||||
_connectionName, result.TagPath, result.Error);
|
||||
|
||||
// DataConnectionLayer-004 / design doc Tag Path Resolution step 2:
|
||||
// Design doc Tag Path Resolution step 2:
|
||||
// mark the attribute quality `bad` so the Instance Actor sees a
|
||||
// signal rather than staying Uncertain indefinitely.
|
||||
if (_subscribers.TryGetValue(instanceName, out var subscriber))
|
||||
@@ -969,7 +969,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
}
|
||||
}
|
||||
|
||||
// DataConnectionLayer-026: now that every tag is registered in
|
||||
// Now that every tag is registered in
|
||||
// _subscriptionsByInstance, deliver the values captured by the initial read.
|
||||
// Re-entering via Self reuses HandleTagValueReceived's generation guard, fan-out
|
||||
// and quality accounting — and crucially runs AFTER registration, so the value
|
||||
@@ -985,7 +985,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
}
|
||||
|
||||
// Start the tag-resolution retry timer if any tags are unresolved.
|
||||
// DataConnectionLayer-022: StartPeriodicTimer with an existing key CANCELS
|
||||
// StartPeriodicTimer with an existing key CANCELS
|
||||
// and replaces the prior timer, so a fan-out of SubscribeTagsRequests
|
||||
// arriving faster than TagResolutionRetryInterval would keep resetting
|
||||
// the timer and starve the retry indefinitely. Gating on IsTimerActive
|
||||
@@ -1000,7 +1000,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
_options.TagResolutionRetryInterval);
|
||||
}
|
||||
|
||||
// DataConnectionLayer-016: the response must match the actor's own assessment.
|
||||
// The response must match the actor's own assessment.
|
||||
// When a connection-level failure is driving the actor into Reconnecting, the
|
||||
// tags were never subscribed at the adapter — replying Success: true would tell
|
||||
// the Instance Actor the subscribe succeeded when it did not. Genuine
|
||||
@@ -1020,7 +1020,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DataConnectionLayer-004: classifies a subscribe exception as a connection-level
|
||||
/// Classifies a subscribe exception as a connection-level
|
||||
/// fault (adapter not connected / transport down) versus a genuine tag-resolution
|
||||
/// failure (the node does not exist on the device). Connection-level faults must
|
||||
/// drive the reconnection state machine; resolution failures are retried on the
|
||||
@@ -1043,10 +1043,10 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
if (!_subscriptionsByInstance.TryGetValue(request.InstanceUniqueName, out var tags))
|
||||
return;
|
||||
|
||||
// WP-14: Cleanup on Instance Actor stop
|
||||
// Cleanup on Instance Actor stop
|
||||
foreach (var tagPath in tags)
|
||||
{
|
||||
// DataConnectionLayer-008: drop this instance's reference; the tag is only
|
||||
// Drop this instance's reference; the tag is only
|
||||
// released at the adapter when no other instance still subscribes to it.
|
||||
// The reference count makes this O(1) instead of an O(instances) scan.
|
||||
var remaining = _tagSubscriberCount.GetValueOrDefault(tagPath) - 1;
|
||||
@@ -1070,7 +1070,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
_totalSubscribed--;
|
||||
_resolvedTags--;
|
||||
|
||||
// DataConnectionLayer-006: drop the tag's tracked quality so it is no
|
||||
// Drop the tag's tracked quality so it is no
|
||||
// longer counted by PushBadQualityForAllTags (which sets _tagsBadQuality
|
||||
// from _lastTagQuality.Count). Leaving it here drifts the quality
|
||||
// counters above _totalSubscribed across disconnect cycles.
|
||||
@@ -1099,27 +1099,27 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
_subscriptionsByInstance.Remove(request.InstanceUniqueName);
|
||||
_subscribers.Remove(request.InstanceUniqueName);
|
||||
|
||||
// DataConnectionLayer-006: keep the reported quality counters in sync after the
|
||||
// Keep the reported quality counters in sync after the
|
||||
// unsubscribed tags' buckets were decremented above.
|
||||
_healthCollector.UpdateTagQuality(_connectionName, _tagsGoodQuality, _tagsBadQuality, _tagsUncertainQuality);
|
||||
_healthCollector.UpdateTagResolution(_connectionName, _totalSubscribed, _resolvedTags);
|
||||
}
|
||||
|
||||
// ── Write Support (WP-11) ──
|
||||
// ── Write Support ──
|
||||
|
||||
private void HandleWrite(WriteTagRequest request)
|
||||
{
|
||||
_log.Debug("[{0}] Writing to tag {1}", _connectionName, request.TagPath);
|
||||
var sender = Sender;
|
||||
|
||||
// DataConnectionLayer-005: bound the write with WriteTimeout. A hung device
|
||||
// Bound the write with WriteTimeout. A hung device
|
||||
// write (TCP black-hole) would otherwise never complete, so PipeTo never
|
||||
// fires and the calling script gets no DCL-level error. The CancellationToken
|
||||
// is passed to the adapter; on timeout we translate cancellation into a
|
||||
// failed WriteTagResponse so the failure is returned synchronously (WP-11).
|
||||
// failed WriteTagResponse so the failure is returned synchronously.
|
||||
var cts = new CancellationTokenSource(_options.WriteTimeout);
|
||||
|
||||
// WP-11: Write through DCL to device, failure returned synchronously
|
||||
// Write through DCL to device, failure returned synchronously
|
||||
_adapter.WriteAsync(request.TagPath, request.Value, cts.Token).ContinueWith(t =>
|
||||
{
|
||||
cts.Dispose();
|
||||
@@ -1145,7 +1145,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
/// <see cref="WriteTagBatchRequest.Values"/> to the device in ONE adapter
|
||||
/// <c>WriteBatchAsync</c> round-trip, then (if present) writes the trigger flag with
|
||||
/// a single <c>WriteAsync</c>. Both legs share one <see cref="DataConnectionOptions.WriteTimeout"/>
|
||||
/// budget (DataConnectionLayer-005). Any failed value in the batch, a failed trigger,
|
||||
/// budget. Any failed value in the batch, a failed trigger,
|
||||
/// the timeout, or an adapter exception is translated into a failed
|
||||
/// <see cref="WriteTagBatchResponse"/> returned synchronously to the caller — never a
|
||||
/// dropped reply. NOTE: this deliberately composes the batch + trigger primitives and
|
||||
@@ -1491,7 +1491,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
}).PipeTo(sender);
|
||||
}
|
||||
|
||||
// ── Tag Resolution Retry (WP-12) ──
|
||||
// ── Tag Resolution Retry ──
|
||||
|
||||
private void HandleRetryTagResolution()
|
||||
{
|
||||
@@ -1503,7 +1503,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
|
||||
var self = Self;
|
||||
|
||||
// DataConnectionLayer-010: only dispatch retries for tags that do not already
|
||||
// Only dispatch retries for tags that do not already
|
||||
// have an attempt in flight. A slow SubscribeAsync overlapping the next tick
|
||||
// would otherwise produce duplicate concurrent subscribes for the same tag.
|
||||
var toResolve = _unresolvedTags.Where(t => !_resolutionInFlight.Contains(t)).ToList();
|
||||
@@ -1533,7 +1533,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bad Quality Push (WP-9) ──
|
||||
// ── Bad Quality Push ──
|
||||
|
||||
private void PushBadQualityForAllTags()
|
||||
{
|
||||
@@ -1555,7 +1555,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
_healthCollector.UpdateTagQuality(_connectionName, _tagsGoodQuality, _tagsBadQuality, _tagsUncertainQuality);
|
||||
}
|
||||
|
||||
// ── Re-subscribe (WP-10) ──
|
||||
// ── Re-subscribe ──
|
||||
|
||||
private void ReSubscribeAll()
|
||||
{
|
||||
@@ -1572,7 +1572,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
_subscriptionIds.Clear();
|
||||
_unresolvedTags.Clear();
|
||||
_resolutionInFlight.Clear();
|
||||
// DataConnectionLayer-018: symmetric with _resolutionInFlight — any pending
|
||||
// Symmetric with _resolutionInFlight — any pending
|
||||
// initial-subscribe completions from the previous adapter generation will
|
||||
// post SubscribeCompleted to the actor, but ReSubscribeAll has just emptied
|
||||
// the in-flight tracking; the stale completion simply has nothing to
|
||||
@@ -1580,7 +1580,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
_subscribesInFlight.Clear();
|
||||
_resolvedTags = 0;
|
||||
|
||||
// DataConnectionLayer-006: reset the quality tracking too. Otherwise tags
|
||||
// Reset the quality tracking too. Otherwise tags
|
||||
// resolved for the first time after reconnect (never in _lastTagQuality) only
|
||||
// increment their bucket and the totals drift above _totalSubscribed. They are
|
||||
// repopulated from fresh TagValueReceived messages once subscriptions activate.
|
||||
@@ -1604,10 +1604,10 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
}).PipeTo(self);
|
||||
}
|
||||
|
||||
// DataConnectionLayer-027: re-advising alone does NOT restore a STATIC tag's value.
|
||||
// Re-advising alone does NOT restore a STATIC tag's value.
|
||||
// PushBadQualityForAllTags flipped every tag Bad on disconnect, and a tag that fires
|
||||
// no further OnDataChange would stay Bad/Uncertain across the reconnect even though
|
||||
// the source reads Good — the same seed gap as the initial subscribe (DCL-026/027),
|
||||
// the source reads Good — the same seed gap as the initial subscribe,
|
||||
// which this path previously did not cover. Mirror HandleSubscribe and re-seed: read
|
||||
// each re-advised tag's current value and deliver it through the normal
|
||||
// generation-guarded path. Capture the adapter + generation up front so a later
|
||||
@@ -1616,7 +1616,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
// generation no longer matches. Delivery fans out via _subscriptionsByInstance
|
||||
// (preserved across reconnect), so it does not depend on _subscriptionIds being
|
||||
// repopulated for the new adapter generation. Seeds delivered after registration is
|
||||
// already established, so the DCL-026 ordering hazard does not apply here.
|
||||
// already established, so the ordering hazard does not apply here.
|
||||
var reseedAdapter = _adapter;
|
||||
Task.Run(async () =>
|
||||
{
|
||||
@@ -1635,7 +1635,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
});
|
||||
}
|
||||
|
||||
// ── Health Reporting (WP-13) ──
|
||||
// ── Health Reporting ──
|
||||
|
||||
private void ReplyWithHealthReport()
|
||||
{
|
||||
@@ -1651,7 +1651,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
|
||||
private void HandleTagResolutionSucceeded(TagResolutionSucceeded msg)
|
||||
{
|
||||
// DataConnectionLayer-010: the retry attempt for this tag has completed.
|
||||
// The retry attempt for this tag has completed.
|
||||
_resolutionInFlight.Remove(msg.TagPath);
|
||||
|
||||
if (_unresolvedTags.Remove(msg.TagPath))
|
||||
@@ -1673,11 +1673,11 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
_log.Debug("[{0}] Tag resolution still failing for {1}: {2}",
|
||||
_connectionName, msg.TagPath, msg.Error);
|
||||
|
||||
// DataConnectionLayer-010: the retry attempt for this tag has completed —
|
||||
// The retry attempt for this tag has completed —
|
||||
// it is eligible for the next retry tick again.
|
||||
_resolutionInFlight.Remove(msg.TagPath);
|
||||
|
||||
// Track as unresolved so periodic retry picks it up. DCL-022: gate on
|
||||
// Track as unresolved so periodic retry picks it up. Gate on
|
||||
// IsTimerActive so a stream of TagResolutionFailed events doesn't keep
|
||||
// cancelling and re-starting the timer faster than its own interval.
|
||||
if (_unresolvedTags.Add(msg.TagPath) && !Timers.IsTimerActive("tag-resolution-retry"))
|
||||
@@ -1692,7 +1692,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
|
||||
private void HandleTagValueReceived(TagValueReceived msg)
|
||||
{
|
||||
// DataConnectionLayer-011: drop values delivered by a disposed adapter. After a
|
||||
// Drop values delivered by a disposed adapter. After a
|
||||
// failover the old adapter's OPC UA SDK threads may still fire callbacks; those
|
||||
// carry a stale generation and must not be forwarded to Instance Actors.
|
||||
if (msg.AdapterGeneration != _adapterGeneration)
|
||||
@@ -1737,7 +1737,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
_healthCollector.UpdateTagQuality(_connectionName, _tagsGoodQuality, _tagsBadQuality, _tagsUncertainQuality);
|
||||
}
|
||||
|
||||
// ── Native alarm subscriptions (Task-10) ──
|
||||
// ── Native alarm subscriptions ──
|
||||
|
||||
private void HandleSubscribeAlarms(SubscribeAlarmsRequest request)
|
||||
{
|
||||
@@ -1762,7 +1762,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
subs.Add(subscriber);
|
||||
_alarmSourceFilter[request.SourceReference] = request.ConditionFilter;
|
||||
// Parse the type-name filter once; this is the authoritative client-side
|
||||
// gate consulted on every routed transition (M2.4 / #8).
|
||||
// gate consulted on every routed transition.
|
||||
_alarmSourceFilterPredicate[request.SourceReference] = AlarmConditionFilter.Parse(request.ConditionFilter);
|
||||
|
||||
// If the adapter feed for this source is already (being) established, the
|
||||
@@ -1796,11 +1796,11 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
{
|
||||
_alarmSubscribesInFlight.Remove(msg.SourceReference);
|
||||
|
||||
// DataConnectionLayer-023: the last (or only) subscriber may have been
|
||||
// The last (or only) subscriber may have been
|
||||
// unsubscribed while this alarm subscribe was in flight. HandleUnsubscribeAlarms
|
||||
// emptied/removed _alarmSourceSubscribers for the source but could not tear down
|
||||
// the adapter feed because the subscription id was not stored yet. Mirror the
|
||||
// DCL-021 tag-path guard: if no subscriber remains, release the just-created
|
||||
// tag-path guard: if no subscriber remains, release the just-created
|
||||
// adapter feed instead of storing an orphaned subscription id that would stream
|
||||
// transitions to nobody for the lifetime of the adapter.
|
||||
if (!_alarmSourceSubscribers.ContainsKey(msg.SourceReference))
|
||||
@@ -1822,8 +1822,8 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
|
||||
if (msg.Success && msg.SubscriptionId != null)
|
||||
{
|
||||
// DataConnectionLayer-029: a concurrent unsubscribe clears the in-flight
|
||||
// marker (DCL-023), so a fresh subscribe for the same source can issue a
|
||||
// A concurrent unsubscribe clears the in-flight
|
||||
// marker, so a fresh subscribe for the same source can issue a
|
||||
// SECOND adapter feed before this completion fires — yielding two completions
|
||||
// for one source. Mirror the tag-path re-check (see HandleTagSubscribeCompleted,
|
||||
// the `_subscriptionIds.ContainsKey` guard): if a feed is already stored, THIS
|
||||
@@ -1860,7 +1860,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
|
||||
private void HandleAlarmTransitionReceived(AlarmTransitionReceived msg)
|
||||
{
|
||||
// DataConnectionLayer-011: drop transitions from a disposed adapter after failover.
|
||||
// Drop transitions from a disposed adapter after failover.
|
||||
if (msg.AdapterGeneration != _adapterGeneration)
|
||||
return;
|
||||
|
||||
@@ -1895,7 +1895,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
if (!match)
|
||||
continue;
|
||||
|
||||
// M2.4 (#8): authoritative client-side condition-type gate. Applied
|
||||
// Authoritative client-side condition-type gate. Applied
|
||||
// per matched source because two sources may share a prefix yet carry
|
||||
// different filters. Empty filter = allow all (historical behaviour);
|
||||
// framing sentinels (SnapshotComplete) are never dropped.
|
||||
@@ -1924,7 +1924,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
_alarmSourceSubscribers.Remove(request.SourceReference);
|
||||
_alarmSourceFilter.Remove(request.SourceReference);
|
||||
_alarmSourceFilterPredicate.Remove(request.SourceReference);
|
||||
// DataConnectionLayer-023: clear the in-flight marker so that if an adapter
|
||||
// Clear the in-flight marker so that if an adapter
|
||||
// subscribe is still in flight for this source, the late AlarmSubscribeCompleted
|
||||
// is recognized as orphaned (its guard checks _alarmSourceSubscribers, now empty)
|
||||
// and the just-created feed is released rather than stored and leaked.
|
||||
@@ -1990,7 +1990,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
||||
IReadOnlyList<SeededValue> SeedValues);
|
||||
|
||||
/// <summary>An initial-read value captured during subscribe, delivered after the
|
||||
/// instance's tags are registered for fan-out (DataConnectionLayer-026).</summary>
|
||||
/// instance's tags are registered for fan-out.</summary>
|
||||
internal record SeededValue(string TagPath, TagValue Value);
|
||||
internal record AlarmTransitionReceived(NativeAlarmTransition Transition, int AdapterGeneration);
|
||||
internal record AlarmSubscribeCompleted(
|
||||
|
||||
@@ -13,7 +13,7 @@ using ZB.MOM.WW.ScadaBridge.SiteEventLogging;
|
||||
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Actors;
|
||||
|
||||
/// <summary>
|
||||
/// WP-34: Protocol extensibility — manages DataConnectionActor instances.
|
||||
/// Protocol extensibility — manages DataConnectionActor instances.
|
||||
/// Routes messages to the correct connection actor based on connection name.
|
||||
/// Adding a new protocol = implement IDataConnection + register with IDataConnectionFactory.
|
||||
/// </summary>
|
||||
@@ -24,7 +24,7 @@ public class DataConnectionManagerActor : ReceiveActor
|
||||
private readonly DataConnectionOptions _options;
|
||||
private readonly ISiteHealthCollector _healthCollector;
|
||||
private readonly ISiteEventLogger? _siteEventLogger;
|
||||
// T17: deployment-wide OPC UA application identity / cert-store paths — the same
|
||||
// Deployment-wide OPC UA application identity / cert-store paths — the same
|
||||
// global options the DataConnectionFactory feeds to RealOpcUaClient when creating OPC
|
||||
// UA connections. Needed by the verify-endpoint probe (VerifyEndpointCommand), which
|
||||
// builds an ApplicationConfiguration directly rather than through a connection actor.
|
||||
@@ -79,7 +79,7 @@ public class DataConnectionManagerActor : ReceiveActor
|
||||
return;
|
||||
}
|
||||
|
||||
// WP-34: Factory creates the correct adapter based on protocol type
|
||||
// Factory creates the correct adapter based on protocol type
|
||||
var adapter = _factory.Create(command.ProtocolType, command.PrimaryConnectionDetails);
|
||||
|
||||
var props = Props.Create(() => new DataConnectionActor(
|
||||
@@ -261,7 +261,7 @@ public class DataConnectionManagerActor : ReceiveActor
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// T17: Handles a <see cref="VerifyEndpointCommand"/> from the Central UI's "Verify"
|
||||
/// Handles a <see cref="VerifyEndpointCommand"/> from the Central UI's "Verify"
|
||||
/// action — probes the endpoint config WITHOUT persisting it (connect → capture an
|
||||
/// untrusted cert → disconnect) and pipes a structured <see cref="VerifyEndpointResult"/>
|
||||
/// back to the sender. Verify does NOT require an existing connection (the config may be
|
||||
|
||||
Reference in New Issue
Block a user