feat(opcua): write-outcome self-correction — capture prior + compare-and-revert on failure
This commit is contained in:
@@ -130,35 +130,23 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl
|
||||
}
|
||||
});
|
||||
|
||||
// Wire the reverse-path inbound operator-write router: a client write to a writable equipment-tag
|
||||
// Wire the reverse-path inbound operator-write gateway: a client write to a writable equipment-tag
|
||||
// node that passes the node manager's WriteOperate gate routes the write to the owning driver child
|
||||
// (RouteNodeWrite → NodeWriteResult) via the local DriverHostActor. This dispatch is FIRE-AND-FORGET
|
||||
// (just like the alarm router): the SDK's CustomNodeManager2.Write holds the node-manager Lock while
|
||||
// invoking OnWriteValue, so a blocking Ask here would freeze ALL address-space operations (reads,
|
||||
// subscription notifications, the publish path) for up to the Ask timeout. We kick off the Ask and
|
||||
// log failures from a continuation; the write reaches the device asynchronously and the value
|
||||
// self-corrects on the next driver poll (standard OPC UA optimistic-write semantics). The
|
||||
// DriverHostActor ref is resolved LAZILY per write — this hosted service's StartAsync runs before
|
||||
// the Akka DriverHostActor registers, so a one-shot resolve here would always miss and leave every
|
||||
// write unavailable. By write time (long after startup) the registry has it; a node that genuinely
|
||||
// has no driver-host (admin-only, no writable driver nodes materialised) logs + drops the write.
|
||||
_server.SetNodeWriteRouter((nodeId, value) =>
|
||||
{
|
||||
if (!_actorRegistry.TryGet<DriverHostActorKey>(out var driverHost))
|
||||
{
|
||||
_logger.LogWarning("Inbound write to {NodeId} dropped: no DriverHostActor registered", nodeId);
|
||||
return;
|
||||
}
|
||||
driverHost.Ask<DriverHostActor.NodeWriteResult>(
|
||||
new DriverHostActor.RouteNodeWrite(nodeId, value), TimeSpan.FromSeconds(10))
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
if (!t.IsCompletedSuccessfully)
|
||||
_logger.LogWarning("Operator write to {NodeId} failed or timed out", nodeId);
|
||||
else if (!t.Result.Success)
|
||||
_logger.LogWarning("Operator write to {NodeId} rejected: {Reason}", nodeId, t.Result.Reason);
|
||||
}, TaskScheduler.Default);
|
||||
});
|
||||
// (RouteNodeWrite → NodeWriteResult) via the local DriverHostActor. The node manager calls the
|
||||
// gateway's WriteAsync FIRE-AND-FORGET: the SDK's CustomNodeManager2.Write holds the node-manager
|
||||
// Lock while invoking OnWriteValue, so a blocking Ask here would freeze ALL address-space operations
|
||||
// (reads, subscription notifications, the publish path) for up to the Ask timeout. The gateway kicks
|
||||
// off the Ask and resolves a NodeWriteOutcome; the node manager applies the client value optimistically
|
||||
// and self-corrects (reverts to the pre-write value) when the device write comes back FAILED — but only
|
||||
// while the node still holds the optimistic value, so a fresh driver poll is not clobbered. The
|
||||
// DriverHostActor ref is resolved LAZILY per write (inside the gateway) — this hosted service's
|
||||
// StartAsync runs before the Akka DriverHostActor registers, so a one-shot resolve here would always
|
||||
// miss and leave every write unavailable. By write time (long after startup) the registry has it; a
|
||||
// node that genuinely has no driver-host (admin-only, no writable driver nodes materialised) logs +
|
||||
// resolves the write to "writes unavailable".
|
||||
_server.SetNodeWriteGateway(new ActorNodeWriteGateway(
|
||||
resolveDriverHost: () => _actorRegistry.TryGet<DriverHostActorKey>(out var driverHost) ? driverHost : null,
|
||||
logger: _loggerFactory.CreateLogger<ActorNodeWriteGateway>()));
|
||||
|
||||
// ServiceLevel publisher needs IServerInternal — only available after Start.
|
||||
if (_server.CurrentInstance is { } serverInternal)
|
||||
@@ -181,8 +169,8 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl
|
||||
// half-disposed NodeManager.
|
||||
_deferredSink.SetSink(null);
|
||||
_deferredServiceLevel.SetInner(null);
|
||||
// Drop the inbound-write router too so a late client write doesn't Ask a stopping DriverHostActor.
|
||||
_server?.SetNodeWriteRouter(null);
|
||||
// Restore the Null write gateway so a late client write doesn't Ask a stopping DriverHostActor.
|
||||
_server?.SetNodeWriteGateway(null);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user