fix(db): use rollback-journal mode for central DB to stop SIGBUS crashes - #1752
Conversation
The central DB (~/.fusion/fusion-central.db) is opened concurrently by every fusion process on a host. In WAL mode those connections coordinate through a memory-mapped `-shm` wal-index; on macOS/APFS a reader takes a SIGBUS (walIndexReadHdr / `cluster_pagein past EOF`) when another process resizes it mid-checkpoint, killing the node process with no JS stack or log. Observed 3x in 3 days. Switch the central DB to journal_mode=DELETE, which uses no `-shm` mmap and coordinates cross-process access via POSIX byte-range locks instead; busy_timeout absorbs the added writer serialization. Per-project DBs keep WAL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Ready to review this PR? Stage has broken it down into 3 individual chapters for you:
Chapters generated by Stage for commit e53f50e on Jun 25, 2026 6:52am UTC. |
|
Warning Review limit reached
More reviews will be available in 28 minutes and 47 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughCentral database SQLite setup now uses DELETE journal mode instead of WAL, removes WAL-specific PRAGMAs, and sets synchronous to FULL. Tests were updated to verify the new journal mode and the absence of ChangesCentral DB journal mode change
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummarySwitches the central DB (
Confidence Score: 5/5Safe to merge — the fix is surgical, the failure path (WAL holder blocks migration) is explicitly handled with a loud warning rather than a throw, and the regression tests are thorough. The change is a well-understood SQLite journal mode switch with no schema changes, no data loss risk (SQLite checkpoints WAL into the main file on first open), and a comprehensive test suite covering the normal path, the -shm absence proof, and the rolling-upgrade warning path. The previous review concern about silent failure on WAL→DELETE is fully addressed: the return value is now captured via prepare().get(), both the throw and the no-op paths emit a console.warn, and a new test exercises the blocked-migration scenario end-to-end. No files require special attention. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant P1 as Process A (new)
participant SQLite as SQLite Engine
participant FS as Filesystem
participant P2 as Process B (WAL holder)
P2->>FS: "PRAGMA journal_mode = WAL (open -shm/-wal)"
P2->>SQLite: "BEGIN (holds WAL read lock)"
Note over P1,SQLite: Rolling-upgrade window
P1->>SQLite: "PRAGMA busy_timeout = 5000"
P1->>SQLite: "PRAGMA journal_mode = DELETE"
SQLite-->>P1: "returns wal (exclusive lock blocked)"
P1->>P1: "journalMode !== delete → console.warn"
Note over P1,FS: Clean state — no WAL holder
P1->>SQLite: "PRAGMA busy_timeout = 5000"
P1->>SQLite: "PRAGMA journal_mode = DELETE"
SQLite->>FS: "checkpoint WAL, delete -wal/-shm"
SQLite-->>P1: "returns delete"
P1->>SQLite: "PRAGMA synchronous = FULL"
P1->>SQLite: "PRAGMA foreign_keys = ON"
Note over P1,FS: No -shm mmap — SIGBUS surface eliminated
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant P1 as Process A (new)
participant SQLite as SQLite Engine
participant FS as Filesystem
participant P2 as Process B (WAL holder)
P2->>FS: "PRAGMA journal_mode = WAL (open -shm/-wal)"
P2->>SQLite: "BEGIN (holds WAL read lock)"
Note over P1,SQLite: Rolling-upgrade window
P1->>SQLite: "PRAGMA busy_timeout = 5000"
P1->>SQLite: "PRAGMA journal_mode = DELETE"
SQLite-->>P1: "returns wal (exclusive lock blocked)"
P1->>P1: "journalMode !== delete → console.warn"
Note over P1,FS: Clean state — no WAL holder
P1->>SQLite: "PRAGMA busy_timeout = 5000"
P1->>SQLite: "PRAGMA journal_mode = DELETE"
SQLite->>FS: "checkpoint WAL, delete -wal/-shm"
SQLite-->>P1: "returns delete"
P1->>SQLite: "PRAGMA synchronous = FULL"
P1->>SQLite: "PRAGMA foreign_keys = ON"
Note over P1,FS: No -shm mmap — SIGBUS surface eliminated
Reviews (2): Last reviewed commit: "Address PR review feedback (#1752)" | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/src/central-db.ts`:
- Line 581: The SQLite journal mode switch is being executed without verifying
whether it actually changed to DELETE, so a failed transition can leave the
database in WAL mode and preserve the crash risk. Update the journal-mode
handling in central-db around this.db.exec so the result of PRAGMA journal_mode
= DELETE is captured and checked, and only proceed when the returned mode
confirms DELETE; if it returns a different mode, treat it as a failure and
surface that condition instead of assuming success.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 8607ebeb-dbf0-497f-a8d8-abca8e32d13f
📒 Files selected for processing (3)
.changeset/central-db-delete-journal-sigbus.mdpackages/core/src/__tests__/central-db.test.tspackages/core/src/central-db.ts
- Verify the WAL->DELETE journal-mode switch instead of discarding exec()'s result. During a rolling upgrade a lingering WAL holder blocks the exclusive lock the switch needs, so SQLite either throws SQLITE_BUSY or no-ops and returns "wal". Capture both outcomes and warn loudly so the residual -shm SIGBUS surface is observable, rather than silently swallowed. - Do not rethrow: the condition is transient and self-healing (the next start after the last WAL holder exits migrates cleanly); hard-failing would make the central DB unopenable during the very upgrade window it describes. - Add a migration-path regression test (a WAL holder blocking the switch) that the prior fresh-DB-only tests did not cover. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Running more than one fusion process on a host (multiple dashboards/CLIs across worktrees, all attaching
~/.fusion/fusion-central.db) could crash anodeprocess at random — instantly, with no JS stack and nothing in the logs. This happened 3 times in 3 days on one machine. After this change those processes coexist without crashing.The crash was an OS-level
SIGBUS(EXC_BAD_ACCESS,FS pagein error/ kernelcluster_pagein past EOF) inside SQLite'swalIndexReadHdr. In WAL mode every connection coordinates through a memory-mapped-shmwal-index; on macOS/APFS, when one process resizes/rebuilds that file during a checkpoint while another has it mmap'd, the reader faults on the now-out-of-bounds page. A hardware memory fault can't be caught bynode:sqliteor JS, so the whole process dies.The fix switches the central DB to
journal_mode = DELETE(rollback journal), which uses no-shmmemory map and coordinates cross-process access via POSIX byte-range locks instead — removing the faulting surface entirely while keeping multi-process access. The existingbusy_timeoutabsorbs the writer serialization that DELETE mode trades for WAL's reader/writer concurrency. Per-project DBs (db.ts) are intentionally left on WAL: they're single-process-per-project and don't hit this cross-process fault. SQLite migrates the existing WAL database on first open (checkpoints-walinto the main file and removes-wal/-shm), so there is no data loss.Test plan
central-db.test.tsassert the central DB reportsjournal_mode = delete(notwal) and that no-shmwal-index file is ever created even after write traffic — i.e. the exact faulted surface is gone.@fusion/coretypechecks clean.Summary by CodeRabbit