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;
}