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
@@ -55,13 +55,17 @@ public sealed class CapturingAddressSpaceBuilder : IAddressSpaceBuilder
/// <summary>A variable handle whose alarm marking is a no-op (discovered alarms are out of scope).</summary>
private sealed class NullHandle(string fullRef) : IVariableHandle
{
/// <inheritdoc />
public string FullReference => fullRef;
/// <inheritdoc />
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
}
/// <summary>A null sink that ignores alarm condition transitions.</summary>
private sealed class NullSink : IAlarmConditionSink
{
/// <inheritdoc />
public void OnTransition(AlarmEventArgs args) { }
}
}
@@ -53,6 +53,7 @@ public static class DeploymentArtifact
/// Empty / malformed blobs return an empty list — callers log + treat as "no drivers".
/// </summary>
/// <param name="blob">The deployment artifact blob to parse.</param>
/// <returns>The parsed driver-instance specs, or an empty list for empty/malformed blobs.</returns>
public static IReadOnlyList<DriverInstanceSpec> ParseDriverInstances(ReadOnlySpan<byte> blob)
{
if (blob.IsEmpty) return Array.Empty<DriverInstanceSpec>();
@@ -91,7 +92,7 @@ public static class DeploymentArtifact
/// other parsers).
/// </summary>
/// <param name="blob">The deployment artifact blob to inspect.</param>
/// <param name="nodeId">The node's identity (e.g. "central-1:4053") to match against <c>Nodes</c>.</param>
/// <param name="nodeId">The node's identity (e.g. a "host:port" string) to match against <c>Nodes</c>.</param>
/// <returns>The resolved <see cref="ClusterScope"/> decision for this node.</returns>
public static ClusterScope ResolveClusterScope(ReadOnlySpan<byte> blob, string nodeId)
{
@@ -133,7 +134,7 @@ public static class DeploymentArtifact
/// cluster's specs (or none when the node's row is absent).
/// </summary>
/// <param name="blob">The deployment artifact blob to parse.</param>
/// <param name="nodeId">The node's identity (e.g. "central-1:4053") used to resolve cluster scope.</param>
/// <param name="nodeId">The node's identity (e.g. a "host:port" string) used to resolve cluster scope.</param>
/// <returns>The driver-instance specs this node should spawn.</returns>
public static IReadOnlyList<DriverInstanceSpec> ParseDriverInstances(ReadOnlySpan<byte> blob, string nodeId)
{
@@ -149,7 +150,7 @@ public static class DeploymentArtifact
/// NOT a JSON string (number / bool / object / array) yields null rather than throwing
/// <see cref="InvalidOperationException"/> from <c>GetString()</c>. Keeps the artifact-decode lenient
/// — a wrong-typed field degrades to the field default / a skipped row, matching the documented
/// "malformed blobs return an empty list/composition" contract (Runtime-002).</summary>
/// "malformed blobs return an empty list/composition" contract.</summary>
private static string? ReadString(JsonElement el, string property) =>
el.TryGetProperty(property, out var p) && p.ValueKind == JsonValueKind.String ? p.GetString() : null;
@@ -190,6 +191,7 @@ public static class DeploymentArtifact
/// nodes.
/// </summary>
/// <param name="blob">The deployment artifact blob to parse.</param>
/// <returns>The projected <see cref="AddressSpaceComposition"/>, or an empty composition for empty/malformed blobs.</returns>
public static AddressSpaceComposition ParseComposition(ReadOnlySpan<byte> blob)
{
if (blob.IsEmpty) return Empty();
@@ -231,7 +233,7 @@ public static class DeploymentArtifact
/// area is in-cluster.
/// When <paramref name="onInconsistency"/> is supplied it is invoked with a human-readable message for each
/// kept driver-bound equipment whose owning UNS line is NOT in the node's cluster — a cross-cluster binding
/// that violates the same-cluster invariant (decision #122) and would orphan the equipment folder. This is
/// that violates the same-cluster invariant and would orphan the equipment folder. This is
/// detection only (observability); the equipment is still returned, since the upstream draft validator
/// is the authority that should prevent the binding in the first place.</summary>
/// <param name="blob">The deployment artifact blob.</param>
@@ -835,7 +837,7 @@ public static class DeploymentArtifact
private static EquipmentNode? ReadEquipmentNode(JsonElement el, IReadOnlyDictionary<string, string?> deviceHostById)
{
var id = ReadString(el, "EquipmentId");
// DisplayName = the UNS level-5 Name segment (friendly browse name, matching UnsArea/UnsLine
// DisplayName = the UNS Name segment (friendly browse name, matching UnsArea/UnsLine
// + the live rebuild's source of truth) — NOT the colloquial MachineCode. NodeId stays the
// logical EquipmentId.
var displayName = ReadString(el, "Name");
@@ -201,7 +201,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
private IActorRef _mediator = null!;
/// <summary>
/// Routes an inbound operator write (resolved from the OPC UA node-manager side, Task 11) to the
/// Routes an inbound operator write (resolved from the OPC UA node-manager side) to the
/// owning driver child. <see cref="HandleRouteNodeWrite"/> gates on the local node being the
/// driver Primary, resolves the <see cref="_driverRefByNodeId"/> reverse map, and Asks the child a
/// <see cref="DriverInstanceActor.WriteAttribute"/> carrying the driver-side <see cref="FullName"/>.
@@ -234,11 +234,13 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
private sealed record ChildEntry(IActorRef Actor, DriverInstanceSpec Spec, bool Stubbed)
{
// Convenience accessors for sites that don't need the full spec.
/// <summary>Gets the driver type name from the child's spec.</summary>
public string DriverType => Spec.DriverType;
/// <summary>Gets the last applied driver configuration JSON from the child's spec.</summary>
public string LastConfigJson => Spec.DriverConfig;
}
/// <inheritdoc />
/// <summary>Gets or sets the timer scheduler for this actor's config-db retry and rediscovery timers.</summary>
public ITimerScheduler Timers { get; set; } = null!;
public sealed class RetryConfigDbConnection
@@ -279,6 +281,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
/// ScriptedAlarm host instead of spawning a real <see cref="ScriptedAlarmHostActor"/> child, so
/// tests can intercept the <see cref="ScriptedAlarmHostActor.ApplyScriptedAlarms"/> message. Null
/// in production (the real host is spawned).</param>
/// <returns>The Akka.NET <see cref="Akka.Actor.Props"/> used to spawn this actor.</returns>
public static Props Props(
IDbContextFactory<OtOpcUaConfigDbContext> dbFactory,
CommonsNodeId localNode,
@@ -676,7 +679,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
/// <summary>
/// Partitions a multi-device driver's captured FixedTree by its (normalized) device-host folder segment
/// (<c>FolderPathSegments[1]</c>) and maps each device's subset under the candidate equipment whose
/// <see cref="EquipmentNode.DeviceHost"/> matches — the follow-up E part-2 multi-device graft. Returns
/// <see cref="EquipmentNode.DeviceHost"/> matches — the multi-device graft. Returns
/// the per-equipment plan map (one entry per device that matched AND had at least one new variable);
/// EMPTY when nothing is graftable.
/// <list type="bullet">
@@ -696,7 +699,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
/// regardless of case/whitespace.
/// <para><b>Warn-spam taming.</b> The unmatched/ambiguous/degenerate condition is warned ONCE then
/// Debug-logged on the repeated re-discovery passes (see <see cref="ShouldWarnPartition"/>).</para>
/// <para><b>Mid-connect partition shrink (M2).</b> If a later pass yields FEWER device partitions than a
/// <para><b>Mid-connect partition shrink.</b> If a later pass yields FEWER device partitions than a
/// prior pass within the same connect, the dropped partition's routes + materialised nodes are NOT
/// actively pruned until the next full redeploy (<see cref="PushDesiredSubscriptions"/> Clears + rebuilds
/// the maps). This matches the existing "FixedTree grows-then-stabilises within a connect" assumption —
@@ -995,7 +998,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
};
/// <summary>
/// Routes an inbound operator write (Task 11 Asks this from the OPC UA node-manager side) to the
/// Routes an inbound operator write (Asked from the OPC UA node-manager side) to the
/// owning driver child. Order matters:
/// <list type="number">
/// <item>PRIMARY gate FIRST — reuses the same <see cref="_localRole"/> redundancy signal the
@@ -1137,8 +1140,8 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
_log.Debug("DriverHost {Node}: dropping native-alarm ack for {Node2} while Stale (config DB unreachable)",
_localNode, msg.ConditionNodeId));
// A driver child's post-connect DiscoveredNodesReady can't be injected while Stale (no composition is
// applied yet, so the equipment can't be resolved). Drop it — Task 6's re-discovery loop re-sends it
// and the Task-8 post-recovery re-apply self-heal it once an apply runs (matches the no-op drops above).
// applied yet, so the equipment can't be resolved). Drop it — the re-discovery loop re-sends it
// and the post-recovery re-apply self-heals it once an apply runs (matches the no-op drops above).
Receive<DriverInstanceActor.DiscoveredNodesReady>(_ => { });
Receive<SubscribeAck>(_ => { /* PubSub ack */ });
Timers.StartPeriodicTimer("retry-db", RetryConfigDbConnection.Instance, ReconnectInterval);
@@ -1481,8 +1484,8 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
// redeploy / re-apply (one that runs while the host is alive, so _discoveredByDriver is populated)
// would drop the injected FixedTree routes + materialised nodes until the driver happens to reconnect
// and re-discover. This loop is INERT on the bootstrap-restore path (RestoreApplied): there the actor
// is freshly constructed so _discoveredByDriver is empty — restart survival comes from Task 6's
// post-connect re-discovery, NOT this re-apply. Re-resolve each cached driver's candidate equipments
// is freshly constructed so _discoveredByDriver is empty — restart survival comes from the
// post-connect re-discovery loop, NOT this re-apply. Re-resolve each cached driver's candidate equipments
// from the CURRENT composition (the SAME EquipmentNodes-UNION-EquipmentTags logic HandleDiscoveredNodes
// uses), then validate each cached (equipmentId → plan) entry PER ENTRY: drop the entry if its
// equipmentId is no longer a resolved candidate for the driver, OR the plan's NodeIds aren't scoped to
@@ -69,7 +69,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
/// Sets the set of references this driver should keep subscribed for the lifetime of the
/// current deployment. Unlike the one-shot <see cref="Subscribe"/>, the desired set is
/// retained and (re)established automatically every time the actor (re)enters
/// <c>Connected</c> — closing the F8b/#113 "re-subscribe across reconnects" gap and giving
/// <c>Connected</c> — closing the F8b "re-subscribe across reconnects" gap and giving
/// <see cref="DriverHostActor"/> a single message to drive the SubscribeBulk pass after an
/// apply. Sending an empty set clears the desired subscription.
/// </summary>
@@ -212,6 +212,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
/// <param name="rediscoverInterval">Optional interval between post-connect re-discovery passes; defaults to 2 seconds.</param>
/// <param name="rediscoverMaxAttempts">Optional cap on re-discovery passes; defaults to 15.</param>
/// <param name="rediscoverDiscoverTimeout">Optional per-pass timeout for <see cref="ITagDiscovery.DiscoverAsync"/>; defaults to 30 seconds.</param>
/// <returns>A <see cref="Akka.Actor.Props"/> instance configured to create the wrapped <see cref="DriverInstanceActor"/>.</returns>
public static Props Props(
IDriver driver,
TimeSpan? reconnectInterval = null,
@@ -241,6 +242,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
/// </summary>
/// <param name="driverType">The type identifier of the driver.</param>
/// <param name="roles">Operational roles configured for this instance.</param>
/// <returns>True if the driver should start in DEV-STUB mode; otherwise false.</returns>
public static bool ShouldStub(string driverType, IEnumerable<string> roles)
{
var isWindowsOnly = driverType is "Galaxy";
@@ -328,7 +330,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
Receive<InitializeRequested>(msg => InitializeAsync(msg.DriverConfigJson));
// Fast-fail writes while still connecting — without this the inbound WriteAttribute dead-letters
// and DriverHostActor.HandleRouteNodeWrite waits its full 8s Ask before reporting a generic
// "write timeout". Synchronous Receive: Sender.Tell on the actor thread is safe (#4a-instance).
// "write timeout". Synchronous Receive: Sender.Tell on the actor thread is safe.
Receive<WriteAttribute>(_ =>
Sender.Tell(new WriteAttributeResult(false, "driver not connected")));
// An ack arriving while still connecting can't reach the upstream alarm system; drop it (the ack is
@@ -456,7 +458,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
{
Receive<RetryConnect>(_ => InitializeAsync(_currentConfigJson ?? "{}"));
// Fast-fail writes while reconnecting (same reason as Connecting — avoids the 8s host Ask
// timeout on an inbound write to a transiently-down driver). Synchronous Receive (#4a-instance).
// timeout on an inbound write to a transiently-down driver). Synchronous Receive.
Receive<WriteAttribute>(_ =>
Sender.Tell(new WriteAttributeResult(false, "driver not connected")));
// An ack arriving while reconnecting can't reach the upstream alarm system; drop it (fire-and-forget,
@@ -565,8 +567,8 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
}
var request = new[] { new WriteRequest(msg.TagId, msg.Value) };
// Bound the write so a hung backend can't pin this actor forever — decision #44/#45 keeps
// retry off by default, but a stalled call still needs an answer.
// Bound the write so a hung backend can't pin this actor forever — retry stays
// off by default, but a stalled call still needs an answer.
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
@@ -23,6 +23,7 @@ public static class DriverSpawnPlanner
/// </summary>
/// <param name="current">The currently running driver children keyed by ID.</param>
/// <param name="target">The target driver instances from the deployment artifact.</param>
/// <returns>The computed <see cref="DriverSpawnPlan"/> with spawn, apply-delta, and stop sets.</returns>
public static DriverSpawnPlan Compute(
IReadOnlyDictionary<string, DriverChildSnapshot> current,
IReadOnlyList<DriverInstanceSpec> target)
@@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Health;
/// <summary>
/// Single-flight cached health probe against the ConfigDb. Reads cached state via
/// <c>Ask&lt;DbHealthStatus&gt;</c>; a single SELECT 1 runs at most every <c>RefreshInterval</c>.
/// Consumed by both the host's <c>/health/ready</c> endpoint (Task 54) and
/// Consumed by both the host's <c>/health/ready</c> endpoint and
/// <c>RedundancyStateActor</c>'s stale calc.
/// </summary>
public sealed class DbHealthProbeActor : ReceiveActor, IWithTimers
@@ -28,6 +28,7 @@ public sealed class DbHealthProbeActor : ReceiveActor, IWithTimers
/// <summary>Creates a Props instance for the DbHealthProbeActor.</summary>
/// <param name="dbFactory">The factory for creating ConfigDb contexts.</param>
/// <returns>The Akka <see cref="Akka.Actor.Props"/> for creating this actor.</returns>
public static Props Props(IDbContextFactory<OtOpcUaConfigDbContext> dbFactory) =>
Akka.Actor.Props.Create(() => new DbHealthProbeActor(dbFactory));
@@ -128,10 +128,7 @@ public sealed class HistorianAdapterActor : ReceiveActor
_ => AlarmSeverity.Critical,
};
/// <summary>Subscribes to the <c>redundancy-state</c> topic (so cluster role changes land as
/// <see cref="RedundancyStateChanged"/> and cache this node's role — the historize enqueue is gated to
/// the Primary so the alerts feed doesn't double-write across the warm-redundant pair) and to the
/// <c>alerts</c> topic (so live <see cref="AlarmTransitionEvent"/>s are translated + historized).</summary>
/// <inheritdoc />
protected override void PreStart()
{
_mediator = DistributedPubSub.Get(Context.System).Mediator;
@@ -17,7 +17,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.OpcUa;
/// <summary>
/// Single-threaded bridge between Akka messages and the OPC UA SDK address space. Hosted on
/// the pinned <c>opcua-synchronized-dispatcher</c> (Task 19 HOCON) so the OPC UA SDK sees
/// the pinned <c>opcua-synchronized-dispatcher</c> (HOCON) so the OPC UA SDK sees
/// only one thread per actor instance — its session/subscription locks expect strict
/// single-threaded access.
///
@@ -121,6 +121,8 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
/// <param name="healthTickInterval">The period between self-driven DB-health refresh ticks (each
/// Asks <paramref name="dbHealthProbe"/> for its cached status); defaults to 5 seconds. No timer is
/// started when <paramref name="dbHealthProbe"/> is null.</param>
/// <returns>The configured <see cref="Props"/> for creating this actor, pinned to the
/// <see cref="DispatcherId"/> dispatcher.</returns>
public static Props Props(
IOpcUaAddressSpaceSink? sink = null,
IServiceLevelPublisher? serviceLevel = null,
@@ -161,6 +163,8 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
/// <param name="healthTickInterval">The period between self-driven DB-health refresh ticks (each
/// Asks <paramref name="dbHealthProbe"/> for its cached status); defaults to 5 seconds. No timer is
/// started when <paramref name="dbHealthProbe"/> is null.</param>
/// <returns>The configured <see cref="Props"/> for creating this actor in tests, without dispatcher
/// pinning or the DPS subscribe.</returns>
public static Props PropsForTests(
IOpcUaAddressSpaceSink? sink = null,
IServiceLevelPublisher? serviceLevel = null,
@@ -331,9 +335,6 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
var outcome = _applier.Apply(plan);
_lastApplied = composition;
// #85 — after the plan diff lands, rebuild the UNS folder hierarchy so OPC UA
// clients see Area/Line/Equipment as proper folders. Idempotent; AddressSpaceApplier
// skips folders that already exist with the same node id.
_applier.MaterialiseHierarchy(composition);
// T14 — scripted alarms get their own pass right after the hierarchy so the equipment
// folders they parent under already exist. Materialises real Part 9 AlarmConditionState
@@ -95,13 +95,11 @@ public sealed class DependencyMuxTagUpstreamSource : ITagUpstreamSource
return new DataValueSnapshot(Value: null, StatusCode: StatusBad, SourceTimestampUtc: null, ServerTimestampUtc: now);
}
// No-replay contract: a new subscriber does NOT receive a synthetic initial
// notification for the current cached value. Callers that need the current value must
// call ReadTag immediately after subscribing. The engine's cold-start path
// (startup-recovery + read-cache-refill) already does this.
/// <inheritdoc/>
/// <remarks>
/// <b>No-replay contract</b>: a new subscriber does NOT receive a synthetic initial
/// notification for the current cached value. Callers that need the current value must
/// call <see cref="ReadTag"/> immediately after subscribing. The engine's cold-start path
/// (startup-recovery + read-cache-refill) already does this.
/// </remarks>
public IDisposable SubscribeTag(string path, Action<string, DataValueSnapshot> observer)
{
ArgumentNullException.ThrowIfNull(path);
@@ -152,6 +150,10 @@ public sealed class DependencyMuxTagUpstreamSource : ITagUpstreamSource
private readonly string _path;
private readonly Action<string, DataValueSnapshot> _observer;
/// <summary>Creates a handle that deregisters <paramref name="observer"/> from <paramref name="owner"/> for <paramref name="path"/> on dispose.</summary>
/// <param name="owner">The upstream source to deregister from.</param>
/// <param name="path">The tag path the observer is subscribed to.</param>
/// <param name="observer">The observer callback to deregister.</param>
public Subscription(DependencyMuxTagUpstreamSource owner, string path, Action<string, DataValueSnapshot> observer)
{
_owner = owner;
@@ -159,6 +161,7 @@ public sealed class DependencyMuxTagUpstreamSource : ITagUpstreamSource
_observer = observer;
}
/// <summary>Deregisters the observer from the owning source. Idempotent — safe to call more than once.</summary>
public void Dispose()
{
// Swap _owner to null first so a double-dispose can't deregister twice (the
@@ -18,7 +18,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.ScriptedAlarms;
/// <para>
/// <b>ActiveState is NOT persisted</b> — the entity has no Active column. On
/// <see cref="LoadAsync"/> it is restored as <see cref="AlarmActiveState.Inactive"/>;
/// the engine re-derives it from the live predicate on startup (Phase 7 decision #14).
/// the engine re-derives it from the live predicate on startup.
/// </para>
/// <para>
/// <b>LastTransitionUtc ↔ UpdatedAtUtc</b>: the table has no dedicated transition
@@ -74,14 +74,6 @@ public sealed class EfAlarmConditionStateStore : IAlarmStateStore
}
/// <inheritdoc />
/// <remarks>
/// <b>Concurrency assumption</b>: saves for a given <c>alarmId</c> are serialized by the
/// owning host actor (one actor owns the engine per equipment). The check-then-insert
/// pattern is therefore safe under that guarantee — two concurrent inserts for the same
/// alarm cannot occur in the live
/// runtime. The <see cref="DbUpdateConcurrencyException"/> catch handles the edge case of a
/// racing concurrent restart during crash recovery.
/// </remarks>
public async Task SaveAsync(AlarmConditionState state, CancellationToken ct)
{
using var db = await _dbFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
@@ -130,6 +130,7 @@ public sealed class ScriptedAlarmHostActor : ReceiveActor
/// <param name="localNode">The local cluster node id, used to read this node's <see cref="RedundancyRole"/>
/// from the <c>redundancy-state</c> topic so only the Primary publishes the cluster-wide <c>alerts</c>
/// transition. Null (the default) leaves the role unknown ⇒ default-emit (single-node deploys + tests).</param>
/// <returns>The <see cref="Akka.Actor.Props"/> used to instantiate the actor.</returns>
public static Props Props(
IActorRef publishActor,
IActorRef? mux,
@@ -37,12 +37,7 @@ public sealed class DpsScriptLogPublisher : IScriptLogPublisher
public DpsScriptLogPublisher(Func<ActorSystem> system) =>
_system = system ?? throw new ArgumentNullException(nameof(system));
/// <summary>
/// Publishes <paramref name="entry"/> onto the DPS <c>script-logs</c> topic. Any failure
/// (system not yet ready, mediator unavailable) is swallowed so the logging pipeline is
/// never disrupted by a transient cluster condition.
/// </summary>
/// <param name="entry">The entry to publish.</param>
/// <inheritdoc />
public void Publish(ScriptLogEntry entry)
{
try
@@ -43,6 +43,7 @@ public static class ServiceCollectionExtensions
/// Call this BEFORE <c>AddAkka</c>.
/// </summary>
/// <param name="services">The service collection to register with.</param>
/// <returns>The same <paramref name="services"/> instance for chaining.</returns>
public static IServiceCollection AddOtOpcUaRuntime(this IServiceCollection services)
{
services.TryAddSingleton<IAlarmHistorianSink>(NullAlarmHistorianSink.Instance);
@@ -189,6 +190,7 @@ public static class ServiceCollectionExtensions
/// </code>
/// </summary>
/// <param name="builder">The Akka configuration builder.</param>
/// <returns>The same <paramref name="builder"/> instance for chaining.</returns>
public static AkkaConfigurationBuilder WithOtOpcUaRuntimeActors(this AkkaConfigurationBuilder builder)
{
// Production cluster HOCON (akka.conf) carries this dispatcher block, but consumers that
@@ -43,6 +43,7 @@ public sealed class VirtualTagActor : ReceiveActor
/// <param name="publisherFactory">Optional factory for creating DPS publishers.</param>
/// <param name="dependencyRefs">Optional list of dependency tag references; defaults to empty.</param>
/// <param name="mux">Optional reference to a dependency multiplexer actor.</param>
/// <returns>A configured <see cref="Akka.Actor.Props"/> for creating the actor.</returns>
public static Props Props(
string virtualTagId,
string expression,
@@ -58,6 +58,7 @@ public sealed class VirtualTagHostActor : ReceiveActor
/// <param name="historyWriter">Sink for results whose plan has <c>Historize=true</c>. Null ⇒
/// <see cref="NullHistoryWriter.Instance"/> (no durable historian wired), so existing call sites
/// compile unchanged and never historize.</param>
/// <returns>The <see cref="Props"/> used to spawn a <see cref="VirtualTagHostActor"/>.</returns>
public static Props Props(IActorRef publishActor, IActorRef? mux, IVirtualTagEvaluator evaluator,
IHistoryWriter? historyWriter = null) =>
Akka.Actor.Props.Create(() => new VirtualTagHostActor(publishActor, mux, evaluator, historyWriter));