Repository navigation
docs(storage): investigate runtime database VACUUM placement - #6034
garvit-arora wants to merge 5 commits into
Conversation
Astro-Han
left a comment
There was a problem hiding this comment.
Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.
Reviewed range: merge-base c11a533 (main) to head bd1ca5b, 1 commit, 3 files, +165/-0 (design doc, benchmark script, manual workflow). This is a first review, so there are no prior findings. No protocol files are touched, so there is no epoch impact.
Verdict: one P2 (PR-caused CI failure, plus a header failure that will follow), and three P3 notes on factual accuracy in the doc and harness. The recommendation itself (after-Ready worker plus a Host-owned writer gate, with report-only as the fallback) is reasonable.
P2: CI test fails on the new workflow. scripts/ci-workflow-policy.test.mjs rejects runtime-vacuum-benchmark.yml twice (job 113832831773):
- "ubuntu-latest queues for a runner; name the image instead". The repo uses
ubuntu-24.04. - "workflows never persist the job credential into the checkout". The checkout step needs
with: persist-credentials: false, which the repo's other 37 checkouts set.
Once those pass, check:asf-headers will fail next. I ran it locally: .github/workflows/runtime-vacuum-benchmark.yml and docs/design/runtime-database-vacuum-placement-6000.md are missing the ASF header (npm run write:asf-headers).
Claims checked against code (correct):
runtime.sqliteis WAL (sqlite-runtime-schema.ts:952).- Production source has no
auto_vacuum/VACUUM, so the database isNONE. - The runtime store sets a 5 s busy timeout (
sqlite-runtime-store.ts:238). - The startup deadline is 75 s (
connect-or-spawn.ts:75,wait-for-ready.ts:24). - Client liveness is 8 s (
connection.ts:97). - The ~55 s before-listen and ~13 s/GiB after-Ready figures match #6000. #5855 merged with runtime compaction deferred to a follow-up.
P3: "the existing Compact database action" doesn't exist on main (doc lines 7 and 48). No such operation, UI string, or protocol entry exists. #6000 lists it as something #5825 agreed future work would use. Call it the planned explicit request, so the "smallest first PR" scope includes adding the action and its protocol operation (with the epoch that implies).
P3: the timer-delay column isn't an independent measurement. clearTimeout(timer) runs synchronously right after conversion.exec('VACUUM'), so the timer callback can never fire. mainThreadTimerDelayMs is therefore always max(0, vacuumMs - 50). The table shows this (0.93 to 0.88, 10.17 to 10.12, 36.18 to 36.13), and my local 50 MiB run gave 320 ms and 270 ms. Either drop the column or measure real delay by yielding before clearing the timer. Since the conversion would run on the main thread, the conclusion that a 1 GiB conversion would stall the event loop past 8 s still holds.
P3: the fixture doesn't match production journal mode. The script sets journal_mode=DELETE, but runtime.sqlite is WAL. In WAL mode, VACUUM writes the rewritten database through the WAL. That changes both timing and peak disk use (the WAL can grow to about the size of the database until a checkpoint). That matters for the doc's 2x free-space preflight, so either benchmark with WAL or state the difference.
Minor: a workflow_dispatch workflow can only be dispatched once it exists on the default branch. The "Ubuntu numbers appended after that run completes" step therefore needs a fork run or a merge first.
Mergeability/CI: GitHub reports MERGEABLE, but the merge state is BLOCKED (no approving review). test fails (PR-caused, above); label passes.
Update for head fc896403: while this review was being prepared, fc896403 added canonical license headers to the workflow and the design doc, so the header-check part of the P2 is addressed. The workflow-policy part is still open on this head: the matrix still uses ubuntu-latest, and the checkout step still has no persist-credentials: false. The inline comments have been moved to the shifted lines.
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| os: [ubuntu-latest, windows-latest] |
There was a problem hiding this comment.
P2: ci-workflow-policy.test.mjs rejects ubuntu-latest ("name the image instead"). The repo uses ubuntu-24.04. This file also needs the ASF header.
| runs-on: ${{ matrix.os }} | ||
| timeout-minutes: 30 | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 |
There was a problem hiding this comment.
P2: the policy test requires with: persist-credentials: false on every checkout.
| const vacuumStart = performance.now(); | ||
| conversion.exec('VACUUM'); | ||
| const vacuumMs = performance.now() - vacuumStart; | ||
| clearTimeout(timer); |
There was a problem hiding this comment.
P3: this runs synchronously right after exec('VACUUM'), so the timer can never fire and mainThreadTimerDelayMs is always vacuumMs - 50. It isn't an independent measurement.
| for (const targetMiB of sizes) { | ||
| const source = join(root, `runtime-${targetMiB}.sqlite`); | ||
| const db = new DatabaseSync(source); | ||
| db.exec('PRAGMA page_size=4096; PRAGMA auto_vacuum=NONE; PRAGMA journal_mode=DELETE;'); |
There was a problem hiding this comment.
P3: runtime.sqlite runs in WAL mode (sqlite-runtime-schema.ts:952). With journal_mode=DELETE here, the benchmark misses VACUUM's WAL write and disk-growth behavior.
|
|
||
| ## Smallest first PR after placement is accepted | ||
|
|
||
| Add the explicit persisted request/status and free-space preflight, plus a Host-owned writer gate and worker-thread conversion behind the existing `Compact database` action. Keep it opt-in, make insufficient space retryable across restart, test cancellation/crash/reopen and concurrent write admission, and report progress/results without adding a compatibility epoch until merge. Do not combine it with automatic compaction policy or background thresholds. |
Astro-Han
left a comment
There was a problem hiding this comment.
Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.
Incremental review: fc89640 to 1d8124a. This adds two commits: 23a6565 (fixes) and 1d8124a (formatting only). The merge-base is unchanged (c11a533) and there was no rebase. The delta touches the workflow, the design doc, and the benchmark script.
Prior findings
- P2 (CI
testfailed inci-workflow-policy.test.mjs): fixed.- The matrix now uses
ubuntu-24.04. - The checkout step sets
persist-credentials: false.
- The matrix now uses
- P3 ("existing
Compact databaseaction"): fixed.- The doc now calls it the planned action and says the action and its protocol operation do not exist yet.
- The first-PR scope now includes adding the action and protocol request, and claiming the compatibility epoch at merge time.
- P3 (the timer-delay column was not an independent measurement): fixed. The timer and the column are removed, and the doc now says the synchronous duration is the block time.
- P3 (the fixture used DELETE instead of production WAL): fixed.
- The harness now uses
journal_mode=WAL. The finaldb.close()checkpoints and removes the WAL beforecpSync, so the copied file is complete. - The table was re-run in WAL mode, and the doc states that peak WAL growth is not measured.
- The free-space preflight now covers the whole database file set, including the WAL.
- I ran the updated harness locally (Linux, SQLite 3.51.3) at 20 and 50 MiB: 130 ms and 276 ms. It works.
- The harness now uses
New findings in this range
P3: One stale number in the "Before listen" row of the placement table (doc line 54). It still says "The 3 GiB fixture is 36 s on this Windows host". That figure is the excluded DELETE-mode result; the WAL table now shows 140.36 s. The correction strengthens the argument against before-listen placement, since a 3 GiB conversion alone exceeds the 75 s startup deadline. Please update the sentence.
No protocol or epoch files are touched. The doc now says the future action will need an epoch.
Mergeability/CI: test passes on 1d8124a. GitHub reports the PR as MERGEABLE, and the merge state is BLOCKED only because there is no approving review. No P0-P2 issues remain.
| | --- | --- | --- | --- | | ||
| | Worker thread, separate SQLite connection, after Ready | It is still a SQLite writer. WAL permits readers in ordinary cases, but the conversion must obtain write locks and may block/fail concurrent writers. The Host must fence/gate its own runtime writes for the operation and report the maintenance state; relying only on SQLite's 5 s busy timeout can make normal writes fail. Do not open a second writer without this Host-level admission. | Does not consume startup deadline. A worker keeps the Host event loop responsive, but liveness alone is not enough: user writes must have an explicit bounded wait or be refused as maintenance-busy while the conversion owns the writer gate. | Regular `VACUUM` uses SQLite's journaling/transaction mechanisms; document recovery as SQLite-managed and verify integrity on reopen. SQLite documents up to 2x database-size temporary space; WAL-mode peak usage must also account for the WAL and existing sidecars, so preflight must measure the complete database file set and reserve additional headroom. `SQLITE_FULL` keeps the user's request retryable; hard errors clear it and are surfaced. | | ||
| | Run only while no client is attached | “No client” is not equivalent to “no Host writer”: maintenance, scheduled work, cleanup, and reconnect can still write. Must gate new clients and drain existing sessions before starting. | After Ready avoids startup deadline, but clients can repeatedly reconnect or wait through a long maintenance window; a 1 GiB run is already longer than liveness if performed on the event loop. | Same SQLite recovery and free-space behavior as worker option. Restart must retain the explicit request; a crash may leave it pending for the next no-client window. | | ||
| | Before listen, with startup progress state | No attached clients, but migration/startup ownership still matters; the operation must run after any schema migrations and before serving writers. | At the reported 1 GiB / 55 s, it consumes most of the 75 s deadline. The 3 GiB fixture is 36 s on this Windows host, while the production-derived 1 GiB time is already high; large/slower disks can exceed startup deadline. Progress UI does not change the deadline. | Same SQLite recovery and disk requirements. A restart repeats or resumes only by rerunning VACUUM; persist the request across failure and show startup recovery state. | |
There was a problem hiding this comment.
P3: "The 3 GiB fixture is 36 s on this Windows host" is the excluded DELETE-mode figure. The WAL-mode table above now reports 140.36 s for 3,072 MiB, which on its own exceeds the 75 s startup deadline. Please update this sentence to match.
liugddx
left a comment
There was a problem hiding this comment.
Thanks for the thorough write-up. The constants check out against main: the 75 s startup deadline (connect-or-spawn.ts:75, wait-for-ready.ts:24), the 8 s liveness timeout (client/connection.ts:97), WAL with synchronous=FULL and the 5 s busy_timeout. Astro-Han's earlier points are addressed, except that line 54 still quotes "3 GiB 36 s", which is the excluded DELETE-mode result; the WAL run is 140 s. Before this can support a placement decision, a few gaps need closing:
- The ownership model.
runtime.sqliteis served through a single process-local owner connection, and repositories get leases rather than opening connections (operational-state-store.ts:180-184). Secondary processes can also open it withrequire_current, for examplesession-bundle-policy.ts:454andstorage-footprint.ts:131. A worker with its own second connection departs from that model, and a Host-only gate doesn't give exclusivity across processes. - What the worker actually buys.
VACUUMholds the write lock for its whole run, so concurrent writers hitSQLITE_BUSYafter 5 s either way. Moving it to a worker only keeps the event loop and reads alive; the doc should say so plainly. - Disk and timing. The WAL grows to roughly the database size during conversion, so the free-disk preflight is closer to 3× than 2×. The harness also stops timing before
close(), which leaves out the final checkpoint, and never assertsauto_vacuum = 2and an empty freelist afterwards. The 1 → 3 GiB jump (10.7 s → 140 s) is super-linear and needs an explanation (WAL growth, page cache, AV scanning). - Missing options.
VACUUM INTOa staging file needs only a read transaction and leaves the live database untouched on crash. Swapping the file at the next start, before listen, avoids the long exclusive write lock (Windows can't rename open handles, so it can't be swapped live). The repo already vacuums a private copy rather than the live file incontext-offload-snapshot.ts:163-166. Also spell out crash recovery: under WAL withFULL, aVACUUMwithout a commit frame is discarded and the original database survives intact. - The smallest first PR is smaller than proposed. New
runtime.sqlitedatabases can be created withauto_vacuum = INCREMENTALbefore the schema and WAL exist, which needs noVACUUMat all.sqlite-context-offload-schema.ts:218-223already does exactly this. Existing databases are a separate, later step. - Two platforms. #6000 asked for measurements on two platforms; only Windows is measured.
On packaging: please don't add a one-off benchmark workflow to apache/maka. It only has to exist on your fork's default branch for you to dispatch it there. I'd also rather not start a docs/design/ directory for this. Could you post the conclusions on #6000 (or keep the harness under scripts/perf/ with a short README) and close this PR? Once a placement is agreed, the decision can go into docs/architecture/ with the implementation.
|
Closed per the request on #6000: the corrected benchmark and option analysis are now on the issue, and the harness notes are under scripts/perf/README.md. Existing-database placement remains undecided pending a second-platform run and cross-process ownership design. |
Summary\n\nDocuments the requested #6000 placement analysis before any implementation: Host writer interaction, the 75 s startup / 8 s liveness constraints, recovery and free-space behavior, an explicit recommendation, and the smallest first PR. Adds a reproducible Node SQLite fixture and manual Windows/Ubuntu benchmark workflow.\n\nThe in-process Windows measurements and the historical #5855 production-derived measurements are kept distinct in the doc. Ubuntu fixture results will be added when the workflow completes.\n\nCloses no issue; maintainer decision requested.