feat(mesh-phase4): retire DB-reachability from ServiceLevel on DB-less nodes
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
@@ -103,6 +103,11 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
private readonly IDbContextFactory<OtOpcUaConfigDbContext>? _dbFactory;
|
||||
private readonly AddressSpaceApplier? _applier;
|
||||
private readonly IActorRef? _dbHealthProbe;
|
||||
/// <summary>Per-cluster mesh Phase 4: this is a driver-only node with NO ConfigDb (LocalDb is its
|
||||
/// config store). There is no DB to probe, so the ServiceLevel DB-reachability input is retired —
|
||||
/// <c>DbReachable</c> is fed constant <c>true</c> and only snapshot staleness can demote. See
|
||||
/// <see cref="RecomputeServiceLevel"/>.</summary>
|
||||
private readonly bool _dbLess;
|
||||
private readonly TimeSpan _staleWindow;
|
||||
private readonly TimeSpan _probeFreshnessWindow;
|
||||
private readonly TimeSpan _healthTickInterval;
|
||||
@@ -153,6 +158,9 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
/// <param name="healthTickInterval">The period between self-driven DB-health refresh ticks (each
|
||||
/// Asks <paramref name="dbHealthProbe"/> for its cached status); defaults to 5 seconds. No timer is
|
||||
/// started when <paramref name="dbHealthProbe"/> is null.</param>
|
||||
/// <param name="dbLess">Per-cluster mesh Phase 4: this is a driver-only node with no ConfigDb, so the
|
||||
/// ServiceLevel DB-reachability input is retired (<c>DbReachable</c> treated constant-true, only
|
||||
/// staleness demotes). No <paramref name="dbHealthProbe"/> is wired on such a node.</param>
|
||||
/// <returns>The configured <see cref="Props"/> for creating this actor, pinned to the
|
||||
/// <see cref="DispatcherId"/> dispatcher.</returns>
|
||||
public static Props Props(
|
||||
@@ -164,7 +172,8 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
IActorRef? dbHealthProbe = null,
|
||||
TimeSpan? staleWindow = null,
|
||||
TimeSpan? probeFreshnessWindow = null,
|
||||
TimeSpan? healthTickInterval = null) =>
|
||||
TimeSpan? healthTickInterval = null,
|
||||
bool dbLess = false) =>
|
||||
Akka.Actor.Props.Create(() => new OpcUaPublishActor(
|
||||
sink ?? NullOpcUaAddressSpaceSink.Instance,
|
||||
serviceLevel ?? NullServiceLevelPublisher.Instance,
|
||||
@@ -175,7 +184,8 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
dbHealthProbe,
|
||||
staleWindow,
|
||||
probeFreshnessWindow,
|
||||
healthTickInterval)).WithDispatcher(DispatcherId);
|
||||
healthTickInterval,
|
||||
dbLess)).WithDispatcher(DispatcherId);
|
||||
|
||||
/// <summary>Test-only Props that omits the pinned-dispatcher requirement and skips the
|
||||
/// DPS subscribe so unit tests can spin up the actor on a vanilla TestKit cluster.</summary>
|
||||
@@ -195,6 +205,9 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
/// <param name="healthTickInterval">The period between self-driven DB-health refresh ticks (each
|
||||
/// Asks <paramref name="dbHealthProbe"/> for its cached status); defaults to 5 seconds. No timer is
|
||||
/// started when <paramref name="dbHealthProbe"/> is null.</param>
|
||||
/// <param name="dbLess">Per-cluster mesh Phase 4: this is a driver-only node with no ConfigDb, so the
|
||||
/// ServiceLevel DB-reachability input is retired (<c>DbReachable</c> treated constant-true, only
|
||||
/// staleness demotes). No <paramref name="dbHealthProbe"/> is wired on such a node.</param>
|
||||
/// <returns>The configured <see cref="Props"/> for creating this actor in tests, without dispatcher
|
||||
/// pinning or the DPS subscribe.</returns>
|
||||
public static Props PropsForTests(
|
||||
@@ -207,7 +220,8 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
IActorRef? dbHealthProbe = null,
|
||||
TimeSpan? staleWindow = null,
|
||||
TimeSpan? probeFreshnessWindow = null,
|
||||
TimeSpan? healthTickInterval = null) =>
|
||||
TimeSpan? healthTickInterval = null,
|
||||
bool dbLess = false) =>
|
||||
Akka.Actor.Props.Create(() => new OpcUaPublishActor(
|
||||
sink ?? NullOpcUaAddressSpaceSink.Instance,
|
||||
serviceLevel ?? NullServiceLevelPublisher.Instance,
|
||||
@@ -218,7 +232,8 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
dbHealthProbe,
|
||||
staleWindow,
|
||||
probeFreshnessWindow,
|
||||
healthTickInterval));
|
||||
healthTickInterval,
|
||||
dbLess));
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="OpcUaPublishActor"/> class.</summary>
|
||||
/// <param name="sink">The OPC UA address space sink.</param>
|
||||
@@ -237,6 +252,9 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
/// <param name="healthTickInterval">The period between self-driven DB-health refresh ticks (each
|
||||
/// Asks <paramref name="dbHealthProbe"/> for its cached status); defaults to 5 seconds. No timer is
|
||||
/// started when <paramref name="dbHealthProbe"/> is null.</param>
|
||||
/// <param name="dbLess">Per-cluster mesh Phase 4: this is a driver-only node with no ConfigDb, so the
|
||||
/// ServiceLevel DB-reachability input is retired (<c>DbReachable</c> treated constant-true, only
|
||||
/// staleness demotes). No <paramref name="dbHealthProbe"/> is wired on such a node.</param>
|
||||
public OpcUaPublishActor(
|
||||
IOpcUaAddressSpaceSink sink,
|
||||
IServiceLevelPublisher serviceLevel,
|
||||
@@ -247,7 +265,8 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
IActorRef? dbHealthProbe = null,
|
||||
TimeSpan? staleWindow = null,
|
||||
TimeSpan? probeFreshnessWindow = null,
|
||||
TimeSpan? healthTickInterval = null)
|
||||
TimeSpan? healthTickInterval = null,
|
||||
bool dbLess = false)
|
||||
{
|
||||
_sink = sink;
|
||||
_serviceLevel = serviceLevel;
|
||||
@@ -256,6 +275,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
_dbFactory = dbFactory;
|
||||
_applier = applier;
|
||||
_dbHealthProbe = dbHealthProbe;
|
||||
_dbLess = dbLess;
|
||||
_staleWindow = staleWindow ?? TimeSpan.FromSeconds(30);
|
||||
_probeFreshnessWindow = probeFreshnessWindow ?? TimeSpan.FromSeconds(30);
|
||||
_healthTickInterval = healthTickInterval ?? TimeSpan.FromSeconds(5);
|
||||
@@ -635,6 +655,28 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
return;
|
||||
}
|
||||
|
||||
// Per-cluster mesh Phase 4: on a driver-only (DB-less) node there is no ConfigDb to probe —
|
||||
// LocalDb IS the config store and is in-process, so it can never be "unreachable". Retire the
|
||||
// DB-reachability input by feeding DbReachable=true CONSTANT and deriving staleness from the
|
||||
// snapshot age ALONE (no DB-health term). This is the survive-alone posture and it is
|
||||
// client-visible: a healthy DB-less node must publish 240 (240+10=250 as Primary) even with
|
||||
// central down, so clients keep steering to it — the old DbReachable=false would have computed
|
||||
// the (false,true,false) → 0 arm and driven clients AWAY from a node serving perfectly.
|
||||
// Staleness ("running behind on config") still demotes to 200. The Detached guard above still
|
||||
// wins, so a DB-less Detached node stays 0.
|
||||
if (_dbLess)
|
||||
{
|
||||
var utcNow = DateTime.UtcNow;
|
||||
var dbLessInputs = new NodeHealthInputs(
|
||||
MemberState: SafeSelfStatus(),
|
||||
DbReachable: true,
|
||||
OpcUaProbeOk: OpcUaProbeOk(),
|
||||
Stale: (utcNow - entry.AsOfUtc) > _staleWindow,
|
||||
IsDriverPrimary: entry.IsDriverPrimary);
|
||||
Self.Tell(new ServiceLevelChanged(ServiceLevelCalculator.Compute(dbLessInputs)));
|
||||
return;
|
||||
}
|
||||
|
||||
// Legacy / back-compat seam: with no DB-health probe wired (or before the first sample
|
||||
// arrives) fall back to the old role-only switch. This preserves historical behaviour and
|
||||
// is the bootstrap value until the first DbHealthStatus lands.
|
||||
|
||||
Reference in New Issue
Block a user