namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// /// A driver-facing snapshot of "should this node be the one asserting an exclusive identity on a /// shared external system right now?". /// /// /// /// Why drivers need this at all. The driver layer has no notion of redundancy, by /// design — a driver talks to a device and the Primary gate is applied above it, in /// DriverHostActor, on the way in. That works because every gated operation is /// inbound: a write, an alarm ack, an alerts publish. A Sparkplug primary-host claim is /// not inbound. It is a standing assertion the driver makes to the broker, on its own /// connection, for as long as it holds one — there is no message for the host actor to /// intercept. So the decision has to reach the driver instead (Gitea #511). /// /// /// The decision, not the role. Same discipline as the alarm-drain view in /// Runtime: this publishes the verdict a single policy produced, so the policy stays the /// one place that decides and a driver can never re-derive a subtly different rule from a raw /// role. The verdict fed here is the data-plane one (an unknown role on a cluster that /// has a driver peer denies), because two nodes claiming one Sparkplug host id is the harm the /// gate exists to prevent — not a duplicate row, but a Secondary's Last Will publishing /// OFFLINE over a live Primary's ONLINE and stopping every edge node in the /// group. /// /// /// Polled, not evented, and that is deliberate. A process-wide singleton raising an /// event onto per-instance drivers outlives every driver it hands a reference to — the exact /// missing--= leak shape the rediscovery signal had to be careful about. Consumers /// sample this instead, so a driver that is torn down simply stops asking. The cost is up to /// one poll interval of latency on a failover, which is inside the edge-visible /// OFFLINE window a failover already implies. /// /// public interface IDriverPrimaryRoleSource { /// /// Whether this node currently holds the Primary data-plane role — and may therefore claim an /// exclusive external identity such as a Sparkplug host id. /// bool ShouldActAsPrimary { get; } /// /// Records a fresh decision. Called by DriverHostActor on every redundancy snapshot. /// /// /// The verdict PrimaryGatePolicy.ShouldServiceAsPrimary produced. /// void PublishPrimaryRole(bool shouldActAsPrimary); } /// /// The a driver uses when nothing publishes one — a /// standalone host, a unit test, or any deployment with no redundancy manager wired. /// /// /// /// It answers , and the alternative is worse. Defaulting closed /// would be the reflexive "fail-safe" choice, but there is no safety to gain: with nothing /// publishing a role there is by definition no peer to collide with, and a closed default /// would make an exclusive-identity claim silently never happen on the single-node deployment /// that is the common case — an inert feature with no error, which is precisely the failure /// mode Gitea #511 was filed about. /// /// /// The risk this shifts onto the wiring is real and is guarded there instead: a redundant /// deployment whose composition root forgets to supply the live source would fall back to this /// and let both nodes claim. DriverFactoryBootstrapPrimaryRoleSourceTests pins that the /// Host resolves a real one. /// /// public sealed class UnmanagedDriverPrimaryRoleSource : IDriverPrimaryRoleSource { /// The shared instance — it holds no state. public static readonly UnmanagedDriverPrimaryRoleSource Instance = new(); private UnmanagedDriverPrimaryRoleSource() { } /// public bool ShouldActAsPrimary => true; /// /// No-op: there is no redundancy manager to publish anything here. public void PublishPrimaryRole(bool shouldActAsPrimary) { } }