Summary
Two deployment migration executors started concurrently against the same completely empty remote D1 database can collide in migration DDL before either executor records the migration in _emdash_migrations.
This is distinct from #2755 (empty-D1 HTTP 400 error handling) and #954 (recovery from an already partial migration state).
Versions
- EmDash main baseline:
6c912b50159f37bcbfbd5eea9a34182ce609c36b
- Empty-D1 fix candidate used for the reproduction:
1bdf24f87337f8aa8f1ed4a6042949ac48fb18c3
- Core migration identity: EmDash 0.35.0, 69 migrations
- Remote database: a new, empty, disposable Cloudflare D1 database
Reproduction
- Confirm the remote D1 database has no application tables and no migration history.
- Start two independent deployment migration executors at the same time with the same account/database target and exact migration fingerprint.
- Let both run the normal migration apply path.
Actual result
- One executor completed all 69 migrations.
- The other executor failed during
004_plugins with a sanitized D1 error equivalent to:
table _plugin_storage already exists (migration: 004_plugins)
The final ledger can appear complete because the successful executor finishes, but the losing executor may already have executed part of a multi-statement migration before the error.
The current race recovery recognizes a migration-history UNIQUE conflict after migration work. It does not prevent both executors from entering the migration body, so a DDL collision can occur before bookkeeping recovery is reached.
Kysely's SQLite migration-lock path assumes a connection-level serialization model and does not provide a cross-process lock for independent D1 REST executors.
Expected behavior
Concurrent deployment migration attempts for the same D1 target should not both enter migration DDL. Either:
- the adapter/runner provides a real cross-executor migration mutex; or
- the deployment interface requires and clearly documents an external single-owner coordinator and fails closed when ownership is unavailable.
A broad catch for table already exists is not safe because the losing executor may be observing a partial schema rather than a completed migration.
Current workaround
Use an external atomic lease scoped to a hash of Cloudflare account ID plus D1 UUID. Bind the lease to the exact migration artifact and fingerprint, allow only one migration owner, fail closed if the coordinator is unavailable, and require explicit status/partial-state checks before recovering an expired lease.
No production deployment or confirmed data loss is part of this report.
Summary
Two deployment migration executors started concurrently against the same completely empty remote D1 database can collide in migration DDL before either executor records the migration in
_emdash_migrations.This is distinct from #2755 (empty-D1 HTTP 400 error handling) and #954 (recovery from an already partial migration state).
Versions
6c912b50159f37bcbfbd5eea9a34182ce609c36b1bdf24f87337f8aa8f1ed4a6042949ac48fb18c3Reproduction
Actual result
004_pluginswith a sanitized D1 error equivalent to:The final ledger can appear complete because the successful executor finishes, but the losing executor may already have executed part of a multi-statement migration before the error.
The current race recovery recognizes a migration-history UNIQUE conflict after migration work. It does not prevent both executors from entering the migration body, so a DDL collision can occur before bookkeeping recovery is reached.
Kysely's SQLite migration-lock path assumes a connection-level serialization model and does not provide a cross-process lock for independent D1 REST executors.
Expected behavior
Concurrent deployment migration attempts for the same D1 target should not both enter migration DDL. Either:
A broad catch for
table already existsis not safe because the losing executor may be observing a partial schema rather than a completed migration.Current workaround
Use an external atomic lease scoped to a hash of Cloudflare account ID plus D1 UUID. Bind the lease to the exact migration artifact and fingerprint, allow only one migration owner, fail closed if the coordinator is unavailable, and require explicit status/partial-state checks before recovering an expired lease.
No production deployment or confirmed data loss is part of this report.