Several documentation.red-gate.com and liquibase.com URLs added in the previous commit had since moved (Redgate restructured its docs under /flyway/reference/, Liquibase Pro pricing moved to /pricing); this repoints them at the current, verified-200 pages. Also captures docs/output/16-liquibase-lock-defaults-javap.txt, the trimmed javap output backing chapter 11's claim about Liquibase's default lock-poll and lock-wait settings, which chapter 11 previously asserted without a committed artifact to back it. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Q6XdRjtsp4862EM44T7i9a
58 lines
4.0 KiB
Markdown
58 lines
4.0 KiB
Markdown
# 8. Concurrent startup and locking
|
|
|
|
[← 7. Why there is no undo](07-why-there-is-no-undo.md) · [Index](../README.md) · Next: [9. Liquibase: anatomy of an update →](09-liquibase-anatomy-of-an-update.md)
|
|
|
|
A rolling deploy starts several instances of the same application at close to the same moment,
|
|
all pointed at the same database, all carrying the same migrations. If two of them both decide
|
|
"I need to run V1" at once, what stops the second from either duplicating work or corrupting the
|
|
history table?
|
|
|
|
[`FlywayConcurrentMigrateTest`](../src/test/java/com/ankurm/dbmigrations/flyway/FlywayConcurrentMigrateTest.java)
|
|
answers this empirically rather than by reading about it: it starts two Flyway instances on two
|
|
real threads, pointed at the same H2 file (opened with `AUTO_SERVER=TRUE` so genuinely separate
|
|
JDBC connections can share one file), both migrating a deliberately slow, 800ms Java-based
|
|
migration ([`V1__SlowMigration.java`](../src/test/java/com/ankurm/dbmigrations/flyway/V1__SlowMigration.java)).
|
|
|
|
```
|
|
instance A migrate() took 905ms
|
|
instance B migrate() took 911ms
|
|
wall-clock time for both, run concurrently: 915ms
|
|
```
|
|
|
|
```
|
|
flyway_schema_history:
|
|
installed_rank | version | description | success
|
|
-1 | NULL | ... | true
|
|
1 | 1 | SlowMigration| true
|
|
```
|
|
|
|
(from [`docs/output/08-flyway-concurrent-lock.txt`](output/08-flyway-concurrent-lock.txt))
|
|
|
|
The migration ran exactly once — the test asserts that directly by counting rows matching
|
|
`SlowMigration`. What's more interesting is the timing: both instances took **almost the same,
|
|
almost-full 800+ms**, and the wall clock for both together is close to *that one duration*, not
|
|
their sum. That shape is the real proof of what's happening underneath: Flyway takes a row-level
|
|
lock on its own schema history table before checking what needs to run. The winner holds it for
|
|
the whole migration; the loser blocks on that same lock for nearly the full window, then — once it
|
|
finally acquires it — finds the migration already recorded and returns almost immediately. No
|
|
external coordinator, no separate lock table: the schema history table *is* the lock.
|
|
|
|
<svg viewBox="0 0 700 170" xmlns="http://www.w3.org/2000/svg" font-family="monospace" font-size="13">
|
|
<text x="20" y="20">instance A</text>
|
|
<rect x="90" y="8" width="260" height="24" fill="#dcfce7" stroke="#16a34a"/>
|
|
<text x="220" y="25" text-anchor="middle" font-size="11">holds lock — runs migration (≈800ms)</text>
|
|
<text x="20" y="60">instance B</text>
|
|
<rect x="90" y="48" width="260" height="24" fill="#fee2e2" stroke="#dc2626"/>
|
|
<text x="220" y="65" text-anchor="middle" font-size="11">blocked, waiting for the same row lock</text>
|
|
<rect x="352" y="48" width="20" height="24" fill="#dcfce7" stroke="#16a34a"/>
|
|
<text x="450" y="65" font-size="11">lock acquired → nothing to do → returns</text>
|
|
<text x="20" y="110" font-size="12" fill="#475569">Both calls "took" roughly 800-900ms — the loser's time is almost entirely the wait,</text>
|
|
<text x="20" y="128" font-size="12" fill="#475569">not the (nonexistent, for it) work. Wall clock ≈ one migration's length, not the sum of both.</text>
|
|
</svg>
|
|
|
|
## Going deeper
|
|
|
|
- **A naive assertion here would compare wall-clock time against the sum of both individual durations**, expecting serialization to look like "one after the other, end to end". That's the wrong model for two threads submitted at the same instant with one blocking on the other's lock — the right check is that *both* individual durations are long, proving the loser genuinely waited rather than racing ahead. This module's test was rewritten once to fix exactly that reasoning error.
|
|
- [Flyway's locking strategy](https://documentation.red-gate.com/fd/migrations-271585107.html) (`rel="nofollow"`) documents the row-lock approach and which databases support it natively versus via a fallback.
|
|
- Liquibase solves the same problem with a dedicated, separate lock table rather than a row lock on the history table itself — chapter [11](11-liquibase-locking.md) measures how differently that behaves under contention.
|