Full restore runs in maintenance mode, switched off again at the end (also after an error)

This commit is contained in:
2026-10-22 00:31:34 +02:00
parent 2616b6e543
commit bdcb4f0cae
5 changed files with 30 additions and 2 deletions
+3
View File
@@ -23,6 +23,9 @@ stopped after 13 hours with "Blob not in index"; the backup itself was fine.
backups record no ID counters, and they stayed at 1. Every counter of a restored table now goes backups record no ID counters, and they stayed at 1. Every counter of a restored table now goes
after its data. Found in a disaster recovery simulation of 138 GB from MariaDB to PostgreSQL, after its data. Found in a disaster recovery simulation of 138 GB from MariaDB to PostgreSQL,
where all 15,772 files and every count were identical. where all 15,772 files and every count were identical.
- A full restore now runs in maintenance mode, switched off again at the end (also after an
error): a background job, a page visit or a user writing into the instance meanwhile could
collide with restored rows ("duplicate key") or be lost.
- Adding a location whose repository config could not be read, while backup data is there, - Adding a location whose repository config could not be read, while backup data is there,
no longer creates a new repository over it: it stops with an explanation. A snapshot that no longer creates a new repository over it: it stops with an explanation. A snapshot that
cannot be taken over when a location is added is now logged instead of left out silently. cannot be taken over when a location is added is now logged instead of left out silently.
+2 -1
View File
@@ -94,7 +94,8 @@ tables that do not exist here, install the apps they belong to and run it again,
out with `--skip-missing-tables`. out with `--skip-missing-tables`.
**8. Let the restore run.** It restores the database, then the data directory, and takes hours for **8. Let the restore run.** It restores the database, then the data directory, and takes hours for
a large backup. If it stops halfway, fix the cause and run the same command again with `--force`. a large backup. Nextcloud is in maintenance mode meanwhile, so nobody and no background job
writes into it; the restore switches it off again at the end, also after an error. If it stops halfway, fix the cause and run the same command again with `--force`.
If occ then says that Nextcloud or one of the apps requires an upgrade, the database is already If occ then says that Nextcloud or one of the apps requires an upgrade, the database is already
the old server's: run `occ upgrade` first, then the same command with `--force`. the old server's: run `occ upgrade` first, then the same command with `--force`.
+1
View File
@@ -88,6 +88,7 @@ HELP);
$this->installApp($app, $output); $this->installApp($app, $output);
} }
} }
$output->writeln('Nextcloud is in maintenance mode while the restore runs (switched off again at the end, also after an error).');
$result = $this->restoreFull->restoreFull($target, $snapshot, $skipMissingTables); $result = $this->restoreFull->restoreFull($target, $snapshot, $skipMissingTables);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$output->writeln('<error>' . $e->getMessage() . '</error>'); $output->writeln('<error>' . $e->getMessage() . '</error>');
+21
View File
@@ -30,6 +30,8 @@ final class RestoreFullService {
private const CONNECTION_KEYS = [ private const CONNECTION_KEYS = [
'dbtype', 'dbhost', 'dbport', 'dbname', 'dbuser', 'dbpassword', 'dbpersistent', 'dbtype', 'dbhost', 'dbport', 'dbname', 'dbuser', 'dbpassword', 'dbpersistent',
'dbtableprefix', 'dbdriveroptions', 'datadirectory', 'dbtableprefix', 'dbdriveroptions', 'datadirectory',
// set by restoreFull() itself for the length of the restore
'maintenance',
]; ];
public function __construct( public function __construct(
@@ -91,6 +93,25 @@ final class RestoreFullService {
if (!$check['ok']) { if (!$check['ok']) {
throw new \RuntimeException($this->describeBlockers($check)); throw new \RuntimeException($this->describeBlockers($check));
} }
// Maintenance mode for the whole restore: background jobs (cron.php stops in maintenance
// mode), page visits (AJAX cron) and users must not write into tables or the data
// directory while they are being replaced; a row written in between would collide with a
// restored one or be silently lost. Switched off again at the end, also after an error:
// occ only offers app commands outside maintenance mode, and the guide's next step is
// to run the same restore again with --force.
$wasInMaintenance = $this->config->getSystemValueBool('maintenance', false);
$this->config->setSystemValue('maintenance', true);
try {
return $this->restoreInMaintenance($target, $snapshotId, $skipMissingTables, $check);
} finally {
if (!$wasInMaintenance) {
$this->config->setSystemValue('maintenance', false);
}
}
}
/** @return array{dbReport:array, dataFiles:int, configKeys:int, dbConsistent:bool, missingApps:list<string>} */
private function restoreInMaintenance(Target $target, string $snapshotId, bool $skipMissingTables, array $check): array {
return $this->withSharedLock($target, function (callable $refresh) use ($target, $snapshotId, $skipMissingTables, $check) { return $this->withSharedLock($target, function (callable $refresh) use ($target, $snapshotId, $skipMissingTables, $check) {
$beat = LeaseService::throttled($refresh); $beat = LeaseService::throttled($refresh);
$repo = $this->targets->repository($target); $repo = $this->targets->repository($target);
+3 -1
View File
@@ -172,7 +172,9 @@ OUT=$(run_on ngb-frt-restore backup:restore:full offsite "$SNAP" "${RESTORE_OPTS
docker exec ngb-frt-restore sh -c 'tail -c 6000 /var/www/html/data/nextcloud.log' || true docker exec ngb-frt-restore sh -c 'tail -c 6000 /var/www/html/data/nextcloud.log' || true
fail "restore:full" fail "restore:full"
} }
ok "backup:restore:full completed" printf '%s\n' "$OUT" | grep -q "maintenance mode while the restore runs" || fail "the restore did not say it runs in maintenance mode"
run_on ngb-frt-restore status 2>&1 | grep -q "maintenance: false" || fail "maintenance mode is still on after the restore"
ok "backup:restore:full completed, in maintenance mode and out of it again"
# Everything below must work in a FRESH occ process (a new PHP process per docker exec), since # 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. # earlier bugs here only showed up after config.php's secret was rewritten mid-restore.