fix(memory): set PRAGMA busy_timeout on every SQLite connection open#39183
fix(memory): set PRAGMA busy_timeout on every SQLite connection open#39183MumuTW wants to merge 2 commits intoopenclaw:mainfrom
Conversation
busy_timeout is per-connection and resets to 0 when the process restarts. With 10+ concurrent processes (gateway, cron agents, subagents) hitting the same SQLite file, a zero timeout means any concurrent write instantly fails with SQLITE_BUSY instead of retrying. Set busy_timeout=5000 in both connection sites: - manager-sync-ops.ts openDatabaseAtPath (write connections) - qmd-manager.ts ensureDb (read-only connection, raised from 1 ms) Closes openclaw#39176
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🟡 Main-thread blocking DoS risk from 1s SQLite busy_timeout on synchronous QMD read connection
Description
Impact:
Vulnerable change: this.db = new DatabaseSync(this.indexPath, { readOnly: true });
this.db.exec("PRAGMA busy_timeout = 1000");RecommendationAvoid long blocking waits on the main thread for the QMD read-only recall path. Options:
this.db = new DatabaseSync(this.indexPath, { readOnly: true });
this.db.exec("PRAGMA busy_timeout = 1"); // or 0
2. 🟡 Node.js event-loop blocking DoS risk from SQLite DatabaseSync busy_timeout=5000 on request-path connection
DescriptionThe builtin memory search/index uses Concrete call chain to the affected connection:
Why this can be abused into a DoS:
Vulnerable code (changed): const db = new DatabaseSync(dbPath, { allowExtension: this.settings.store.vector.enabled });
db.exec("PRAGMA busy_timeout = 5000");Impact: a client that can trigger RecommendationAvoid long Options (choose one):
// Request-path (search): fail fast to avoid event-loop stalls
const readDb = new DatabaseSync(dbPath, { readOnly: true, allowExtension: false });
readDb.exec("PRAGMA busy_timeout = 50");
// Background sync/indexing: can tolerate waiting
const writeDb = new DatabaseSync(dbPath, { allowExtension: this.settings.store.vector.enabled });
writeDb.exec("PRAGMA busy_timeout = 5000");
Also consider enabling WAL mode (if compatible) to reduce reader/writer blocking and lock convoying. Analyzed PR: #39183 at commit Last updated on: 2026-03-07T22:14:12Z |
Greptile SummaryThis PR fixes a Key changes:
One design consideration worth noting: Confidence Score: 4/5
Last reviewed commit: 8b43255 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8b43255ea5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| 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. | ||
| this.db.exec("PRAGMA busy_timeout = 5000"); |
There was a problem hiding this comment.
Keep QMD read timeout low to prevent blocking requests
Setting the read-only QMD connection to PRAGMA busy_timeout = 5000 can block the Node event loop for up to 5s whenever the updater or another process holds a SQLite lock, because this code path uses synchronous DatabaseSync queries (resolveDocLocation and readCounts) during request handling. This regresses the previous low-latency behavior (the old 1ms timeout and existing busy-error handling) and can cause visible stalls/timeouts under normal concurrent update traffic.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good point — lowered to 1000 ms in cc744d0. The QMD read path uses synchronous DatabaseSync, so 5 s was too aggressive. In WAL mode readers rarely contend with writers anyway, so 1 s provides a safe margin without risking event-loop stalls.
The QMD connection uses synchronous DatabaseSync queries on the main thread, so a 5 s timeout could block the event loop. In WAL mode readers rarely contend with writers, so 1 s is a safe upper bound that still prevents the SQLITE_BUSY crash without risking noticeable stalls.
|
Landed on What we changed while landing:
Source commits reviewed: Thanks @MumuTW. |
…hanks @MumuTW) Co-authored-by: MumuTW <clothl47364@gmail.com>
…hanks @MumuTW) Co-authored-by: MumuTW <clothl47364@gmail.com>
…hanks @MumuTW) Co-authored-by: MumuTW <clothl47364@gmail.com>
…hanks @MumuTW) Co-authored-by: MumuTW <clothl47364@gmail.com>
…hanks @MumuTW) Co-authored-by: MumuTW <clothl47364@gmail.com>
…hanks @MumuTW) Co-authored-by: MumuTW <clothl47364@gmail.com>
…hanks @MumuTW) Co-authored-by: MumuTW <clothl47364@gmail.com>
…hanks @MumuTW) Co-authored-by: MumuTW <clothl47364@gmail.com>
…hanks @MumuTW) Co-authored-by: MumuTW <clothl47364@gmail.com>
…hanks @MumuTW) Co-authored-by: MumuTW <clothl47364@gmail.com>
Summary
Fixes #39176
PRAGMA busy_timeoutis per-connection and resets to 0 when the process restarts. With 10+ concurrent processes (gateway, cron agents, subagents) hitting the same SQLite file, a zero timeout means any concurrent write instantly fails withSQLITE_BUSYinstead of retrying.Changes
src/memory/manager-sync-ops.ts: SetPRAGMA busy_timeout = 5000inopenDatabaseAtPath(), which is used by all write connection opens (initial, vacuum, reopen)src/memory/qmd-manager.ts: Raisebusy_timeoutfrom 1 ms to 5000 ms on the read-only QMD connectionTest plan
pnpm vitest run src/memory/index.test.ts— 10 tests passpnpm vitest run src/memory/qmd-manager.test.ts— 56 tests passPRAGMA busy_timeoutreturns 5000 on active connections