fix(memory): restore sqlite busy_timeout on reopen (#39183, thanks @MumuTW)

Co-authored-by: MumuTW <clothl47364@gmail.com>
This commit is contained in:
Peter Steinberger 2026-03-07 22:17:55 +00:00
parent 733f7af92b
commit 3a2fdc5136
5 changed files with 43 additions and 3 deletions

View File

@ -287,6 +287,7 @@ Docs: https://docs.openclaw.ai
- Daemon/Windows schtasks status normalization: derive runtime state from locale-neutral numeric `Last Run Result` codes only (without language string matching) and surface unknown when numeric result data is unavailable, preventing locale-specific misclassification drift. (#39153) Thanks @scoootscooob.
- Telegram/polling conflict recovery: reset the polling `webhookCleared` latch on `getUpdates` 409 conflicts so webhook cleanup re-runs on restart cycles and polling avoids infinite conflict loops. (#39205) Thanks @amittell.
- Heartbeat/requests-in-flight scheduling: stop advancing `nextDueMs` and avoid immediate `scheduleNext()` timer overrides on requests-in-flight skips, so wake-layer retry cooldowns are honored and heartbeat cadence no longer drifts under sustained contention. (#39182) Thanks @MumuTW.
- Memory/SQLite contention resilience: re-apply `PRAGMA busy_timeout` on every sync-store and QMD connection open so process restarts/reopens no longer revert to immediate `SQLITE_BUSY` failures under lock contention. (#39183) Thanks @MumuTW.
## 2026.3.2

View File

@ -258,7 +258,12 @@ export abstract class MemoryManagerSyncOps {
const dir = path.dirname(dbPath);
ensureDir(dir);
const { DatabaseSync } = requireNodeSqlite();
return new DatabaseSync(dbPath, { allowExtension: this.settings.store.vector.enabled });
const db = new DatabaseSync(dbPath, { allowExtension: this.settings.store.vector.enabled });
// busy_timeout is per-connection and resets to 0 on restart.
// Set it on every open so concurrent processes retry instead of
// failing immediately with SQLITE_BUSY.
db.exec("PRAGMA busy_timeout = 5000");
return db;
}
private seedEmbeddingCache(sourceDb: DatabaseSync): void {

View File

@ -109,4 +109,14 @@ describe("memory manager readonly recovery", () => {
expect(runSyncSpy).toHaveBeenCalledTimes(1);
expect(openDatabaseSpy).toHaveBeenCalledTimes(0);
});
it("sets busy_timeout on memory sqlite connections", async () => {
const currentManager = await createManager();
const db = (currentManager as unknown as { db: DatabaseSync }).db;
const row = db.prepare("PRAGMA busy_timeout").get() as
| { busy_timeout?: number; timeout?: number }
| undefined;
const busyTimeout = row?.busy_timeout ?? row?.timeout;
expect(busyTimeout).toBe(5000);
});
});

View File

@ -2,6 +2,7 @@ import { EventEmitter } from "node:events";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { DatabaseSync } from "node:sqlite";
import type { Mock } from "vitest";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@ -88,6 +89,7 @@ import { spawn as mockedSpawn } from "node:child_process";
import type { OpenClawConfig } from "../config/config.js";
import { resolveMemoryBackendConfig } from "./backend-config.js";
import { QmdMemoryManager } from "./qmd-manager.js";
import { requireNodeSqlite } from "./sqlite.js";
const spawnMock = mockedSpawn as unknown as Mock;
@ -2644,6 +2646,24 @@ describe("QmdMemoryManager", () => {
).rejects.toThrow(/qmd query returned invalid JSON/);
await manager.close();
});
it("sets busy_timeout on qmd sqlite connections", async () => {
const { manager } = await createManager();
const indexPath = (manager as unknown as { indexPath: string }).indexPath;
await fs.mkdir(path.dirname(indexPath), { recursive: true });
const { DatabaseSync } = requireNodeSqlite();
const seedDb = new DatabaseSync(indexPath);
seedDb.close();
const db = (manager as unknown as { ensureDb: () => DatabaseSync }).ensureDb();
const row = db.prepare("PRAGMA busy_timeout").get() as
| { busy_timeout?: number; timeout?: number }
| undefined;
const busyTimeout = row?.busy_timeout ?? row?.timeout;
expect(busyTimeout).toBe(1000);
await manager.close();
});
describe("model cache symlink", () => {
let defaultModelsDir: string;
let customModelsDir: string;

View File

@ -1556,8 +1556,12 @@ export class QmdMemoryManager implements MemorySearchManager {
}
const { DatabaseSync } = requireNodeSqlite();
this.db = new DatabaseSync(this.indexPath, { readOnly: true });
// Keep QMD recall responsive when the updater holds a write lock.
this.db.exec("PRAGMA busy_timeout = 1");
// busy_timeout is per-connection; set it on every open so concurrent
// processes retry instead of failing immediately with SQLITE_BUSY.
// Use a lower value than the write path (5 s) because this read-only
// connection runs synchronous queries on the main thread via DatabaseSync.
// In WAL mode readers rarely block, so 1 s is a safe upper bound.
this.db.exec("PRAGMA busy_timeout = 1000");
return this.db;
}