# 11. Liquibase locking [← 10. Rollback: auto-generated vs explicit](10-rollback-auto-generated-vs-explicit.md) · [Index](../README.md) · Next: [12. The OSS license service →](12-the-oss-license-service.md) Liquibase's answer to chapter [8](08-concurrent-startup-and-locking.md) is a dedicated table, `DATABASECHANGELOGLOCK`, holding exactly one row that a real `update()` call has to acquire before touching anything else. [`LiquibaseConcurrentUpdateTest`](../src/test/java/com/ankurm/dbmigrations/liquibase/LiquibaseConcurrentUpdateTest.java) runs the same experiment as the Flyway version: two instances, one deliberately slow (800ms) [`customChange`](../src/test/java/com/ankurm/dbmigrations/liquibase/SlowCustomChange.java), started on two real threads against the same database. ``` instance A update() took 860ms instance B update() took 10094ms wall-clock time for both, run concurrently: 10096ms databasechangelog after both finished: ID | AUTHOR | EXECTYPE --------------+--------+--------- 1-slow-change | ankurm | EXECUTED ``` (from [`docs/output/13-liquibase-lock-contention.txt`](output/13-liquibase-lock-contention.txt)) The changeset ran exactly once — same guarantee as Flyway. The *shape* of the wait is different, though, and it's a real, verified difference: instance B's call took over ten seconds, for 800ms of underlying work. That's not noise. Liquibase's `LockService` doesn't retry immediately when a lock is held — it polls, and `liquibase.changeLogLockPollRate`'s default, confirmed by decompiling `GlobalConfiguration`'s own bytecode with `javap`, is **10 seconds**: ``` liquibase.changeLogLockPollRate -> default 10 (seconds between checks while the lock is held) liquibase.changeLogLockWaitTimeInMinutes -> default 5 (minutes before giving up entirely) ``` (from [`docs/output/16-liquibase-lock-defaults-javap.txt`](output/16-liquibase-lock-defaults-javap.txt), the trimmed `javap -p -c -constants` output showing both `long` constants — `5` and `10` — right next to the field they initialize) instance A ≈800ms work instance B polling every ~10s until it happens to check after A released the lock Flyway's row lock releases the waiter the instant it's free (chapter 8: ≈900ms total). Liquibase's default poll rate means the same race can cost up to ~10s of pure waiting. A losing instance in a real rolling deploy can therefore sit doing nothing for up to ten seconds even though the work it's waiting on took under a second — worth knowing before you set a readiness-probe timeout shorter than that. ## A real defect this test's own history ran into An earlier version of this test tried to avoid a *different* race — two instances both trying to **create** `DATABASECHANGELOG`/`DATABASECHANGELOGLOCK` for the first time, which fails with a plain `DatabaseException` ("table already exists") rather than a graceful wait, since the lock table that would make the loser wait doesn't exist yet either — by running a bootstrap `update()` against an *empty* changelog first, to get the tracking tables created before the real race. That bootstrap changelog file was originally named `master.yaml`, same as the real one, just in a different directory. Doing that reliably reproduced a genuine Liquibase 5.0.3 defect: both real instances would log a completely normal `Run: 1` / "successful" summary, but the changeset's own code never actually executed, and — checked from each instance's *own* connection, immediately after its own `update()` call, no cross-connection visibility question involved — `DATABASECHANGELOG` stayed empty. A phantom success, caused by something in Liquibase's changelog-history handling that keys off the changelog's simple filename rather than its full resource path.
Verified, not guessed: this was isolated by toggling only the bootstrap file's name with everything else held constant. Naming it master.yaml (matching the real changelog's filename) reproduced the phantom success on every run. Naming it anything else — this module settled on bootstrap-only.yaml — never did, across dozens of runs. See the test's own javadoc for the exact reproduction notes.
## Going deeper - [`GlobalConfiguration`](https://javadoc.io/doc/org.liquibase/liquibase-core/latest/liquibase/GlobalConfiguration.html) (`rel="nofollow"`) lists every global Liquibase setting, including both lock-related ones above, with their system-property and environment-variable spellings. - If you hit a "table already exists" failure the very first time two instances of a brand-new service start against a brand-new database, this is why — it's a one-time bootstrap race, not a recurring lock-contention bug, and a health-check retry (the same thing a rolling deploy already does for a failed pod) resolves it. - This filename-collision defect is specific to how this test constructed two changelogs with the same simple name from two different `DirectoryResourceAccessor` roots — a completely ordinary application, with exactly one changelog file, will never encounter it.