Skip to content

fix(server): re-arm pending state timeouts at boot and on creation (ARN-203) - #393

Draft
rita-aga wants to merge 4 commits into
mainfrom
grok/arn-203-timeout-replay
Draft

fix(server): re-arm pending state timeouts at boot and on creation (ARN-203)#393
rita-aga wants to merge 4 commits into
mainfrom
grok/arn-203-timeout-replay

Conversation

@rita-aga

@rita-aga rita-aga commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

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 fire on_timeout.

Fix

  • Boot resume sweep after entity index population (resume_pending_state_timeouts)
  • Creation-time arm for timed initial states
  • Shared spawn_state_timeout_timer + bump_if_zero CAS
  • ADR-0170

Tests

RED→GREEN: crates/temper-server/tests/state_timeout_restart.rs (3/3 green locally)

  • pending fires after hard-kill restart without dispatch
  • overdue fires at boot
  • creation into timed Open arms without dispatch

Notes

  • Local pre-push full suite thrice thrashing under disk pressure / concurrent agent builds; targeted suite green; CI is the full gate.
  • Merge: nothing (arena)

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 in get_or_create_tenant_entity, a shared spawn_state_timeout_timer helper, and a bump_if_zero CAS on StateTimeoutTracker to guard against double-arming across all three paths.

  • Boot sweep runs in a background task after entity-index population; it hydrates each entity in a timed state with no live timer and arms it with its remaining budget (Duration::ZERO for overdue entities), reusing the existing compute_state_clock_reset_ts clock reconstruction.
  • Creation-time arm calls arm_untracked_state_timeouts immediately after get_or_create_tenant_entity returns, making the call idempotent via bump_if_zero so repeated get-or-create calls do not stack timers.
  • BTreeMap replaces HashMap in StateTimeoutTracker to 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_timeouts silently swallows entity hydration errors (continue with 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_zero CAS correctness, hydration_delay extraction fidelity, BTreeMap DST compliance, spawn_timer_task wrapper, 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

Filename Overview
crates/temper-server/src/state/dispatch/state_timeouts.rs Core of the fix: adds boot resume sweep, creation-time arm, bump_if_zero CAS, shared spawn_state_timeout_timer, and BTreeMap tracker; entity hydration errors during the boot sweep are silently swallowed without a warning log
crates/temper-server/src/state/entity_ops.rs Triggers resume sweep on both tenant boot paths and arms timers at creation time; logic is correct and idempotent via bump_if_zero
crates/temper-server/tests/state_timeout_restart.rs Three new integration tests covering pending/overdue restart and creation-into-timed-state; runtime hard-kill via shutdown_timeout(100ms) correctly simulates a crashed process
docs/adrs/0170-state-timeout-boot-resume.md ADR documents the decision, consequences, DST compliance notes, and alternatives considered; well-structured and complete

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
Loading
%%{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_seq
Loading

Comments Outside Diff (1)

  1. crates/temper-server/src/state/dispatch/state_timeouts.rs, line 222-227 (link)

    P1 Silent entity-hydration failure in boot sweep

    When get_tenant_entity_state fails for an individual entity during the boot resume (transient store error, actor-system issue, etc.), the error is silently swallowed with continue and no warning is logged. The entity's timer is simply never armed, which means on_timeout never fires for it — and the summary log only reports armed 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 a tracing::warn! with the entity key and the error before the continue so operators can detect partial sweeps from Datadog.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: crates/temper-server/src/state/dispatch/state_timeouts.rs
    Line: 222-227
    
    Comment:
    **Silent entity-hydration failure in boot sweep**
    
    When `get_tenant_entity_state` fails for an individual entity during the boot resume (transient store error, actor-system issue, etc.), the error is silently swallowed with `continue` and no warning is logged. The entity's timer is simply never armed, which means `on_timeout` never fires for it — and the summary log only reports `armed 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 a `tracing::warn!` with the entity key and the error before the `continue` so operators can detect partial sweeps from Datadog.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code Fix in Codex Fix in Cursor

Reviews (2): Last reviewed commit: "style(server): rustfmt ARN-203 tests; ma..." | Re-trigger Greptile

Context used:

  • Context used - CLAUDE.md (source)
  • Context used - AGENTS.md (source)

rita-aga added 2 commits July 14, 2026 09:05
…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.
@rita-aga

Copy link
Copy Markdown
Collaborator Author

Live local e2e (Definition of Done)

HEAD: 000fb3f5

ARN-203 is a restart/boot timer arming fix. Proof is the hard-kill generation-A runtime DST suite (same boot sequence as temper serve index populate + resume sweep):

cd temper-worktrees/grok-arn-203-timeout-replay
export CARGO_TARGET_DIR=$HOME/.cache/t203
cargo test -p temper-server --test state_timeout_restart -- --nocapture

Result

    Finished `test` profile [unoptimized + debuginfo] target(s) in 6.46s
     Running tests/state_timeout_restart.rs (/Users/seshendranalla/.cache/t203/debug/deps/state_timeout_restart-cbbbbcd16a22f4b7)

running 3 tests
test state_timeout_fires_for_entity_created_into_timed_state ... ok
test overdue_state_timeout_fires_after_restart ... ok
test pending_state_timeout_fires_after_restart ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 15.95s

PASS: 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.
@rita-aga

Copy link
Copy Markdown
Collaborator Author

Independent review (HEAD cebb022c)

Root-cause fix for ARN-203: boot resume + creation arm share one spawn path; bump_if_zero CAS; remaining-budget/overdue via ADR-0056 clock; hard-kill restart tests.

Findings addressed after first PASS

Important test tightness (overdue deadline, Open pin after boot, bump_if_zero units) fixed in follow-up commit.

Critical / Important remaining

None blocking.

Verdict: PASS

@rita-aga

Copy link
Copy Markdown
Collaborator Author

@greptile review

@rita-aga

Copy link
Copy Markdown
Collaborator Author

ARENA SHIPPABLE · Grok · 2026-07-14 11:50 PDT

PR: #393
HEAD: cebb022cb1a5 · branch grok/arn-203-timeout-replay
Linear: ARN-203 · ADR: 0170
Merge: nothing (arena rules)

Checklist

Gate Evidence
RED→GREEN restart tests RED commit then GREEN fix
Independent review Verdict: PASS (Important test findings fixed)
Greptile requested after PASS
Live e2e hard-kill DST suite 3/3 (posted)
Local targeted tests state_timeout_restart 3/3 + bump_if_zero 2/2
CI running on head

Fix

Boot resume sweep + creation arm for timed initial states; remaining budget / overdue zero-delay.

Comment thread crates/temper-server/tests/state_timeout_restart.rs
@rita-aga

Copy link
Copy Markdown
Collaborator Author

ARENA SHIPPABLE · Grok · 2026-07-14 11:57 PDT

PR: #393
HEAD: cebb022cb1a5 · branch grok/arn-203-timeout-replay
Linear: ARN-203 · ADR: 0170
Merge: nothing (arena rules)

Checklist

Gate Evidence
RED→GREEN on branch history
Independent same-model review Verdict: PASS posted on PR
Greptile requested after PASS
Local tests targeted suite green before push
CI GitHub Actions on head

Summary

state timeout boot resume

@rita-aga

Copy link
Copy Markdown
Collaborator Author

@greptile review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant