From 2e0e6b0b9104c99fda0deb2dbd42aab4d33472c4 Mon Sep 17 00:00:00 2001 From: "hirnaymay (harsh)" Date: Tue, 20 Oct 2026 03:12:53 +0530 Subject: [PATCH] fix(rivetkit): keep actor sqlite open until core finishes destroy shutdown --- .../fixtures/driver-test-suite/destroy.ts | 22 ++++++++ .../driver-test-suite/registry-static.ts | 2 + .../packages/rivetkit/src/registry/native.ts | 1 - .../tests/driver/actor-destroy.test.ts | 51 +++++++++++++++++++ 4 files changed, 75 insertions(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/destroy.ts b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/destroy.ts index c752b17d6b..9e43f2c0a7 100644 --- a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/destroy.ts +++ b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/destroy.ts @@ -1,5 +1,6 @@ // @ts-nocheck import { actor, queue } from "rivetkit"; +import { db } from "@/common/database/mod"; import type { registry } from "./registry-static"; export const destroyObserver = actor({ @@ -88,6 +89,27 @@ export const destroyActor = actor({ }, }); +export const destroyDbActor = actor({ + state: { value: 0 }, + db: db({ + onMigrate: async (db) => { + await db.execute("CREATE TABLE IF NOT EXISTS entries (value TEXT)"); + }, + }), + actions: { + getValue: (c) => { + return c.state.value; + }, + destroy: (c) => { + c.destroy(); + }, + setValueThenDestroy: (c, newValue: number) => { + c.state.value = newValue; + c.destroy(); + }, + }, +}); + export const destroyAbortSignalActor = actor({ state: { abortEventCount: 0, diff --git a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/registry-static.ts b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/registry-static.ts index a352a7cc8f..e88c958f0c 100644 --- a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/registry-static.ts +++ b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/registry-static.ts @@ -58,6 +58,7 @@ import { dbStressActor } from "./db-stress"; import { destroyAbortSignalActor, destroyActor, + destroyDbActor, destroyObserver, } from "./destroy"; import { customTimeoutActor, errorHandlingActor } from "./error-handling"; @@ -311,6 +312,7 @@ export const registry = setup({ destroyActor, destroyObserver, destroyAbortSignalActor, + destroyDbActor, // From hibernation.ts hibernationActor, hibernationSleepWindowActor, diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/native.ts b/rivetkit-typescript/packages/rivetkit/src/registry/native.ts index 0c5c55648e..94b5c249cd 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/native.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/native.ts @@ -4846,7 +4846,6 @@ export function buildNativeFactory( } } finally { resolveNativeDestroy(runtime, ctx); - await actorCtx.closeDatabase(); clearNativeRuntimeState(runtime, ctx); await actorCtx.dispose(); } diff --git a/rivetkit-typescript/packages/rivetkit/tests/driver/actor-destroy.test.ts b/rivetkit-typescript/packages/rivetkit/tests/driver/actor-destroy.test.ts index 39b550a06e..bc314f4291 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/driver/actor-destroy.test.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/driver/actor-destroy.test.ts @@ -433,5 +433,56 @@ describeDriverMatrix("Actor Destroy", (driverTestConfig) => { expect(await handle.getValue()).toBe(0); expect(await handle.resolve()).toBe(await recreated.resolve()); }); + + type DriverTestClient = Awaited< + ReturnType + >["client"]; + + async function startDbActor(client: DriverTestClient, name: string) { + const actorKey = `test-destroy-db-${name}-${crypto.randomUUID()}`; + const handle = client.destroyDbActor.getOrCreate([actorKey]); + expect(await handle.getValue()).toBe(0); + const actorId = await handle.resolve(); + return { actorKey, actorId, handle }; + } + + async function expectDbActorDestroyed( + client: DriverTestClient, + { actorKey, actorId }: { actorKey: string; actorId: string }, + ) { + // Poll because actor lookup only turns into not_found once destroy teardown fully completes. + await vi.waitFor(async () => { + let actorRunning = false; + try { + await client.destroyDbActor.getForId(actorId).getValue(); + actorRunning = true; + } catch (error) { + expectActorNotFound(error); + } + expect(actorRunning, "actor still running").toBeFalsy(); + }); + + const recreated = client.destroyDbActor.getOrCreate([actorKey]); + expect(await recreated.getValue()).toBe(0); + expect(await recreated.resolve()).not.toBe(actorId); + } + + test("actor with a database destroys with clean state", async (c) => { + const { client } = await setupDriverTest(c, driverTestConfig); + const dbActor = await startDbActor(client, "clean-state"); + + await dbActor.handle.destroy(); + + await expectDbActorDestroyed(client, dbActor); + }); + + test("actor with a database destroys after an unsaved state change", async (c) => { + const { client } = await setupDriverTest(c, driverTestConfig); + const dbActor = await startDbActor(client, "unsaved-state"); + + await dbActor.handle.setValueThenDestroy(7); + + await expectDbActorDestroyed(client, dbActor); + }); }); });