fix(redundancy): advertise ServiceLevel 0 until the node has a redundancy level (#885)
v2-ci / unit-tests (push) Has been cancelled
v2-ci / unit-tests (push) Has been cancelled
The SDK starts Server.ServiceLevel at 255 (full service), and OpcUaPublishActor publishes nothing until the node's first redundancy snapshot arrives, so a restarting pair node advertised 255 for the ~10-13 s it took to join, out-ranking the real Primary (250) for any client that picks the highest ServiceLevel. OtOpcUaSdkServer now writes StartupServiceLevel = 0 in OnNodeManagerStartedAsync: after the SDK's CreateServerObjectAsync wrote 255 and before the server goes Running, so no session can ever read 255. 0 is the design's existing Down / Detached tier (ServiceLevelCalculator returns 0 for a member that is not Up/Joining, and a node with no snapshot has no role yet). It is below the SDK 2 session-headroom floor (200), so it stands through session churn before the #884 claim, and the claim's bind replays any level already requested. Single-node deployments are unaffected beyond the window: the first snapshot publishes 250 as before. New Node_with_no_requested_level_advertises_the_startup_level_until_one_is_published: reads 0 before the bind, 0 after a bind with nothing to replay, then 250 once published. Without the startup write it reads 255 on the first session (mutation-checked). docs/Redundancy.md updated for #884 and #885. Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
@@ -305,11 +305,30 @@ public sealed class OtOpcUaSdkServer : StandardServer
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <c>Server.ServiceLevel</c> a node advertises from the moment its endpoint accepts sessions until the
|
||||
/// redundancy layer publishes a real level (lmxopcua#885): <c>0</c>, the design's Down / Detached tier
|
||||
/// (<c>ServiceLevelCalculator</c> returns 0 for a member that is not Up/Joining, and a node that has not yet
|
||||
/// received its first redundancy snapshot has no role). The SDK starts the value at 255 — full service — so
|
||||
/// a restarting pair node out-ranked the real Primary (250) for the ~10 s it took to join. Below the SDK's
|
||||
/// 200 floor, so the session-headroom calculator never lifts it either.
|
||||
/// </summary>
|
||||
public const byte StartupServiceLevel = 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask OnNodeManagerStartedAsync(
|
||||
IServerInternal server, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await base.OnNodeManagerStartedAsync(server, cancellationToken).ConfigureAwait(false);
|
||||
// Runs after CreateServerObjectAsync wrote the SDK's 255 and before the server goes Running, so no session can
|
||||
// ever read 255. SdkServiceLevelPublisher claims the value once the host binds and replays any level the
|
||||
// redundancy layer already requested; until one is requested this value stands.
|
||||
if (server.ServerObject?.ServiceLevel is { } serviceLevel)
|
||||
{
|
||||
serviceLevel.Value = StartupServiceLevel;
|
||||
serviceLevel.Timestamp = DateTime.UtcNow;
|
||||
serviceLevel.ClearChangeMasks(server.DefaultSystemContext, includeChildren: false);
|
||||
}
|
||||
var auditType = server.DiagnosticsNodeManager?
|
||||
.FindPredefinedNode<BaseObjectTypeState>(ObjectTypeIds.AuditWriteUpdateEventType)
|
||||
?? throw new InvalidOperationException("AuditWriteUpdateEventType is unavailable during server startup.");
|
||||
|
||||
Reference in New Issue
Block a user