feat(opcua): F14 Phase7Plan + Phase7Applier
v2-ci / build (push) Failing after 34s
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 (push) Has been skipped
v2-ci / build (push) Failing after 34s
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 (push) Has been skipped
Splits the side-effecting half of Phase7Composer (deferred at Task 47) into two pieces that mirror DriverHostActor's spawn-plan pattern: Phase7Plan + Phase7Planner.Compute (pure): Diff two Phase7CompositionResult snapshots by stable id (EquipmentId, DriverInstanceId, ScriptedAlarmId). Emits Added/Removed/Changed lists per entity class. Added/Removed are sorted by id for deterministic apply order. Changed wraps both Previous and Current projections so consumers can decide between in-place mutation and tear-down + rebuild. Phase7Applier (side-effecting): Drives an IOpcUaAddressSpaceSink against a plan. Removed equipment/ alarms get an inactive AlarmState write per id; Added/Removed of Equipment or ScriptedAlarm triggers RebuildAddressSpace. Driver-only changes correctly skip the rebuild — those flow through DriverHost- Actor's spawn-plan in Runtime. Sink exceptions are caught + logged so one bad node doesn't abort the apply. Tests: OpcUaServer 6 -> 20 (+14): - Phase7PlannerTests x9 (empty-in/empty-out, add/remove/change per entity class, mixed changes, deterministic ordering) - Phase7ApplierTests x5 (empty plan no-op, removal writes inactive states + rebuild, added equipment triggers rebuild, driver-only skips rebuild, sink fault is non-fatal) The remaining piece is the EquipmentNodeWalker integration against a real SDK NodeManager — split as F14b, gated on F10b's SDK builder. All 6 v2 test suites green: 146 tests passing.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
|
||||
/// <summary>
|
||||
/// Side-effecting orchestrator over <see cref="Phase7Plan"/>. Drives an
|
||||
/// <see cref="IOpcUaAddressSpaceSink"/> to materialise the diff between two
|
||||
/// <see cref="Phase7CompositionResult"/> snapshots:
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item>RemovedEquipment / RemovedAlarms — write Bad-quality on every removed
|
||||
/// node id then call <c>RebuildAddressSpace</c> at the end so the sink can
|
||||
/// actually tear down the OPC UA folders + variables.</item>
|
||||
/// <item>AddedEquipment / AddedAlarms — same Rebuild trigger (real SDK NodeManager
|
||||
/// will repopulate from the persisted artifact). For now we record the work.</item>
|
||||
/// <item>ChangedEquipment / ChangedAlarms — record what changed; the SDK adapter
|
||||
/// that lands in F10b will decide between in-place property writes and
|
||||
/// tear-down + rebuild.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// This is the side-effecting layer Task 47 deferred to F14. It stays pure-of-SDK so
|
||||
/// production binds a real SDK sink, dev/Mac binds <see cref="NullOpcUaAddressSpaceSink"/>,
|
||||
/// and tests can capture every call.
|
||||
/// </summary>
|
||||
public sealed class Phase7Applier
|
||||
{
|
||||
private readonly IOpcUaAddressSpaceSink _sink;
|
||||
private readonly ILogger<Phase7Applier> _logger;
|
||||
|
||||
public Phase7Applier(IOpcUaAddressSpaceSink sink, ILogger<Phase7Applier> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(sink);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
_sink = sink;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply <paramref name="plan"/> to the sink. Returns a summary of what was applied so
|
||||
/// callers (OpcUaPublishActor) can correlate the work back to the originating deployment.
|
||||
/// </summary>
|
||||
public Phase7ApplyOutcome Apply(Phase7Plan plan)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(plan);
|
||||
|
||||
if (plan.IsEmpty)
|
||||
{
|
||||
_logger.LogDebug("Phase7Applier: plan is empty; skipping sink writes");
|
||||
return new Phase7ApplyOutcome(RemovedNodes: 0, AddedNodes: 0, ChangedNodes: 0, RebuildCalled: false);
|
||||
}
|
||||
|
||||
var ts = DateTime.UtcNow;
|
||||
var removedCount = 0;
|
||||
foreach (var eq in plan.RemovedEquipment)
|
||||
{
|
||||
SafeWriteAlarmState(eq.EquipmentId, active: false, acknowledged: false, ts);
|
||||
removedCount++;
|
||||
}
|
||||
foreach (var alarm in plan.RemovedAlarms)
|
||||
{
|
||||
SafeWriteAlarmState(alarm.ScriptedAlarmId, active: false, acknowledged: false, ts);
|
||||
removedCount++;
|
||||
}
|
||||
|
||||
var changedCount =
|
||||
plan.ChangedEquipment.Count + plan.ChangedDrivers.Count + plan.ChangedAlarms.Count;
|
||||
var addedCount =
|
||||
plan.AddedEquipment.Count + plan.AddedDrivers.Count + plan.AddedAlarms.Count;
|
||||
|
||||
// Any add/remove of Equipment or ScriptedAlarm requires a real address-space rebuild.
|
||||
// Driver-instance changes don't touch the address-space topology directly — they go
|
||||
// through DriverHostActor's spawn-plan in Runtime.
|
||||
var needsRebuild =
|
||||
plan.AddedEquipment.Count > 0 || plan.RemovedEquipment.Count > 0 ||
|
||||
plan.AddedAlarms.Count > 0 || plan.RemovedAlarms.Count > 0;
|
||||
|
||||
if (needsRebuild)
|
||||
{
|
||||
try
|
||||
{
|
||||
_sink.RebuildAddressSpace();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Phase7Applier: sink.RebuildAddressSpace threw");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Phase7Applier: applied plan (added={Added}, removed={Removed}, changed={Changed}, rebuild={Rebuild})",
|
||||
addedCount, removedCount, changedCount, needsRebuild);
|
||||
|
||||
return new Phase7ApplyOutcome(removedCount, addedCount, changedCount, needsRebuild);
|
||||
}
|
||||
|
||||
private void SafeWriteAlarmState(string nodeId, bool active, bool acknowledged, DateTime ts)
|
||||
{
|
||||
try { _sink.WriteAlarmState(nodeId, active, acknowledged, ts); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: WriteAlarmState threw for {Node}", nodeId); }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Summary of one apply pass. Useful for tests + audit-log entries on the deploy path.</summary>
|
||||
public sealed record Phase7ApplyOutcome(
|
||||
int RemovedNodes,
|
||||
int AddedNodes,
|
||||
int ChangedNodes,
|
||||
bool RebuildCalled);
|
||||
Reference in New Issue
Block a user