fix(historian): #674 review follow-ups — HistoryUpdate boot-window gate, node-level v1 refusal, plan-before-gate, UTC skew compare
- HistoryUpdate Primary gate now reads IHistoryUpdateRoleView (PrimaryGatePolicy.ShouldServeHistoryUpdate): the data-plane verdict but CLOSED until this node's first redundancy snapshot, so both pair nodes cannot accept in the pre-join boot window. Single nodes open on their own singleton's first snapshot; admin-only nodes register no view => no gate. DriverHostActor publishes it at every Primary-verdict publish point. - SqlServer v1: the updater throws NotSupportedException on every arm; the node manager maps it to a node-level BadHistoryOperationUnsupported (was per-item under a Good node for UpdateData/DeleteAtTime). HistoryWrite bit still advertised on v1 (not knowable at materialise time) — documented. - The update plan is evaluated before the Primary gate: an unservable request (Remove, IsDeleteModified, not historized, no updater) is BadHistoryOperationUnsupported off-Primary, not BadInvalidState. - Local-kind SourceTimestamp converted to UTC before the future-skew compare and sent as UTC. - Hosted-service wiring extracted to BuildHistoryUpdatePosture and tested (gate source, skew, clock). - Docs: Historian.md (boot window, ordering, node-level v1, HistoryWrite bit), CLAUDE.md. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -75,9 +75,29 @@ public interface IHistorizationRoleView
|
||||
void PublishHistorizationRole(bool shouldRecordHistorizedValues);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #674 review: a singleton snapshot of the OPC UA <b>HistoryUpdate</b> verdict
|
||||
/// (<see cref="PrimaryGatePolicy.ShouldServeHistoryUpdate"/>), read by the node manager's HistoryUpdate Primary gate.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A separate verdict from <see cref="IDriverPrimaryRoleSource.ShouldActAsPrimary"/> because it is closed until the
|
||||
/// first redundancy snapshot, where that one mirrors the live data-plane gate and is open through the pre-join boot
|
||||
/// window. Registered unconditionally on every driver-role node (same object as the other views); a node that does
|
||||
/// not register it wires no gate.
|
||||
/// </remarks>
|
||||
public interface IHistoryUpdateRoleView
|
||||
{
|
||||
/// <summary>Whether this node may serve HistoryUpdate right now.</summary>
|
||||
bool ShouldServeHistoryUpdate { get; }
|
||||
|
||||
/// <summary>Records a fresh decision. Called by <c>DriverHostActor</c> whenever it publishes its Primary verdict.</summary>
|
||||
/// <param name="shouldServeHistoryUpdate">The decision <see cref="PrimaryGatePolicy.ShouldServeHistoryUpdate"/> produced.</param>
|
||||
void PublishHistoryUpdateRole(bool shouldServeHistoryUpdate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe <see cref="IRedundancyRoleView"/> + <see cref="IDriverPrimaryRoleSource"/> +
|
||||
/// <see cref="IHistorizationRoleView"/>, each backed by its own volatile field.
|
||||
/// <see cref="IHistorizationRoleView"/> + <see cref="IHistoryUpdateRoleView"/>, each backed by its own volatile field.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
@@ -90,8 +110,15 @@ public interface IHistorizationRoleView
|
||||
/// them is the bug that live gate already found once.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class RedundancyRoleView : IRedundancyRoleView, IDriverPrimaryRoleSource, IHistorizationRoleView
|
||||
public sealed class RedundancyRoleView : IRedundancyRoleView, IDriverPrimaryRoleSource, IHistorizationRoleView,
|
||||
IHistoryUpdateRoleView
|
||||
{
|
||||
// Gitea #674 review: seeded CLOSED, unlike the data-plane verdict. HistoryUpdate waits for a real redundancy verdict
|
||||
// (PrimaryGatePolicy.ShouldServeHistoryUpdate); a node that never publishes here is not a driver node with a
|
||||
// redundancy manager, and such a node does not register this view as its gate source.
|
||||
private volatile bool _shouldServeHistoryUpdate =
|
||||
PrimaryGatePolicy.ShouldServeHistoryUpdate(redundancySnapshotReceived: false, localRole: null, driverMemberCount: 1);
|
||||
|
||||
// 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
|
||||
@@ -121,6 +148,12 @@ public sealed class RedundancyRoleView : IRedundancyRoleView, IDriverPrimaryRole
|
||||
/// <inheritdoc/>
|
||||
public bool ShouldActAsPrimary => _shouldActAsPrimary;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool ShouldServeHistoryUpdate => _shouldServeHistoryUpdate;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void PublishHistoryUpdateRole(bool shouldServeHistoryUpdate) => _shouldServeHistoryUpdate = shouldServeHistoryUpdate;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Publish(bool shouldDrainAlarmHistory) => _shouldDrainAlarmHistory = shouldDrainAlarmHistory;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user