docs(xmldoc): fill missing XML docs + strip tracking-ID comments across src
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped

Adds <summary>/<param>/<returns>/<inheritdoc> where missing and removes
project bookkeeping IDs (task/tracking refs) from shipped code comments,
so the docs read cleanly and CommentChecker is quiet except for known
false positives (PLC/protocol terms, event/IEqualityComparer inheritdoc).
Doc/comment-only; no logic changed; solution builds clean.
This commit is contained in:
Joseph Doherty
2026-07-07 12:38:39 -04:00
parent 384dbd7d36
commit 9cad9ed0fc
375 changed files with 1899 additions and 2493 deletions
@@ -87,7 +87,7 @@ public sealed class GalaxyDriver
private bool _alarmFeedWired;
// List preserves insertion order so OnAlarmFeedTransition always picks the
// earliest-registered handle — a deterministic choice that doesn't vary as
// handles are added/removed (Driver.Galaxy-006 fix: HashSet.First() is unstable).
// handles are added/removed (HashSet.First() is unstable).
private readonly List<GalaxyAlarmSubscriptionHandle> _alarmSubscriptions = new();
// Production runtime owned by InitializeAsync (BuildProductionRuntimeAsync). The
@@ -200,9 +200,10 @@ public sealed class GalaxyDriver
/// <see cref="ReconnectSupervisor.ReportTransportFailure"/> drives this on a
/// background task in production; tests prefer to invoke it directly so the
/// <see cref="GalaxyReconnectOptions.ReplayOnSessionLost"/> branch can be
/// asserted deterministically (Driver.Galaxy-013).
/// asserted deterministically.
/// </summary>
/// <param name="cancellationToken">Cancellation token for the replay operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
internal Task InvokeReplayForTestAsync(CancellationToken cancellationToken) =>
ReplayAsync(cancellationToken);
@@ -310,7 +311,7 @@ public sealed class GalaxyDriver
/// handles are dead once the session reopened. The faulted <see cref="EventPump"/>
/// is recreated first so the replayed subscriptions have a live StreamEvents
/// consumer; without that restart the replayed tags are subscribed on the gw but
/// never reach <c>OnDataChange</c> (Driver.Galaxy-008). PR 6.x can swap this for
/// never reach <c>OnDataChange</c>. PR 6.x can swap this for
/// the gw's batched <c>ReplaySubscriptionsCommand</c> once it ships.
/// </summary>
private async Task ReplayAsync(CancellationToken cancellationToken)
@@ -319,11 +320,11 @@ public sealed class GalaxyDriver
var entries = _subscriptions.SnapshotEntries();
if (entries.Count == 0) return;
// Driver.Galaxy-013: honor ReplayOnSessionLost. When operators opt out (false)
// we skip the per-tag SubscribeBulk fan-out — they're delegating to the
// gateway's session-level ReplaySubscriptions or accept post-reconnect tag
// loss. We still restart the EventPump so a future Subscribe call lands on
// a live consumer.
// Honor ReplayOnSessionLost. When operators opt out (false) we skip the
// per-tag SubscribeBulk fan-out — they're delegating to the gateway's
// session-level ReplaySubscriptions or accept post-reconnect tag loss. We
// still restart the EventPump so a future Subscribe call lands on a live
// consumer.
if (!_options.Reconnect.ReplayOnSessionLost)
{
RestartEventPumpForReplay();
@@ -440,10 +441,10 @@ public sealed class GalaxyDriver
private MxGatewayClientOptions BuildClientOptions(GalaxyGatewayOptions gw) => new()
{
Endpoint = new Uri(gw.Endpoint, UriKind.Absolute),
// Driver.Galaxy-010: pass the logger so the literal-arm cleartext fallback
// surfaces a startup warning rather than silently shipping the key. The
// resolver lives in Driver.Galaxy.Contracts (GalaxySecretRef) so the runtime
// driver and the AdminUI browser share one implementation.
// Pass the logger so the literal-arm cleartext fallback surfaces a startup
// warning rather than silently shipping the key. The resolver lives in
// Driver.Galaxy.Contracts (GalaxySecretRef) so the runtime driver and the
// AdminUI browser share one implementation.
ApiKey = GalaxySecretRef.ResolveApiKey(gw.ApiKeySecretRef, _logger),
UseTls = gw.UseTls,
CaCertificatePath = gw.CaCertificatePath,
@@ -459,8 +460,8 @@ public sealed class GalaxyDriver
// Reuse the lazily-built repository client (DiscoverAsync constructs it on demand).
// If discovery hasn't run yet, build the client here so the watcher has a target.
// Driver.Galaxy-009 fix: guard with ??= so if BuildDefaultHierarchySource later runs
// it reuses this client rather than overwriting the field and leaking the first instance.
// Guard with ??= so if BuildDefaultHierarchySource later runs it reuses this client
// rather than overwriting the field and leaking the first instance.
_ownedRepositoryClient ??= ZB.MOM.WW.MxGateway.Client.GalaxyRepositoryClient.Create(
BuildClientOptions(_options.Gateway));
@@ -470,31 +471,26 @@ public sealed class GalaxyDriver
// StartAsync schedules the background loop and returns Task.CompletedTask immediately.
// It throws InvalidOperationException synchronously if called twice (programming error).
// Driver.Galaxy-009 fix: don't discard the return value — observe any synchronous throw.
// Don't discard the return value — observe any synchronous throw.
var startTask = _deployWatcher.StartAsync(CancellationToken.None);
// The task is already completed (StartAsync is synchronous); surface any synchronous fault.
if (startTask.IsFaulted) startTask.GetAwaiter().GetResult();
}
/// <inheritdoc />
/// <remarks>
/// <para>
/// In-place config reapply. The driver does not currently support
/// hot-swapping <see cref="GalaxyDriverOptions"/> at runtime — changing the
/// gateway endpoint, MxAccess client name, or reconnect policy requires
/// tearing down the gw session, supervisor, event pump, and address space.
/// The host stack handles that via DriverInstance restart, so this method
/// only accepts an equivalent config (no meaningful change) and refreshes
/// health; a non-equivalent reapply throws <see cref="NotSupportedException"/>
/// so the caller knows the change wasn't applied (Driver.Galaxy-013:
/// previously the method silently ignored <c>driverConfigJson</c>).
/// </para>
/// </remarks>
public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (!string.IsNullOrWhiteSpace(driverConfigJson))
{
// In-place config reapply. The driver does not currently support hot-swapping
// GalaxyDriverOptions at runtime — changing the gateway endpoint, MxAccess
// client name, or reconnect policy requires tearing down the gw session,
// supervisor, event pump, and address space. The host stack handles that via
// DriverInstance restart, so this method only accepts an equivalent config (no
// meaningful change) and refreshes health; a non-equivalent reapply throws
// NotSupportedException so the caller knows the change wasn't applied.
//
// Materialise the incoming config and compare against the live options. We
// refuse any change that would require a session teardown rather than
// pretending to apply it.
@@ -562,17 +558,13 @@ public sealed class GalaxyDriver
public IReadOnlyList<HostConnectivityStatus> GetHostStatuses() => _hostStatuses.Snapshot();
/// <inheritdoc />
/// <remarks>
/// Estimated footprint: 64 bytes × tracked item handles (one gw subscription entry
/// per bound tag) + 256 bytes × tracked driver subscriptions (registry overhead per
/// OPC UA monitored item). Returns 0 when no subscriptions are active. These
/// constants are conservative — a 50k-tag set occupies ~3 MB and registers clearly
/// with the server's cache-flush heuristic. Driver.Galaxy-011: the stale
/// "PR 4.4 sets this" comment is removed; PR 4.4 shipped the SubscriptionRegistry
/// but never wired it here.
/// </remarks>
public long GetMemoryFootprint()
{
// Estimated footprint: 64 bytes × tracked item handles (one gw subscription entry
// per bound tag) + 256 bytes × tracked driver subscriptions (registry overhead per
// OPC UA monitored item). Returns 0 when no subscriptions are active. These
// constants are conservative — a 50k-tag set occupies ~3 MB and registers clearly
// with the server's cache-flush heuristic.
const long BytesPerItemHandle = 64L; // TagBinding + reverse-map entry
const long BytesPerSubscription = 256L; // SubscriptionEntry overhead
return (_subscriptions.TrackedItemHandleCount * BytesPerItemHandle)
@@ -584,13 +576,12 @@ public sealed class GalaxyDriver
// ===== ITagDiscovery (PR 4.1) =====
/// <summary>
/// Run-once: <see cref="DiscoverAsync"/> fetches the full Galaxy hierarchy inline and
/// streams the complete node set within a single awaited call — there is no FOCAS-style
/// background cache that fills in after connect. Galaxy is a heavy network driver, so the
/// bounded post-connect retry loop is deliberately avoided; re-discovery on Galaxy
/// redeploy is handled separately via <see cref="IRediscoverable"/> + the deploy-event watcher.
/// </summary>
/// <inheritdoc />
// Run-once: DiscoverAsync fetches the full Galaxy hierarchy inline and streams the complete
// node set within a single awaited call — there is no FOCAS-style background cache that fills
// in after connect. Galaxy is a heavy network driver, so the bounded post-connect retry loop
// is deliberately avoided; re-discovery on Galaxy redeploy is handled separately via
// IRediscoverable + the deploy-event watcher.
public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once;
/// <inheritdoc />
@@ -710,10 +701,10 @@ public sealed class GalaxyDriver
}
// Register bindings so the pump knows to dispatch events for these handles.
// Driver.Galaxy-012: index the SubscribeBulk results once and correlate to
// references in O(1) instead of FirstOrDefault per element (O(n²) over the
// batch). On the 50k-tag soak path this turns a 2.5G-comparison loop into
// a single Dictionary build + linear scan.
// Index the SubscribeBulk results once and correlate to references in O(1)
// instead of FirstOrDefault per element (O(n²) over the batch). On the
// 50k-tag soak path this turns a 2.5G-comparison loop into a single
// Dictionary build + linear scan.
var resultIndex = BuildResultIndex(results);
var bindings = new List<TagBinding>(fullReferences.Count);
for (var i = 0; i < fullReferences.Count; i++)
@@ -853,8 +844,8 @@ public sealed class GalaxyDriver
// recorded with a non-positive ItemHandle so the caller can detect partial failure
// by inspecting the returned handle's diagnostic context — full per-tag error
// surface lands in PR 5.3's parity tests.
// Driver.Galaxy-012: index results once, correlate in O(1) per reference rather
// than FirstOrDefault inside the loop (O(n²) on the 50k-tag path).
// Index results once, correlate in O(1) per reference rather than
// FirstOrDefault inside the loop (O(n²) on the 50k-tag path).
var resultIndex = BuildResultIndex(results);
var bindings = new List<TagBinding>(fullReferences.Count);
for (var i = 0; i < fullReferences.Count; i++)
@@ -1084,9 +1075,9 @@ public sealed class GalaxyDriver
// by SourceNodeId (not by handle), so every active subscriber sees the same
// transition regardless of which handle is attached here. Using the first
// insertion-order entry is deterministic and stable as long as at least one
// subscription remains — HashSet.First() was unstable across mutations
// (Driver.Galaxy-006 fix). _alarmSubscriptions is a List, so [0] is always
// the earliest-registered handle.
// subscription remains — HashSet.First() was unstable across mutations.
// _alarmSubscriptions is a List, so [0] is always the earliest-registered
// handle.
subCount = _alarmSubscriptions.Count;
handle = subCount > 0 ? _alarmSubscriptions[0] : null;
}
@@ -1162,9 +1153,9 @@ public sealed class GalaxyDriver
/// </summary>
private IGalaxyHierarchySource BuildDefaultHierarchySource()
{
// Driver.Galaxy-009 fix: reuse a client that StartDeployWatcher may have already
// created (??=) rather than always overwriting the field and leaking the first
// instance. Both paths produce equivalent clients from the same options.
// Reuse a client that StartDeployWatcher may have already created (??=) rather
// than always overwriting the field and leaking the first instance. Both paths
// produce equivalent clients from the same options.
_ownedRepositoryClient ??= GalaxyRepositoryClient.Create(BuildClientOptions(_options.Gateway));
return new TracedGalaxyHierarchySource(
new GatewayGalaxyHierarchySource(_ownedRepositoryClient), _options.MxAccess.ClientName);
@@ -1173,10 +1164,11 @@ public sealed class GalaxyDriver
/// <summary>
/// Asynchronous disposal. Prefer <c>await using</c> over <c>using</c> — the
/// async path does not block the caller while awaiting EventPump / session /
/// client shutdown (Driver.Galaxy-007: the sync path blocked on
/// <c>GetAwaiter().GetResult()</c> for every async sub-component, risking a
/// deadlock under thread-pool starvation).
/// client shutdown (the sync path blocks on <c>GetAwaiter().GetResult()</c>
/// for every async sub-component, risking a deadlock under thread-pool
/// starvation).
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
public async ValueTask DisposeAsync()
{
if (_disposed) return;
@@ -1250,26 +1242,18 @@ public sealed class GalaxyDriver
System.Collections.Concurrent.ConcurrentDictionary<string, SecurityClassification> map)
: IAddressSpaceBuilder
{
/// <summary>Creates a folder node and returns a builder for populating it.</summary>
/// <param name="browseName">The OPC UA BrowseName of the folder.</param>
/// <param name="displayName">The display name for the folder.</param>
/// <inheritdoc />
public IAddressSpaceBuilder Folder(string browseName, string displayName)
=> new SecurityCapturingBuilder(inner.Folder(browseName, displayName), map);
/// <summary>Creates a variable node and captures its security classification.</summary>
/// <param name="browseName">The OPC UA BrowseName of the variable.</param>
/// <param name="displayName">The display name for the variable.</param>
/// <param name="attributeInfo">The driver attribute metadata including security classification.</param>
/// <inheritdoc />
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
{
map[attributeInfo.FullName] = attributeInfo.SecurityClass;
return inner.Variable(browseName, displayName, attributeInfo);
}
/// <summary>Adds a property node to the current parent.</summary>
/// <param name="browseName">The OPC UA BrowseName of the property.</param>
/// <param name="dataType">The OPC UA data type of the property.</param>
/// <param name="value">The property value.</param>
/// <inheritdoc />
public void AddProperty(string browseName, DriverDataType dataType, object? value)
=> inner.AddProperty(browseName, dataType, value);
}