c.destroy() does not destroy an actor with a db when state changed in the same action (transaction_closed on shutdown state save)
I maintainer di solito rispondono entro 1 giorno
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 48/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- node.js, rust, sqlite, typescript
Direzione di ricerca
The report includes a TypeScript/Vitest reproduction but names no Rust source files or tests. Start by running the reproduction and tracing the actor shutdown path from the reported DestroyGrace and Destroying transitions to serialize_state_save and the SQLite VFS close. Done when the state-change-then-destroy case reliably destroys the actor without a write to a closed database; compare it with the clean-state destroy case.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Description
c.destroy() does not destroy an actor that has a db when c.state changed in the same action and has not been saved yet.
During the destroy, the actor's SQLite VFS closes in the grace phase. After that, the Destroying phase saves the pending state deltas (serialize_state_save) to the closed database. The write fails with transaction_closed, the stop ends as Stopped{code: Error}, and the actor stays alive with the same actor id. With the crash-as-sleep change, the next request wakes the same actor as generation 2.
The same actor with no state change before c.destroy() is destroyed correctly.
Environment
- rivetkit: 2.3.25 (also 2.3.15, 2.3.16, 2.3.17, 2.3.18, 2.3.21). Passes on 2.3.10 and 2.3.11.
- Engine:
rivetdev/engine:2.3.25in Docker onlocalhost:6420, withRIVET__AUTH__ADMIN_TOKENandRIVET__AUTH__INSECURE_ALLOW_UNAUTHENTICATED=true RIVET_RUN_SERVICES=0- Node 22.23.1, macOS arm64, Vitest 3
Reproduction
import { randomUUID } from "node:crypto";
import { actor, setup } from "rivetkit";
import { db } from "rivetkit/db";
import { setupTest } from "rivetkit/test";
import { expect, it } from "vitest";
const counter = actor({
createState: () => ({ count: 0 }),
db: db({ onMigrate: (d) => d.execute("CREATE TABLE IF NOT EXISTS t (v TEXT)") }),
actions: {
ping: () => true,
destroy: (c) => {
c.destroy();
},
bumpThenDestroy: (c) => {
c.state.count += 1;
c.destroy();
},
},
});
const registry = setup({ use: { counter }, envoy: { poolName: `repro-${randomUUID()}` } });
async function destroyedAfter(t: any, action: "destroy" | "bumpThenDestroy") {
const { client } = await setupTest(t, registry);
const key = [randomUUID()];
const handle = client.counter.getOrCreate(key);
await handle.ping();
const before = await handle.resolve();
await handle[action]();
await new Promise((r) => setTimeout(r, 1500));
const getFails = await client.counter.get(key).resolve().then(() => false, () => true);
const after = await client.counter.getOrCreate(key).resolve();
return { getFails, newActorId: after !== before };
}
it("destroy with clean state destroys the actor", async (t) => {
expect(await destroyedAfter(t, "destroy")).toEqual({ getFails: true, newActorId: true });
});
it("destroy right after a state change destroys the actor", async (t) => {
expect(await destroyedAfter(t, "bumpThenDestroy")).toEqual({ getFails: true, newActorId: true });
});
Result on 2.3.25: the first test passes. The second test fails every run:
AssertionError: expected { getFails: false, newActorId: false } to deeply equal { getFails: true, newActorId: true }
Logs (debug, shortened)
actor lifecycle transition old=Started new=DestroyGrace
invoking napi TSF callback kind=onDestroy
sqlite vfs close summary generation=Some(1) ... commit_count=27
napi run handler exited cleanly
actor lifecycle transition old=DestroyGrace new=Destroying
actor event enqueued operation=shutdown_serialize_state event=serialize_state_save
applying actor state deltas delta_count=1 delta_bytes=0 state_revision=2 save_request_revision=3
sqlite operation failed backend=LocalNative operation=execute_batch sql=<batch> binding_count=2 statement_count=2 code=transaction_closed error_message="SQLite transaction coordinator is closed."
actor stop had both shutdown and join failures; only the shutdown error is returned shutdown_error="transaction_closed: SQLite transaction coordinator is closed." discarded_join_error="actor task failed"
actor stop failed after asynchronous completion handoff
EventActorStateUpdate{state: Stopped{code: Error, message: "transaction_closed: SQLite transaction coordinator is closed."}}
Bisect (same engine 2.3.25, only the rivetkit version changes)
| rivetkit | Result |
|---|---|
| 2.3.10, 2.3.11 | both pass |
| 2.3.12, 2.3.13 | both tests time out (perhaps not compatible with engine 2.3.25) |
| 2.3.14 | did not install from npm |
| 2.3.15 to 2.3.25 | state-change case fails |
Expected
The pending state save happens before the database closes (or it is skipped on destroy), and the actor is destroyed.
Impact
Any actor that sets state and then self-destroys in the same action is never destroyed. We found it in two actors: a tool approval actor that self-destroys from a scheduled action, and a loop actor that resets through c.destroy().
Possibly related: #5554, #5555 (transaction_closed after the SQLite coordinator closes).
- Lingua principale
- Rust
- Stelle
- 6.5k
- Fork
- 291
- Merge medio
- 14h 38m
- PR unite (30g)
- 38
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
-
rivetkit 3.0: HTTP 400 overloaded on node pool upsert is treated as fatal and exits the processAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
rivet-dev/rivet#5933 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
effect: action on a missing actor instance fails as internal_error instead of actor.abortedForse già presa @kunalgoesbyken l’ha presa 4 giorni fa. Aperta
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 26 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 37 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 40 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
Tutte le issue di rivet-dev/rivet
Issue simili
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
swc-project/swc-next#1165 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 73/100
Alive24/shea-symphony#574 ·
-
bug tests Windows
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
documentation good first issue help wanted
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
dev-five-git/devup-ui#879 ·
I maintainer di solito rispondono entro 11 giorni
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 77/100
farion1231/cc-switch#9038 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno