Full restore on SQLite: take the write lock first, so another writer is waited for instead of "database is locked"
CI / unit (8.2) (push) Has been cancelled
CI / unit (8.3) (push) Has been cancelled
CI / unit (8.4) (push) Has been cancelled
CI / psalm (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / integration (mariadb, 32) (push) Has been cancelled
CI / integration (mariadb, 33) (push) Has been cancelled
CI / integration (mariadb, 34) (push) Has been cancelled
CI / integration (mariadb, 35) (push) Has been cancelled
CI / integration (pgsql, 32) (push) Has been cancelled
CI / integration (pgsql, 33) (push) Has been cancelled
CI / integration (pgsql, 34) (push) Has been cancelled
CI / integration (pgsql, 35) (push) Has been cancelled
CI / integration (sqlite, 32) (push) Has been cancelled
CI / integration (sqlite, 33) (push) Has been cancelled
CI / integration (sqlite, 34) (push) Has been cancelled
CI / integration (sqlite, 35) (push) Has been cancelled
CI / integration-targets (push) Has been cancelled
CI / integration-user (32) (push) Has been cancelled
CI / integration-user (33) (push) Has been cancelled
CI / integration-user (34) (push) Has been cancelled
CI / integration-user (35) (push) Has been cancelled
CI / integration-full-restore (push) Has been cancelled

SQLite in WAL mode starts a transaction as a reader; a write from another
process between our first read and our first write makes our first write
fail at once, whatever the busy timeout. The SQLite full-restore test now
runs a second writer alongside the restore.
This commit is contained in:
2026-10-29 03:06:05 +01:00
parent cdf0b70e25
commit 3eec2e9554
4 changed files with 38 additions and 3 deletions
+7 -1
View File
@@ -169,7 +169,7 @@ final class DbDumper {
$foreignKeys = [];
$this->db->beginTransaction();
try {
$foreignKeys = $this->suspendForeignKeys();
$foreignKeys = $this->suspendForeignKeys(); // first: on SQLite it also takes the write lock
foreach ($manifest['tables'] as $table => $info) {
if (in_array($table, $report['missing'], true)) {
continue;
@@ -412,6 +412,12 @@ final class DbDumper {
return $checks;
}
if ($platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform) {
// SQLite (WAL) starts a transaction as a reader. When another process (a cron job, the
// installer still finishing) writes between our first read and our first write, that
// write fails at once with "database is locked": no waiting helps a reader whose view
// is out of date. Take the write lock first, so we wait for the other writer instead
// and it waits for us after that.
$this->db->executeStatement('DELETE FROM ' . $this->db->quoteIdentifier($this->prefix . 'appconfig') . ' WHERE 0 = 1');
$this->db->executeStatement('PRAGMA defer_foreign_keys = ON'); // checked at commit
}
return [];