fix(server): re-arm pending state timeouts at boot and on creation (ARN-203) - #393
fix(server): re-arm pending state timeouts at boot and on creation (ARN-203)#393rita-aga wants to merge 4 commits into
Conversation
…uts (ARN-203) RED: hard-kill gen-A runtime so boot resume alone must fire timers; overdue case; creation into timed Open without dispatch.
…RN-203) Boot resume sweep after index population, creation-time arm for timed initial states, shared spawn path, bump_if_zero CAS. ADR-0170.
Live local e2e (Definition of Done)HEAD: ARN-203 is a restart/boot timer arming fix. Proof is the hard-kill generation-A runtime DST suite (same boot sequence as ResultPASS: pending timeout fires after process-equivalent hard kill without dispatch; overdue fires at boot; creation into timed Open arms without dispatch. |
…t tests Address independent review Important findings: assert Open after boot, cap overdue wait below full budget, unit-lock bump_if_zero idempotency.
Independent review (HEAD
|
|
@greptile review |
ARENA SHIPPABLE · Grok · 2026-07-14 11:50 PDTPR: #393 Checklist
FixBoot resume sweep + creation arm for timed initial states; remaining budget / overdue zero-delay. |
ARENA SHIPPABLE · Grok · 2026-07-14 11:57 PDTPR: #393 Checklist
Summarystate timeout boot resume |
|
@greptile review |
Summary
Fixes ARN-203: pending
[[state_timeout]]timers are in-memory only and ADR-0056 re-arm only runs on dispatch. Entities sitting in a timed state across restart (or created into a timed initial state) never fireon_timeout.Fix
resume_pending_state_timeouts)spawn_state_timeout_timer+bump_if_zeroCASTests
RED→GREEN:
crates/temper-server/tests/state_timeout_restart.rs(3/3 green locally)Notes
Linear
ARN-203
Greptile Summary
This PR fixes ARN-203 by ensuring
[[state_timeout]]timers survive server restarts and fire correctly for entities created into a timed initial state — two gaps left by the original in-memory-only design. The fix introduces a boot resume sweep (resume_pending_state_timeouts) triggered on both tenant boot paths, a creation-time arm inget_or_create_tenant_entity, a sharedspawn_state_timeout_timerhelper, and abump_if_zeroCAS onStateTimeoutTrackerto guard against double-arming across all three paths.Duration::ZEROfor overdue entities), reusing the existingcompute_state_clock_reset_tsclock reconstruction.arm_untracked_state_timeoutsimmediately afterget_or_create_tenant_entityreturns, making the call idempotent viabump_if_zeroso repeated get-or-create calls do not stack timers.BTreeMapreplacesHashMapinStateTimeoutTrackerto satisfy the DST deterministic-iteration requirement for sim-visible crates.Confidence Score: 4/5
Safe to merge once the boot-sweep silent-failure path is addressed; the core CAS logic, shared timer spawn, and all three integration tests are correct.
The boot resume sweep in
resume_pending_state_timeoutssilently swallows entity hydration errors (continuewith no log), so a partial sweep leaves orphaned timers with no operator signal — this was flagged as a previous outside-diff comment and remains unaddressed. All other logic —bump_if_zeroCAS correctness,hydration_delayextraction fidelity,BTreeMapDST compliance,spawn_timer_taskwrapper, and the creation-time arm — is sound.crates/temper-server/src/state/dispatch/state_timeouts.rs — the entity hydration error handling in the boot sweep loop
Important Files Changed
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Boot as Boot Path participant EO as entity_ops participant ST as state_timeouts participant Tracker as StateTimeoutTracker participant Store as EventStore Boot->>EO: populate_index_from_store(tenant) EO->>EO: index populated EO->>ST: spawn_timer_task(resume_pending_state_timeouts) Note over ST: runs in background ST->>ST: collect timed_types from registry loop for each entity_id in index ST->>Tracker: "current(&key) != 0?" alt already armed Tracker-->>ST: "seq > 0, skip" else not armed ST->>Store: get_tenant_entity_state() Store-->>ST: EntityResponse ST->>Tracker: "bump_if_zero(&key)" alt CAS wins (seq was 0) Tracker-->>ST: Some(1) ST->>ST: hydration_delay (remaining budget) ST->>ST: spawn_state_timeout_timer_with_seq else dispatch already armed Tracker-->>ST: None, break end end end Note over EO,ST: Creation path (ARN-203) EO->>EO: get_or_create_tenant_entity(...) EO->>ST: arm_untracked_state_timeouts(response) ST->>Tracker: "bump_if_zero(&key)" alt no live timer Tracker-->>ST: Some(1), arm timer else timer exists Tracker-->>ST: None, no-op end Note over ST,Tracker: Dispatch path (existing ADR-0056) ST->>Tracker: "bump(&key), always increment" ST->>ST: spawn_state_timeout_timer_with_seq%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant Boot as Boot Path participant EO as entity_ops participant ST as state_timeouts participant Tracker as StateTimeoutTracker participant Store as EventStore Boot->>EO: populate_index_from_store(tenant) EO->>EO: index populated EO->>ST: spawn_timer_task(resume_pending_state_timeouts) Note over ST: runs in background ST->>ST: collect timed_types from registry loop for each entity_id in index ST->>Tracker: "current(&key) != 0?" alt already armed Tracker-->>ST: "seq > 0, skip" else not armed ST->>Store: get_tenant_entity_state() Store-->>ST: EntityResponse ST->>Tracker: "bump_if_zero(&key)" alt CAS wins (seq was 0) Tracker-->>ST: Some(1) ST->>ST: hydration_delay (remaining budget) ST->>ST: spawn_state_timeout_timer_with_seq else dispatch already armed Tracker-->>ST: None, break end end end Note over EO,ST: Creation path (ARN-203) EO->>EO: get_or_create_tenant_entity(...) EO->>ST: arm_untracked_state_timeouts(response) ST->>Tracker: "bump_if_zero(&key)" alt no live timer Tracker-->>ST: Some(1), arm timer else timer exists Tracker-->>ST: None, no-op end Note over ST,Tracker: Dispatch path (existing ADR-0056) ST->>Tracker: "bump(&key), always increment" ST->>ST: spawn_state_timeout_timer_with_seqComments Outside Diff (1)
crates/temper-server/src/state/dispatch/state_timeouts.rs, line 222-227 (link)When
get_tenant_entity_statefails for an individual entity during the boot resume (transient store error, actor-system issue, etc.), the error is silently swallowed withcontinueand no warning is logged. The entity's timer is simply never armed, which meanson_timeoutnever fires for it — and the summary log only reportsarmed N timers, giving no signal that any entities were skipped. AGENTS.md / CLAUDE.md TigerStyle rule: "No silent failures: Every error path must be logged or propagated." The fix is to add atracing::warn!with the entity key and the error before thecontinueso operators can detect partial sweeps from Datadog.Prompt To Fix With AI
Reviews (2): Last reviewed commit: "style(server): rustfmt ARN-203 tests; ma..." | Re-trigger Greptile
Context used: