# 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) 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## 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.master.yaml(matching the real changelog's filename) reproduced the phantom success on every run. Naming it anything else — this module settled onbootstrap-only.yaml— never did, across dozens of runs. See the test's own javadoc for the exact reproduction notes.