Fix 5 code review findings (P1-P3)

P1: Wire OPC UA monitored items to MXAccess subscriptions
  - Override OnCreateMonitoredItemsComplete/OnDeleteMonitoredItemsComplete
    in LmxNodeManager to trigger ref-counted SubscribeTag/UnsubscribeTag
  - Clients subscribing to tags now start live MXAccess data pushes

P1: Write timeout now returns false instead of true
  - Previously a missing OnWriteComplete callback was treated as success
  - Now correctly reports failure so OPC UA clients see the error

P1: Auto-reconnect retries from Error state (not just Disconnected)
  - Monitor loop now checks both Disconnected and Error states
  - Prevents permanent outages after a single failed reconnect attempt

P2: Topological sort on hierarchy before building address space
  - Parents guaranteed to appear before children regardless of input order
  - Prevents misplaced nodes when SQL returns unsorted results

P3: Skip redundant first-poll rebuild on startup
  - ChangeDetectionService accepts initial deploy time from OpcUaService
  - First poll only triggers rebuild if deploy time is actually unknown
  - Eliminates duplicate DB fetch and address space rebuild at startup

All 212 tests pass (205 unit + 7 integration).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-25 07:16:23 -04:00
parent ee7e190fab
commit 71254e005e
5 changed files with 98 additions and 14 deletions

View File

@@ -22,10 +22,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository
public event Action? OnGalaxyChanged;
public DateTime? LastKnownDeployTime => _lastKnownDeployTime;
public ChangeDetectionService(IGalaxyRepository repository, int intervalSeconds)
public ChangeDetectionService(IGalaxyRepository repository, int intervalSeconds, DateTime? initialDeployTime = null)
{
_repository = repository;
_intervalSeconds = intervalSeconds;
_lastKnownDeployTime = initialDeployTime;
}
public void Start()
@@ -43,8 +44,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository
private async Task PollLoopAsync(CancellationToken ct)
{
// First poll always triggers
bool firstPoll = true;
// If no initial deploy time was provided, first poll triggers unconditionally
bool firstPoll = _lastKnownDeployTime == null;
while (!ct.IsCancellationRequested)
{