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
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:
@@ -28,6 +28,10 @@ instead carried by the app description, the git tag, the release, and this file.
|
||||
pasted key lost its line breaks; Nextcloud 34 still read it, Nextcloud 35 reads keys with a
|
||||
stricter library. The line breaks are now restored before the key is used, so a stored location
|
||||
works again without re-entering it.
|
||||
- A full restore onto SQLite could stop at once with "database is locked" when another process
|
||||
wrote to the database just as the restore began (a background job that was already running).
|
||||
The restore now takes the write lock first and waits for the other process instead. The
|
||||
full-restore test on SQLite runs with a second process writing alongside.
|
||||
|
||||
## [1.0.0] - 2026-10-29
|
||||
|
||||
|
||||
@@ -462,8 +462,12 @@ in README "Known limitations":
|
||||
bij #22 en 2026-10-29 op `main` na de merge van 1.0.0): de restore stopt meteen na het
|
||||
aanzetten van de onderhoudsmodus. Niet altijd, dus een tweede proces dat op hetzelfde moment
|
||||
de database gebruikt (cron, een webverzoek of de opstart van de container). Kan ook bij een
|
||||
echte restore naar SQLite gebeuren. Uitzoeken wie de lock vasthoudt; dan wachten en opnieuw
|
||||
proberen (busy timeout) of eerst de andere schrijvers stoppen.
|
||||
echte restore naar SQLite gebeuren. Oorzaak (2026-10-29): SQLite (WAL) begint een transactie
|
||||
als lezer; schrijft een ander proces tussen ons eerste lezen en ons eerste schrijven, dan
|
||||
faalt dat schrijven direct (geen wachttijd helpt). In CI was dat de installer, die
|
||||
"installed" zet en daarna nog seconden doorschrijft. Nu neemt de restore eerst het
|
||||
schrijfslot en wacht hij op de ander; de SQLite-test draait met een tweede schrijver
|
||||
ernaast — **klaar** (2026-10-29).
|
||||
- **DR-tests over databasegrenzen** (besluit 2026-10-20; productie nc01 draait PostgreSQL): na
|
||||
de MariaDB→PostgreSQL-fout (tellers, rc5) elke overstap in de integratietest (`to-pgsql`,
|
||||
`to-mysql`, `to-sqlite`, ook in CI). Volle DR-tests met echte data: vóór 1.0.0 de S3-simulatie
|
||||
|
||||
+7
-1
@@ -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 [];
|
||||
|
||||
@@ -173,6 +173,20 @@ if [ "$LEGACY" = legacy ] && [ "$DB" != sqlite ]; then
|
||||
ok "older backup refused without --skip-missing-tables, missing tables named"
|
||||
RESTORE_OPTS+=(--skip-missing-tables)
|
||||
fi
|
||||
if [ "$DB" = sqlite ]; then
|
||||
# Another process writing while the restore starts (a cron job that was already running, or
|
||||
# the installer still finishing after it says "installed"): the restore must wait for it, not
|
||||
# stop with "database is locked". Short write transactions on a table of its own, until the
|
||||
# restore is done (/tmp/frt-side-stop; Nextcloud's own commands after it are not under test).
|
||||
docker exec -d -u www-data ngb-frt-restore php -r '
|
||||
$p = new PDO("sqlite:/var/www/html/data/nextcloud.db"); $p->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS frt_side_writer (t REAL)");
|
||||
for ($end = microtime(true) + 300; microtime(true) < $end && !file_exists("/tmp/frt-side-stop");) {
|
||||
$p->beginTransaction(); $p->exec("INSERT INTO frt_side_writer VALUES (" . microtime(true) . ")"); usleep(300000); $p->commit(); usleep(20000);
|
||||
}
|
||||
touch("/tmp/frt-side-done");'
|
||||
sleep 1
|
||||
fi
|
||||
sftp_logins() { docker logs ngb-frt-sftp 2>&1 | grep -c "Accepted password" || true; }
|
||||
[ "$MODE" = sftp ] && LOGINS_BEFORE=$(sftp_logins)
|
||||
OUT=$(run_on ngb-frt-restore backup:restore:full offsite "$SNAP" "${RESTORE_OPTS[@]}" 2>&1) || {
|
||||
@@ -184,6 +198,13 @@ printf '%s\n' "$OUT" | grep -q "maintenance mode while the restore runs" || fail
|
||||
run_on ngb-frt-restore status 2>&1 | grep -q "maintenance: false" || fail "maintenance mode is still on after the restore"
|
||||
run_on ngb-frt-restore config:app:get ng_backup restore_full_started >/dev/null 2>&1 && fail "the restore left its 'started' marker behind"
|
||||
ok "backup:restore:full completed, in maintenance mode and out of it again"
|
||||
if [ "$DB" = sqlite ]; then
|
||||
docker exec ngb-frt-restore touch /tmp/frt-side-stop
|
||||
for i in $(seq 1 20); do docker exec ngb-frt-restore test -f /tmp/frt-side-done && break; sleep 0.5; done
|
||||
WRITES=$(docker exec -u www-data ngb-frt-restore php -r 'echo (new PDO("sqlite:/var/www/html/data/nextcloud.db"))->query("SELECT COUNT(*) FROM frt_side_writer")->fetchColumn();')
|
||||
[ "${WRITES:-0}" -gt 3 ] || fail "the other writer did not run alongside the restore ($WRITES writes)"
|
||||
ok "the restore waited for another process writing to SQLite ($WRITES writes alongside)"
|
||||
fi
|
||||
|
||||
# Everything below must work in a FRESH occ process (a new PHP process per docker exec), since
|
||||
# earlier bugs here only showed up after config.php's secret was rewritten mid-restore.
|
||||
|
||||
Reference in New Issue
Block a user