fix: track realtime physical work from invocation to settlement
RT-RR-01. An effect or recovery task was registered as retained only after its public wait expired, so a close() that arrived first saw an empty set and reported quiescence while the raw task was still running against the authority. Tasks are now registered when they are created and removed when they settle; DRAINING keeps its narrower meaning through a separate timed-out set. RT-RR-02. Admission happened when an event was queued; execution is a second decision. A queue entry admitted before the stream entered DRAINING no longer starts running inside it. And an abandoned task may have applied part of its effect, so the resume token it was based on is discarded and recovery is required explicitly — the next ordinary event can no longer skip authoritative recovery on the strength of state a timed-out effect may have invalidated. RT-RR-03. close() cached the first timeout forever, so a writer that later settled could never be proved quiescent and the retained registry could never be pruned. Only an in-flight close is shared now, every writer a close fences is retained until its tail actually settles, and the tail prunes itself. A second close therefore converges to success once the writer finishes. RT-RR-04. Checkpoint work joins writer tails in the physical-task registry from invocation to settlement and is drained on the same terms. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a7390e3b3a
commit
c0f53d1855
@@ -943,6 +943,118 @@ describe("transport-independent realtime stream coordinator", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* RT-RR-01 / RT-RR-02. A physical effect exists from the moment the coordinator
|
||||
* calls the authority, not from the moment its public wait expires. Registering
|
||||
* only on timeout let a `close()` that arrived first see an empty retained set
|
||||
* and report quiescence while the raw task was still running against the
|
||||
* authority.
|
||||
*/
|
||||
describe("realtime physical task ownership", () => {
|
||||
it("does not report quiescence while an apply is still running", async () => {
|
||||
const applyGate = deferred<RealtimeResult<void>>();
|
||||
const harness = createHarness({
|
||||
apply: async () => await applyGate.promise,
|
||||
taskLimits: { effectTimeoutMs: 10_000, drainTimeoutMs: 20 },
|
||||
});
|
||||
await harness.initialize();
|
||||
|
||||
const applying = harness.coordinator.accept(harness.event());
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
// close() arrives long before the effect deadline.
|
||||
const closed = await harness.coordinator.close();
|
||||
expect(closed.ok).toBe(false);
|
||||
expect(closed.ok ? null : closed.error.kind).toBe("IDLE_TIMEOUT");
|
||||
expect(harness.coordinator.lifecycle(STREAM_ID)).toBe("DRAINING");
|
||||
|
||||
applyGate.resolve(realtimeSuccess(undefined));
|
||||
await applying;
|
||||
});
|
||||
|
||||
it("reports quiescence once the raw task settles", async () => {
|
||||
const applyGate = deferred<RealtimeResult<void>>();
|
||||
const harness = createHarness({
|
||||
apply: async () => await applyGate.promise,
|
||||
taskLimits: { effectTimeoutMs: 10_000, drainTimeoutMs: 200 },
|
||||
});
|
||||
await harness.initialize();
|
||||
|
||||
const applying = harness.coordinator.accept(harness.event());
|
||||
await Promise.resolve();
|
||||
const closing = harness.coordinator.close();
|
||||
applyGate.resolve(realtimeSuccess(undefined));
|
||||
await applying;
|
||||
|
||||
expect(await closing).toMatchObject({ ok: true });
|
||||
});
|
||||
|
||||
it("does not start a queued event once the stream is DRAINING", async () => {
|
||||
const firstApply = deferred<RealtimeResult<void>>();
|
||||
let applyCalls = 0;
|
||||
const harness = createHarness({
|
||||
apply: async () => {
|
||||
applyCalls += 1;
|
||||
if (applyCalls === 1) return await firstApply.promise;
|
||||
return realtimeSuccess(undefined);
|
||||
},
|
||||
taskLimits: { effectTimeoutMs: 15, drainTimeoutMs: 50 },
|
||||
});
|
||||
await harness.initialize();
|
||||
|
||||
const first = harness.coordinator.accept(
|
||||
harness.event({ eventId: "event-0001", sequence: "1" }),
|
||||
);
|
||||
// Queued behind the first while it is still inside its deadline.
|
||||
const second = harness.coordinator.accept(
|
||||
harness.event({ eventId: "event-0002", sequence: "2" }),
|
||||
);
|
||||
|
||||
await first;
|
||||
expect(harness.coordinator.lifecycle(STREAM_ID)).toBe("DRAINING");
|
||||
|
||||
// The queued event is dropped at execution time, not applied and not
|
||||
// recovered: admission happened before DRAINING, execution happens inside
|
||||
// it, and the second decision is the one that counts.
|
||||
const secondResult = await second;
|
||||
expect(secondResult).toMatchObject({
|
||||
ok: true,
|
||||
value: { outcome: "DROPPED", reason: "CLOSED" },
|
||||
});
|
||||
expect(applyCalls).toBe(1);
|
||||
|
||||
firstApply.resolve(realtimeSuccess(undefined));
|
||||
});
|
||||
|
||||
it("discards the resume token when an effect is abandoned", async () => {
|
||||
const firstApply = deferred<RealtimeResult<void>>();
|
||||
let applyCalls = 0;
|
||||
const harness = createHarness({
|
||||
apply: async () => {
|
||||
applyCalls += 1;
|
||||
if (applyCalls === 1) return await firstApply.promise;
|
||||
return realtimeSuccess(undefined);
|
||||
},
|
||||
taskLimits: { effectTimeoutMs: 15, drainTimeoutMs: 50 },
|
||||
});
|
||||
await harness.initialize();
|
||||
expect(harness.coordinator.inspect(STREAM_ID).hasResumeState).toBe(true);
|
||||
|
||||
await harness.coordinator.accept(
|
||||
harness.event({ eventId: "event-0001", sequence: "1" }),
|
||||
);
|
||||
|
||||
// The abandoned effect may have applied part of its change, so the token it
|
||||
// was based on is no longer authoritative evidence.
|
||||
const inspection = harness.coordinator.inspect(STREAM_ID);
|
||||
expect(inspection.hasResumeState).toBe(false);
|
||||
expect(inspection.freshness).toBe("UNKNOWN");
|
||||
|
||||
firstApply.resolve(realtimeSuccess(undefined));
|
||||
});
|
||||
});
|
||||
|
||||
type HarnessOptions = Readonly<{
|
||||
registry?: RealtimePolicyRegistry;
|
||||
mappers?: typeof TEST_MAPPERS | Readonly<Record<string, typeof TEST_MAPPER>>;
|
||||
|
||||
Reference in New Issue
Block a user