From 2bfe18abcff615952cfb7024627a9ea04cf4c68c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 7 Jun 2026 05:46:24 -0400 Subject: [PATCH] chore(runtime): warn on missing VirtualTag evaluator; document Stale-recovery VirtualTag behaviour Log a WARNING on startup when IVirtualTagEvaluator is not registered so a DI misconfig on a driver-role node is visible in logs instead of silently evaluating all VirtualTags to NoChange. Add a comment in PushDesiredSubscriptions noting that TryRecoverFromStale does not call this method, so VirtualTags remain empty after a Stale recovery until the next deployment dispatch (intentional, consistent with driver recovery). --- .../ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs | 5 +++++ .../ServiceCollectionExtensions.cs | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs index be81c509..4d8954f7 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs @@ -524,6 +524,11 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers // VirtualTagActor per plan and streams their evaluated values back onto the just-rebuilt // address space. Runs on BOTH the fresh-apply path (ApplyAndAck) and the bootstrap-restore // path (RestoreApplied) because both call this method, so one send covers both. + // NOTE: the Stale-recovery path (TryRecoverFromStale) does NOT call PushDesiredSubscriptions, + // so — like drivers — VirtualTags remain empty after a Stale recovery until the next + // deployment dispatch. This is intentional and consistent with driver recovery: the Stale + // path only restores the revision marker + NodeDeploymentState; a subsequent dispatch + // (or a redeploy from AdminUI) triggers the full apply + subscribe pass. _virtualTagHost?.Tell(new VirtualTagHostActor.ApplyVirtualTags(composition.EquipmentVirtualTags)); if (composition.EquipmentVirtualTags.Count > 0) { diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs index f1ec86f6..d17adae3 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs @@ -93,7 +93,13 @@ public static class ServiceCollectionExtensions // Production evaluator is the Host's RoslynVirtualTagEvaluator (registered as // IVirtualTagEvaluator); fall back to the null evaluator for test harnesses that don't // register one (VirtualTagActor children then evaluate to nothing). - var virtualTagEvaluator = resolver.GetService() ?? NullVirtualTagEvaluator.Instance; + var virtualTagEvaluator = resolver.GetService(); + if (virtualTagEvaluator is null) + { + loggerFactory.CreateLogger("ZB.MOM.WW.OtOpcUa.Runtime.ServiceCollectionExtensions") + .LogWarning("IVirtualTagEvaluator not registered; Equipment VirtualTags will evaluate to NoChange (no live values). Expected only in test harnesses — driver-role nodes should register RoslynVirtualTagEvaluator."); + virtualTagEvaluator = NullVirtualTagEvaluator.Instance; + } var dbHealth = system.ActorOf( DbHealthProbeActor.Props(dbFactory),