fix: keep state database handles isolated

This commit is contained in:
Peter Steinberger
2026-05-16 08:13:33 +01:00
parent 840e6d08fd
commit 1b78a8015a
2 changed files with 40 additions and 17 deletions

View File

@@ -110,6 +110,27 @@ describe("openclaw state database", () => {
expect(fs.existsSync(databasePath)).toBe(true);
});
it("keeps cached handles open when another state path is opened", () => {
const firstPath = path.join(
createTempStateDir(),
"state",
`first-${process.pid}-${Date.now()}.sqlite`,
);
const secondPath = path.join(
createTempStateDir(),
"state",
`second-${process.pid}-${Date.now()}.sqlite`,
);
const first = openOpenClawStateDatabase({ path: firstPath });
const second = openOpenClawStateDatabase({ path: secondPath });
expect(first.db.isOpen).toBe(true);
expect(second.db.isOpen).toBe(true);
expect(openOpenClawStateDatabase({ path: firstPath })).toBe(first);
expect(readSqliteNumberPragma(first.db, "user_version")).toBe(1);
});
it("uses savepoints for nested write transaction rollback", () => {
const stateDir = createTempStateDir();
const options = { env: { OPENCLAW_STATE_DIR: stateDir } };

View File

@@ -68,7 +68,7 @@ export type RecordOpenClawStateBackupRunOptions = OpenClawStateDatabaseOptions &
manifest: Record<string, unknown>;
};
let cachedDatabase: OpenClawStateDatabase | null = null;
const cachedDatabases = new Map<string, OpenClawStateDatabase>();
type OpenClawStateMetadataDatabase = Pick<
OpenClawStateKyselyDatabase,
@@ -150,14 +150,14 @@ export function openOpenClawStateDatabase(
): OpenClawStateDatabase {
const env = options.env ?? process.env;
const pathname = resolveDatabasePath(options);
if (cachedDatabase && cachedDatabase.path === pathname) {
return cachedDatabase;
const cached = cachedDatabases.get(pathname);
if (cached?.db.isOpen) {
return cached;
}
if (cachedDatabase) {
cachedDatabase.walMaintenance.close();
clearNodeSqliteKyselyCacheForDatabase(cachedDatabase.db);
cachedDatabase.db.close();
cachedDatabase = null;
if (cached) {
cached.walMaintenance.close();
clearNodeSqliteKyselyCacheForDatabase(cached.db);
cachedDatabases.delete(pathname);
}
ensureOpenClawStatePermissions(pathname, env);
@@ -178,8 +178,9 @@ export function openOpenClawStateDatabase(
throw err;
}
ensureOpenClawStatePermissions(pathname, env);
cachedDatabase = { db, path: pathname, walMaintenance };
return cachedDatabase;
const database = { db, path: pathname, walMaintenance };
cachedDatabases.set(pathname, database);
return database;
}
export function runOpenClawStateWriteTransaction<T>(
@@ -273,17 +274,18 @@ export function recordOpenClawStateBackupRun(options: RecordOpenClawStateBackupR
}
export function closeOpenClawStateDatabase(): void {
if (!cachedDatabase) {
return;
for (const database of cachedDatabases.values()) {
database.walMaintenance.close();
clearNodeSqliteKyselyCacheForDatabase(database.db);
if (database.db.isOpen) {
database.db.close();
}
}
cachedDatabase.walMaintenance.close();
clearNodeSqliteKyselyCacheForDatabase(cachedDatabase.db);
cachedDatabase.db.close();
cachedDatabase = null;
cachedDatabases.clear();
}
export function isOpenClawStateDatabaseOpen(): boolean {
return cachedDatabase?.db.isOpen === true;
return Array.from(cachedDatabases.values()).some((database) => database.db.isOpen);
}
export const closeOpenClawStateDatabaseForTest = closeOpenClawStateDatabase;