Merge review R1 fixes: removed driver's resilience tracker released; SDK timed-unshelve no-op (engine owns expiry); commented re-ack stored as AddComment; native-ack Primary verdict published from start; drain gate SharesRowsWithPeer; malformed cache bytes don't adopt the revision; HostAbortedException passthrough; ClusterRoleInfo seed filtered
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+42
-8
@@ -263,14 +263,18 @@ public sealed class ConditionMethodEventTests
|
||||
|
||||
/// <summary>
|
||||
/// A scripted TimedShelve expires: the engine's expiry comes back as an Unshelved re-projection, which
|
||||
/// reports exactly one unshelve event after the one shelve event.
|
||||
/// reports exactly one unshelve event after the one shelve event — and the SDK's own expiry timer routes
|
||||
/// nothing (review R1 M2).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The expiry is the engine's (computed from the command's <c>UnshelveAtUtc</c>). Since Gitea #597 the node
|
||||
/// is shelved for the real duration, so the SDK's own timer fires at the same instant — but it only routes
|
||||
/// an Unshelve back to the engine (ignored by this stand-in, a no-op on an unshelved engine alarm) and
|
||||
/// changes no node state, so it adds no event. The stand-in engine here schedules its expiry from the real
|
||||
/// <c>UnshelveAtUtc</c> the method handler computed.
|
||||
/// is shelved for the real duration, so the SDK's own timer fires at that instant. It used to route an
|
||||
/// operator-style <c>Unshelve</c> by <c>system</c>, which — because the real engine only checks expiry every
|
||||
/// 5 s — pre-empted the engine's <c>AutoUnshelve</c> on every expiry. This stand-in engine therefore
|
||||
/// (a) expires LATE, like the real one, so the SDK timer demonstrably fires first; and (b) ACTS on a routed
|
||||
/// <c>Unshelve</c> as the real engine would, recording who unshelved it. The wrapped
|
||||
/// <c>OnTimedUnshelve</c> is the positive control: the SDK timer did fire, and still nothing was routed and
|
||||
/// the unshelve on record is the engine's own.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task A_scripted_timed_shelve_expires_with_exactly_one_unshelve_event()
|
||||
@@ -283,21 +287,47 @@ public sealed class ConditionMethodEventTests
|
||||
rig.MaterialiseScripted();
|
||||
var current = PreState("TimedShelve", native: false) with { Message = "base" };
|
||||
Task? expiry = null;
|
||||
var routed = new System.Collections.Concurrent.ConcurrentQueue<string>();
|
||||
string? unshelvedBy = null;
|
||||
var engineLag = TimeSpan.FromMilliseconds(1500); // the real engine's 5 s shelving-check granularity
|
||||
rig.Nm.AlarmCommandRouter = cmd =>
|
||||
{
|
||||
routed.Enqueue(cmd.Operation);
|
||||
if (cmd.Operation == "Unshelve")
|
||||
{
|
||||
// What the real engine does with a routed Unshelve: an operator-style unshelve.
|
||||
unshelvedBy ??= $"Unshelve:{cmd.User}";
|
||||
current = current with { Shelving = AlarmShelvingKind.Unshelved, UnshelveAtUtc = null };
|
||||
rig.ProjectLater(native: false, current);
|
||||
return;
|
||||
}
|
||||
if (cmd.Operation != "TimedShelve") return;
|
||||
current = current with { Shelving = AlarmShelvingKind.Timed };
|
||||
// The real engine's projection carries its expiry, so the re-projection keeps the SDK timer armed
|
||||
// for the real remaining time (without it, it would re-arm for MaxTimeShelved and never fire here).
|
||||
current = current with { Shelving = AlarmShelvingKind.Timed, UnshelveAtUtc = cmd.UnshelveAtUtc };
|
||||
rig.ProjectLater(native: false, current);
|
||||
var unshelveAt = cmd.UnshelveAtUtc!.Value;
|
||||
expiry = Task.Run(async () =>
|
||||
{
|
||||
var wait = unshelveAt - DateTime.UtcNow;
|
||||
var wait = unshelveAt + engineLag - DateTime.UtcNow;
|
||||
if (wait > TimeSpan.Zero) await Task.Delay(wait);
|
||||
current = current with { Shelving = AlarmShelvingKind.Unshelved };
|
||||
if (current.Shelving != AlarmShelvingKind.Timed) return; // ApplyShelvingCheck: Timed only
|
||||
unshelvedBy ??= "AutoUnshelve:system";
|
||||
current = current with { Shelving = AlarmShelvingKind.Unshelved, UnshelveAtUtc = null };
|
||||
rig.Project(native: false, current);
|
||||
});
|
||||
};
|
||||
|
||||
// Count the SDK's own expiry-timer firings without changing what the production handler does.
|
||||
var condition = rig.Nm.TryGetAlarmCondition(ScriptedAlarmId)!;
|
||||
var sdkTimerFired = 0;
|
||||
var production = condition.OnTimedUnshelve!;
|
||||
condition.OnTimedUnshelve = (context, c) =>
|
||||
{
|
||||
Interlocked.Increment(ref sdkTimerFired);
|
||||
return production(context, c);
|
||||
};
|
||||
|
||||
using var session = await rig.ConnectAsync(anonymous: false, ct);
|
||||
var events = new RecordedEvents();
|
||||
var subscription = await RecordedEvents.SubscribeAsync(session, events, ct);
|
||||
@@ -315,6 +345,10 @@ public sealed class ConditionMethodEventTests
|
||||
rig.Project(false, current = current with { Message = "sentinel" });
|
||||
await RecordedEvents.WaitUntilAsync(() => events.Has(item, "sentinel"));
|
||||
|
||||
Volatile.Read(ref sdkTimerFired).ShouldBeGreaterThanOrEqualTo(1, "the SDK's timed-unshelve timer fired");
|
||||
routed.ShouldNotContain("Unshelve", "the SDK timer must not route an operator-style Unshelve");
|
||||
unshelvedBy.ShouldBe("AutoUnshelve:system", "the engine's own expiry owns the unshelve");
|
||||
|
||||
var shelving = events.ConditionEvents(item, "base").Select(e => e[RecordedEvents.ShelvingField].Value).ToList();
|
||||
shelving.ShouldBe(
|
||||
[ObjectIds.ShelvedStateMachineType_Unshelved, ObjectIds.ShelvedStateMachineType_TimedShelved,
|
||||
|
||||
Reference in New Issue
Block a user