Existing actor stays unreachable (no_envoys, sliding reschedule) after runner restart if woken while no envoy is connected — Wake ignored in Reallocating (2.3.27)
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 35/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- rust
- Ambito
- backend, distributed-systems
Direzione di ricerca
Read engine/packages/pegboard/src/workflows/actor2/mod.rs, especially the Reallocating transition and Main::Wake handling, then inspect the retry logic in runtime.rs and the threshold in config/pegboard.rs. Reproduce the runner restart with repeated wake requests and compare it with the quiet control. Done when an existing actor can reallocate after an envoy returns despite continuing wake traffic, with regression coverage for both cases.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Disclosure: this report and the reproduction behind it were produced by AI agents (coding agents running a validation campaign for a project built on Rivet), reviewed by an AI orchestrator, and filed with the project owner's approval. We've tried to separate what we observed from what we inferred from source. Apologies for anything we got wrong. Happy to rerun anything you'd like.
Summary
On Rivet Engine 2.3.27 in serverful (envoy) mode, if a request wakes an existing actor while no envoy is connected, the actor fails allocation with no_envoys. It then stays unreachable after the runner comes back, as long as requests keep arriving. Each new request pushes reschedule_ts further out and is not served. A fresh actor key on the same runner works immediately. The stuck actor recovers right away after POST /actors/{id}/sleep plus one request, with its durable state intact.
In our original incident, a runner restart under normal client traffic (clients reconnecting and pinging) made the actor unavailable for about 318 s. That's consistent with the default actor_retry_duration_threshold of 5 min, after which the actor goes to Sleeping and the next wake succeeds.
Environment
rivetdev/engine:2.3.27(Docker), single node, local filesystem/native SQLite storage (backend=LocalNative)rivetkit2.3.27, serverful / native envoy mode, Node 24 runner- One runner, runner selector
default, namespacedefault, actor crash policysleep - Isolated engine with its own data directory, with no other traffic
Reproduction (confirmed)
Times are relative to the runner kill (t = 0).
- Start the engine and one runner. Create an actor (
getOrCreateby key) and write some durable state to it. - SIGKILL the runner and keep it down.
- Poll the actor record (
GET /actors?namespace=default&actor_ids=<id>) only, sending no requests to the actor, until it showserror = envoy_connection_lost. That happened at t ≈ 15.3 s, withsleep_tsset andreschedule_ts = null. - While the runner is still down (t ≈ 15.3 s), start sending requests to that existing actor through the gateway (
POST /gateway/<actor>/action/ping?rvt-method=getOrCreate&rvt-key=<key>&rvt-runner=default&rvt-crash-policy=sleep). Send them one at a time with an 8 s client timeout, so requests arrive at t ≈ 15.3 s, 23.6 s and 31.9 s.- About 3 s later the record shows
error = no_envoysandreschedule_ts≈ t + 19.3 s.
- About 3 s later the record shows
- Start the runner again (start issued at t ≈ 18.6 s) while requests to the existing key continue.
- Control: a request to a fresh key at t ≈ 22.6 s → HTTP 200 in 77 ms. The runner is healthy.
- Existing key: it stays at
error = no_envoys. Across samples,reschedule_tsmoved to t + 35.3 s, then t + 55.6 s (first seen right after the t ≈ 23.6 s request arrived), then t + 95.9 s (right after the t ≈ 31.9 s request). Three requests each timed out at our 8 s client timeout, with zero successes. POST /actors/{id}/sleep?namespace=default→ HTTP 200 in 2 ms. One request afterwards → HTTP 200 in 62 ms, the error clears andconnectable_tsis set. The actor's durable state was unchanged: its single stored ledger row, pending count and cursor/epoch all matched pre-fault values.
Quiet control (confirmed)
The same steps, except no requests are sent to the actor until after the runner is back. We SIGKILLed, waited about 24 s until envoy_connection_lost, restarted the runner, then sent the first request. The actor never showed no_envoys. The first request succeeded in 64 ms with the same durable state.
So the trigger is a wake while zero envoys are available, followed by continued wake traffic. Losing the runner alone doesn't trigger it.
Observed actor-record transitions
connectable
→ error=envoy_connection_lost, sleep_ts set, reschedule_ts=null (runner killed)
→ error=no_envoys, reschedule_ts set (first wake while no runner)
→ error=no_envoys, reschedule_ts keeps sliding later (runner back, requests continuing)
→ error cleared, connectable_ts set (POST /sleep, then one request)
The public record doesn't expose the workflow transition or generation. The Reallocating interpretation below comes from source, not from a log line we observed.
Expected vs actual
- Expected: once an envoy is registered again, the next request (or the next retry) allocates the existing actor, as it does for a fresh key and in the quiet control.
- Actual: the existing actor stays unreachable while traffic continues, for up to
actor_retry_duration_threshold(5 min by default). Gateway requests time out instead of being served.
Likely mechanism (from source, v2.3.27; inferred, not instrumented)
engine/packages/pegboard/src/workflows/actor2/mod.rs:
- L604–631,
Transition::Reallocating: each loop callsget_next_retry_ts(), which incrementsretry_count(inruntime.rs). It then doeslisten_n_until(next_retry_ts).reschedule_actoris only attempted if no signal arrived (if signals.is_empty()). Any incoming signal therefore cancels that retry attempt, and the next loop has a longer backoff. - L913–931,
Main::WakeduringReallocatingfalls into_ =>, logs"cannot wake actor that is not sleeping"and does nothing. - L607–613: the actor goes to
Sleepingonly oncelast_retry_ts > since_ts + actor_retry_duration_threshold(config/pegboard.rsL251–253, default5 * 60 * 1000). - L965–968,
Main::SleepduringReallocating→Sleeping. A following Wake then callsreschedule_actor, which matches the sleep→wake workaround we observed.
So with a steady stream of wakes arriving faster than the growing backoff, the actor never attempts reallocation until the 300 s threshold.
Not yet confirmed by us: we haven't run a variant where requests stop entirely after the runner returns, to check that the actor then recovers at reschedule_ts by itself.
Hypotheses we checked and ruled out as the trigger
- SQLite close at shutdown (#5554 / PR #5860): we see
failed to sync scheduled actor alarm … sqlite transaction coordinator is closed (transaction_closed)andSELECT MIN(trigger_at) FROM _rivet_schedule_events … transaction_closedwhen the runner stops. Fresh actors that stayed healthy logged the same error, and the quiet control recovers despite it. Our app doesn't schedule alarms, and_rivet_schedule_eventswas empty when we inspected it. It's real, but not the trigger here. - #5859 (dropped event dispatch): its
actor event processor failedandfailed to ingest event/channel closedsignatures are absent from our logs. - #5738 (drain eviction / VFS loss): its signatures are absent, and it's a different version and setup.
- #5484 (protocol mismatch): it doesn't fit, because fresh actors work on the same runner.
- #5835 (non-hibernating WebSocket default) is already included in 2.3.27.
We found no released fix after 2.3.27, and main appears to have the same Reallocating / Wake handling.
Workaround we're using
A supervisor in our runner, after envoy registration, probes each actor with a short timeout. On a timeout or no_envoys with a future reschedule_ts, it calls POST /actors/{id}/sleep then sends one normal request. It's idempotent, with bounded retries and backoff. This preserves actor storage in our tests.
Possible fixes (suggestions only)
- In
Reallocating, treatMain::Wakeas "retry now", i.e. attemptreschedule_actorimmediately. Alternatively, don't let an interrupting signal skip the pending retry or advance the backoff. - And/or re-trigger reallocation for actors in
Reallocatingwhen a matching envoy registers.
We can share the repro script, with endpoints and tokens removed, if that helps.
- Lingua principale
- Rust
- Stelle
- 6.3k
- Fork
- 280
- Merge medio
- 22h 56m
- PR unite (30g)
- 41
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Nessuna guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di rivet-dev/rivet
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
rivet-dev/rivet#5891 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Rust client drops connection-level errors when actionId is null (JSON/CBOR)Forse già presa @DibbayajyotiRoy l’ha presa 21 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
rivet-dev/rivet#5819 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Queue docs say completable messages are removed on receive, but they stay stored until complete()Forse già presa @Saisharathchandranandnetha l’ha presa 32 giorni fa. Aperta
Difficoltà 1/5 1-3 ore Idoneità per principianti 91/100
rivet-dev/rivet#5762 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Serverless listener forwards /start to the application when basePath is "/"Forse già presa @Saisharathchandranandnetha l’ha presa 34 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 90/100
rivet-dev/rivet#5755 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 35/100
rivet-dev/rivet#5908 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di rivet-dev/rivet
Issue simili
-
checkpoint/verify/rebuild_indexes on a thread holding a WriteTransaction deadlock instead of INT-001Apertabug known-issue
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
project-minigraf/minigraf#734 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 88/100
I maintainer di solito rispondono entro 4 giorni
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
bitcoindevkit/electrum_streaming_client#57 ·
I maintainer di solito rispondono entro 2 giorni