Skip to content

fix(local): claim WAL before the sidecars race for it - #1781

Merged
CREDO23 merged 1 commit into
MODSetter:devfrom
CREDO23:fix/local-wal-boot-race
Sep 15, 2026
Merged

CREDO23 merged 1 commit into
MODSetter:devfrom
CREDO23:fix/local-wal-boot-race

Conversation

@CREDO23

@CREDO23 CREDO23 commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

The bug

On any fresh data directory the studio worker died at import, so Studio jobs queued with no consumer: the API accepted them, the tab showed processing, and nothing ever ran them.

worker.py -> worker/consumer.py:6 -> shared/queue.py:13 -> SqliteHuey(...)
  -> huey/storage.py:874 in _create_connection
sqlite3.OperationalError: database is locked
[PYI-25514:ERROR] Failed to execute script 'worker' due to unhandled exception!
[main] sidecar worker-studio crashed (code=1)

Cause

SQLite wants an exclusive lock to switch a file to WAL, and answers SQLITE_BUSY without running the busy handler, so busy_timeout does not cover that one statement. The API and both workers boot in the same tick and each opens huey.db and surfsense.db; on a file nobody had initialised yet, huey's own pragma journal_mode="wal" raised at import, which PyInstaller turns into exit 1. Started last, the studio worker was usually the loser.

Once a file is in WAL the pragma takes no lock — which is why the next launch always looked fine, and why this hid since both files were added on 2026-09-03.

Fix

  • shared/sqlite.py: enable_wal retries the pragma for 5s, matching busy_timeout.
  • shared/queue.py: claims WAL on the queue file before constructing the two SqliteHuey instances, so huey's own pragma only reads the mode back.
  • shared/db.py: the connect hook sets busy_timeout ahead of the pragmas that need to wait.

Verification

Racing the frozen binaries on a fresh data dir, five rounds each: the previous build lost the studio worker once to database is locked, this build zero times. Afterwards, twelve artifacts generated in one batch (four concurrent) all reached ready with no failures.

The unit test pins the mechanism rather than the odds: it holds a write transaction open, asserts SQLite refuses the pragma outright, then asserts enable_wal waits it out and ends in WAL.

Also drops the onRetry/retry leftovers from studio-panel.test.tsx that #1780 left behind — vitest does not typecheck, so pnpm dist was the first thing to notice.

High-level PR Summary

This PR fixes a race condition where the studio worker would crash on startup with a database is locked error when multiple sidecars attempted to simultaneously switch a fresh SQLite database to WAL mode. The fix introduces a retry mechanism (enable_wal) that waits up to 5 seconds for the exclusive lock needed to enable WAL mode, and pre-claims WAL on queue files before Huey connects. The reordering of pragmas in the database connection hook ensures busy_timeout is set before operations that need to wait for locks.

⏱️ Estimated Review Time: 15-30 minutes

💡 Review Order Suggestion
Order File Path
1 surfsense_local/backend/shared/sqlite.py
2 surfsense_local/backend/tests/unit/shared/test_sqlite.py
3 surfsense_local/backend/shared/queue.py
4 surfsense_local/backend/shared/db.py
5 surfsense_local/frontend/src/features/studio/studio-panel.test.tsx
⚠️ Inconsistent Changes Detected
File Path Warning
surfsense_local/frontend/src/features/studio/studio-panel.test.tsx This change removes leftover test code from a previous PR (#1780) and is unrelated to the SQLite WAL race condition fix that is the main purpose of this PR

Need help? Join our Discord

The studio worker died at import on any fresh data directory, so Studio
jobs queued with no consumer: the API accepted them, the tab showed
processing, and nothing ever ran them.

SQLite wants an exclusive lock to switch a file to WAL, and answers
SQLITE_BUSY without running the busy handler, so busy_timeout does not
cover that one statement. The API and both workers boot in the same tick
and each opens huey.db and surfsense.db; on a file nobody had initialised
yet, huey's own `pragma journal_mode="wal"` raised inside shared/queue.py
at import, which PyInstaller turns into exit 1. Started last, the studio
worker was usually the loser. Once a file is in WAL the pragma takes no
lock, which is why the next launch always looked fine and why this hid
since both files were added on 2026-09-03.

shared.sqlite.enable_wal retries the pragma for 5s, matching busy_timeout.
queue.py claims WAL on the queue file before constructing the two
SqliteHuey instances, and the engine's connect hook now sets busy_timeout
ahead of the pragmas that need to wait.

Racing the frozen binaries on a fresh data dir, five rounds each: the
previous build lost the studio worker once to "database is locked", this
one zero times. The unit test pins the mechanism rather than the odds -
it holds a write transaction open, asserts SQLite refuses the pragma
outright, then asserts enable_wal waits it out and ends in WAL.

Also drops the onRetry/retry leftovers from studio-panel.test.tsx. The
merge removed both, vitest does not typecheck, so `pnpm dist` was the
first thing to notice.
@vercel

vercel Bot commented Sep 15, 2026

Copy link
Copy Markdown

@CREDO23 is attempting to deploy a commit to the Rohan Verma's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Sep 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 6ad38140-2c16-42cf-be67-e1d0e2beee8c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@CREDO23
CREDO23 merged commit 161c10f into MODSetter:dev Sep 15, 2026
4 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant