fix: release sqlite locks on sync errors

This commit is contained in:
Peter Steinberger
2026-05-16 17:45:52 +01:00
parent 72c0b75c69
commit 2233cd3da4
4 changed files with 65 additions and 13 deletions

View File

@@ -458,6 +458,38 @@ describe("SQLite session transcript store", () => {
]);
});
it("preserves an explicit transcript database path when listing by agent", () => {
const stateDir = createTempDir();
const env = { OPENCLAW_STATE_DIR: stateDir };
const customPath = path.join(stateDir, "custom-agent.sqlite");
appendSqliteSessionTranscriptEvent({
env,
path: customPath,
agentId: "worker-1",
sessionId: "session-1",
event: { type: "message", id: "m1" },
now: () => 100,
});
const [scope] = listSqliteSessionTranscripts({
env,
path: customPath,
agentId: "worker-1",
});
expect(scope).toEqual({
agentId: "worker-1",
path: customPath,
sessionId: "session-1",
updatedAt: 100,
eventCount: 1,
});
expect(
scope ? loadSqliteSessionTranscriptEvents(scope).map((entry) => entry.event) : [],
).toEqual([{ type: "message", id: "m1" }]);
});
it("deletes transcript snapshots with the transcript", () => {
const stateDir = createTempDir();
const env = { OPENCLAW_STATE_DIR: stateDir };

View File

@@ -479,7 +479,7 @@ export function listSqliteSessionTranscripts(
? [
{
agentId: normalizeAgentId(options.agentId),
path: undefined,
path: options.path,
},
]
: listOpenClawRegisteredAgentDatabases(options);

View File

@@ -117,6 +117,23 @@ describe("withOpenClawStateLock", () => {
});
});
it("releases the lease when the guarded task throws synchronously", async () => {
await withTempDir({ prefix: "openclaw-state-lock-sync-throw-" }, async (dir) => {
const dbPath = path.join(dir, "state.sqlite");
expect.assertions(2);
await expect(
withOpenClawStateLock("shared", { path: dbPath, retries: FAST_RETRY }, () => {
throw new Error("boom");
}),
).rejects.toThrow("boom");
await expect(
withOpenClawStateLock("shared", { path: dbPath, retries: FAST_RETRY }, async () => "ok"),
).resolves.toBe("ok");
});
});
it("rejects and aborts the guarded task when lease renewal loses ownership", async () => {
await withTempDir({ prefix: "openclaw-state-lock-lost-" }, async (dir) => {
const dbPath = path.join(dir, "state.sqlite");
@@ -135,16 +152,19 @@ describe("withOpenClawStateLock", () => {
});
await entered;
runOpenClawStateWriteTransaction((database) => {
const db = getNodeSqliteKysely<StateLockTestDatabase>(database.db);
executeSqliteQuerySync(
database.db,
db
.deleteFrom("state_leases")
.where("scope", "=", "runtime.lock")
.where("lease_key", "=", "shared"),
);
}, { path: dbPath });
runOpenClawStateWriteTransaction(
(database) => {
const db = getNodeSqliteKysely<StateLockTestDatabase>(database.db);
executeSqliteQuerySync(
database.db,
db
.deleteFrom("state_leases")
.where("scope", "=", "runtime.lock")
.where("lease_key", "=", "shared"),
);
},
{ path: dbPath },
);
await expect(locked).rejects.toThrow("Lost SQLite state lock runtime.lock:shared");
expect(signal.aborted).toBe(true);

View File

@@ -277,9 +277,9 @@ export async function withOpenClawStateLock<T>(
}
}, renewEveryMs);
renewal.unref?.();
const taskPromise = task(abortController.signal);
void taskPromise.catch(() => {});
try {
const taskPromise = Promise.resolve(task(abortController.signal));
void taskPromise.catch(() => {});
return await Promise.race([taskPromise, lostLock]);
} finally {
clearInterval(renewal);