fix(db): serialize database init and migrations across processes by gitRasheed · Pull Request #44558 · anomalyco/opencode · GitHub
Skip to content

fix(db): serialize database init and migrations across processes - #44558

Open
gitRasheed wants to merge 6 commits into
anomalyco:devfrom
gitRasheed:fix/db-migration-race
Open

fix(db): serialize database init and migrations across processes#44558
gitRasheed wants to merge 6 commits into
anomalyco:devfrom
gitRasheed:fix/db-migration-race

Conversation

@gitRasheed

@gitRasheed gitRasheed commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #33320

Type of change

  • Bug fix

What does this PR do?

I reproduced the race by starting six opencode run processes against a fresh global SQLite database. Five failed with "database is locked" within ~15ms.

Both drivers enable WAL before setting busy_timeout, and changing journal mode bypasses the busy handler. DatabaseMigration.apply uses an in-process semaphore around its check and write, so separate processes can run the same migration. Because one shipped migration resets the database, running it twice can silently delete session data. The migration transactions also used DEFERRED mode, which returns SQLITE_BUSY during a read-to-write upgrade without consulting busy_timeout.

I set busy_timeout before the first write in both drivers and retry WAL conversion within that budget. DatabaseMigration.apply now takes the existing cross-process Flock used by models-dev.ts, keyed by the path from pragma_database_list so aliases share a lock. Memory databases skip the lock. Migrations use BEGIN IMMEDIATE and recheck the journal inside the transaction, allowing raced callers to skip completed migrations.

#38849 stays open: this covers its startup crashes but not the mid-session ones, which come from a different write path. Its busy_timeout=0 reading is a per-connection artifact; the pragma is set, just bypassed here.

How did you verify your code works?

Two cross-process tests in test/database-migration.test.ts spawn real children on a file barrier: one races four processes at a fresh database, the other rewinds the last migration and asserts the journal ends with exactly one row. They fail unpatched; the manual six-process race went from 1/6 to 6/6 clean. Full core suite and typecheck pass.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Concurrent CLI processes racing one database file crash at startup:
the driver enables WAL at connection-open before any busy_timeout is
set, so the losers fail instantly with 'database is locked', and the
migration path guards its check-then-write with an in-process
semaphore only, letting two fresh processes both run schema.up — or
both run a pending migration, which for destructive migrations means
data loss.

Set busy_timeout in both drivers before the first write, and take the
cross-process Flock (already used for the models.dev cache race)
inside DatabaseMigration.apply, keyed by SQLite's own resolved path so
every caller is covered and path aliases collapse. Migration
transactions use immediate behavior so a read-to-write upgrade waits
on busy_timeout instead of failing at once.
@gitRasheed
gitRasheed force-pushed the fix/db-migration-race branch from aeffe0b to 7024062 Compare August 24, 2026 00:10
@Enough1122

Copy link
Copy Markdown

Default filesystems on Windows and macOS are case-insensitive, so two
spellings of one database path must map to one lock. Folding on
case-sensitive filesystems too only over-serializes two distinct files
for the duration of a migration.
@gitRasheed

Copy link
Copy Markdown
Contributor Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Concurrent opencode run workers can fail at startup with database is locked

2 participants