using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Runtime.Drivers; namespace ZB.MOM.WW.OtOpcUa.Runtime.Redundancy; /// /// A singleton snapshot of the alarm-history drain decision, so code that lives outside an actor /// can ask "should this node be draining the alarm queue right now?". /// /// /// /// Exists for the alarm store-and-forward drain. That drain runs on a timer owned by the /// sink, not on an actor mailbox, so it cannot receive RedundancyStateChanged /// directly — but it must be role-scoped, because the queue it drains replicates to the /// pair peer and two draining nodes would deliver every event twice. /// /// /// Deliberately publishes the decision rather than the role that produced it, so /// stays the single place that decides. /// /// /// This is not the device-write gate. It is fed by /// , which opens on an unknown role /// where closes. The naming is /// explicit about the consumer for that reason: an earlier revision published the /// write-gate verdict here, and on a cluster with several driver nodes but no configured /// redundancy it suspended the drain everywhere, so alarm history accumulated on every node /// and left none. /// /// public interface IRedundancyRoleView { /// Whether this node should be draining the alarm store-and-forward queue right now. bool ShouldDrainAlarmHistory { get; } /// Records a fresh decision. Called by DriverHostActor on every redundancy snapshot. /// /// The decision produced. /// void Publish(bool shouldDrainAlarmHistory); } /// /// A singleton snapshot of the continuous-historization capture decision (Gitea #537), so /// the recorder can ask "should this node be recording historized values right now?". /// /// /// /// A separate interface from even though one object /// implements both, because the two are registered on different terms: /// IRedundancyRoleView is registered by AddAlarmHistorian and so exists only /// where the alarm section is enabled, while this one is registered unconditionally on every /// driver-role node. Piggybacking on the alarm registration would silently drop the /// historization gate on any pair that runs historization without alarm history — the same /// registration accident Gitea #511 documents on the neighbouring verdict. /// /// /// Publishes the decision rather than the role, so /// stays the single place that /// decides. Note it is not the alarm drain verdict: the two policies agree on the role /// rule but disagree on what makes a peer relevant (a queue-sharing replication partner vs. a /// node serving the same address space), and the wrong peer notion in either direction is a /// silent gap. /// /// public interface IHistorizationRoleView { /// Whether this node should be capturing continuous-historization values right now. bool ShouldRecordHistorizedValues { get; } /// Records a fresh decision. Called by DriverHostActor on every redundancy snapshot. /// /// The decision produced. /// void PublishHistorizationRole(bool shouldRecordHistorizedValues); } /// /// Thread-safe + + /// , each backed by its own volatile field. /// /// /// /// Two verdicts, one object, on purpose. Both are published from the same /// DriverHostActor snapshot handler and both are read off-actor, so a second class /// would duplicate the publish plumbing to hold a second bool. They are kept as distinct /// properties rather than collapsed into one because they are genuinely different /// decisions from different policies — see 's remarks on why /// the drain gate must open on an unknown role where the data-plane gate closes. Collapsing /// them is the bug that live gate already found once. /// /// public sealed class RedundancyRoleView : IRedundancyRoleView, IDriverPrimaryRoleSource, IHistorizationRoleView { // Seeded with the decision for "role unknown" rather than a bare false. An unpublished view and a // node whose role nothing ever reports are genuinely the same situation, and they must behave the // same: a deployment that runs no redundancy at all never publishes here, and defaulting closed // would silently stop its alarm history forever. private volatile bool _shouldDrainAlarmHistory = PrimaryGatePolicy.ShouldDrainAlarmHistory(localRole: null, queueSharingPeerIsPrimary: false); // Seeded from the same policy that will later publish here, for the unknown-role/no-peer case: // a node nothing has reported a role for, on a cluster with no driver peer, is a single-node // deployment and may claim. The moment a snapshot arrives this is replaced by the real verdict, // and a multi-driver cluster's unknown role denies. private volatile bool _shouldActAsPrimary = PrimaryGatePolicy.ShouldServiceAsPrimary(localRole: null, driverMemberCount: 1); // Seeded for the unknown-role case from the same policy that will later publish here: a node // nothing has reported a role for captures, exactly as an unpaired or single-node deployment must. // Defaulting closed would leave a deployment that never publishes a role with no trend data at all. private volatile bool _shouldRecordHistorizedValues = PrimaryGatePolicy.ShouldRecordHistorizedValues(localRole: null, addressSpaceSharingPeerIsPrimary: false); /// public bool ShouldDrainAlarmHistory => _shouldDrainAlarmHistory; /// public bool ShouldRecordHistorizedValues => _shouldRecordHistorizedValues; /// public bool ShouldActAsPrimary => _shouldActAsPrimary; /// public void Publish(bool shouldDrainAlarmHistory) => _shouldDrainAlarmHistory = shouldDrainAlarmHistory; /// public void PublishPrimaryRole(bool shouldActAsPrimary) => _shouldActAsPrimary = shouldActAsPrimary; /// public void PublishHistorizationRole(bool shouldRecordHistorizedValues) => _shouldRecordHistorizedValues = shouldRecordHistorizedValues; }