Repository navigation
deleteAllDurableObjects() leaves storage of objects aborted by abortAllDurableObjects() #8024
Description
deleteAllDurableObjects() from workerd:unsafe does not delete the storage of a Durable Object that is not live at the time of the call. After abortAllDurableObjects(), no object is live, so a following deleteAllDurableObjects() deletes nothing and the next request to an object reopens its old SQLite file.
This surfaced as cloudflare/workers-sdk#16510: reset() from cloudflare:test (@cloudflare/vitest-plugin) is a direct call to deleteAllDurableObjects(), and miniflare runs each local D1 database as a Durable Object in a namespace without preventEviction, so a reset() right after abortAllDurableObjects() leaves D1 as it was.
Where
ActorNamespace::delete_all in src/workerd/server/server/actor.rs (same code in v1.20261023.1, v1.20261027.1 and main):
for container in self.actors.borrow().values() {
container.reset_storage();
}
self.abort_all(reason);
match self.storage.get() {
Some(storage) => Ok(ffi::actor_storage_delete_all(storage)?),
None => Ok(()),
}reset_storage() runs only for containers in self.actors. abort_all() empties that map (std::mem::take), and actor_storage_delete_all() in worker-factory-actor.c++ only calls alarmScheduler->deleteAll(). So storage of any object that is not in the map (aborted earlier) is left on disk. The C++ server in v1.20260918.1 had the same shape in ActorNamespace::deleteAll (server.c++): resetStorage() for each entry of actors, then abortAll(), then the alarm scheduler.
unsafe.h describes deleteAllDurableObjects() as "Like abortAllDurableObjects(), but also deletes storage and cancels alarms so DOs restart with clean state", and test_delete_all_durable_objects in unsafe-module-test.js only covers an object that is live when the delete runs.
Repro
With @cloudflare/vitest-plugin 1.4.8 (miniflare 5.20261023.0-beta, workerd 1.20261023.1), vitest 4.1.11, any Worker with one D1 binding DB (the project in cloudflare/workers-sdk#16510):
import { env } from "cloudflare:workers";
import { abortAllDurableObjects, reset } from "cloudflare:test";
import { expect, test } from "vitest";
const db = (env as unknown as { DB: D1Database }).DB;
async function tables(): Promise<string[]> {
const { results } = await db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE '!_cf!_%' ESCAPE '!'").all<{ name: string }>();
return results.map(row => row.name);
}
test("reset() right after abortAllDurableObjects() leaves D1 as it was", async () => {
await db.exec("CREATE TABLE a (x INTEGER)");
await abortAllDurableObjects();
await reset();
const afterFirst = await tables();
await reset();
const afterSecond = await tables();
console.log(JSON.stringify({ afterFirst, afterSecond }));
expect(afterFirst).toEqual([]);
});Output: {"afterFirst":["a"],"afterSecond":[]}. The read between the two resets brings the D1 object back into the map, so the second reset deletes it. No query needs to be in flight at the abort.
Possible fix
In delete_all, also delete the on-disk SQLite files (and their -wal/-shm and facet files) of objects in the namespace directory that are not live, after the live ones are reset and aborted. A test in unsafe-module-test.js: write to an object, abortAllDurableObjects(), deleteAllDurableObjects(), then expect the value to be gone on a fresh stub. I have not built workerd, so this is a proposal, not a tested patch.
Activity