mirror of https://github.com/openclaw/openclaw.git
fix(memory): restore sqlite busy_timeout on reopen (#39183, thanks @MumuTW)
Co-authored-by: MumuTW <clothl47364@gmail.com>
This commit is contained in:
parent
733f7af92b
commit
3a2fdc5136
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue