mirror of
https://github.com/kevinveenbirkenbach/docker-volume-backup.git
synced 2026-07-22 00:14:15 +00:00
fix(restore): wrap the postgres dump replay in a single transaction
The dump replay ran statement-by-statement with autocommit. When restore --empty runs against a LIVE database, the pre-clean drops a table, the replay recreates it and autocommits, and a concurrent writer (discourse's mini_scheduler upserting scheduler_stats(id=1)) inserts the same primary key into the empty table before the dump's COPY loads it. The COPY then aborts with a duplicate-key violation under ON_ERROR_STOP and the whole restore fails. Running the replay with --single-transaction keeps the recreated table invisible to other sessions until commit, so the writer can never insert the racing row. The --empty pre-clean stays multi-statement (\gexec, one DROP per statement): running every DROP in one transaction exhausts max_locks_per_transaction on large schemas (e.g. gitlab). Extract the pre-clean SQL from the inline string into restore/db/empty_preclean.sql (loaded via dirname(__file__)) and declare it as package-data so it ships in the wheel. Add a unit test guarding the single-transaction/multi-statement split and an e2e that reproduces the live-writer race. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
44
tests/unit/restore/test_postgres_single_transaction.py
Normal file
44
tests/unit/restore/test_postgres_single_transaction.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from baudolo.restore.db import postgres as pg_mod
|
||||
|
||||
|
||||
class TestPostgresSingleTransaction(unittest.TestCase):
|
||||
def test_replay_is_single_transaction_but_preclean_is_not(self) -> None:
|
||||
calls = []
|
||||
|
||||
def _capture(container, argv, **kwargs):
|
||||
calls.append(argv)
|
||||
return MagicMock()
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".sql") as sql:
|
||||
sql.write(b"CREATE TABLE t (id int);\nINSERT INTO t VALUES (1);\n")
|
||||
sql.flush()
|
||||
with patch.object(pg_mod, "docker_exec", side_effect=_capture):
|
||||
pg_mod.restore_postgres_sql(
|
||||
container="db",
|
||||
db_name="discourse",
|
||||
user="discourse",
|
||||
password="pw",
|
||||
sql_path=sql.name,
|
||||
empty=True,
|
||||
)
|
||||
|
||||
self.assertEqual(len(calls), 2, f"expected pre-clean + replay: {calls}")
|
||||
preclean, replay = calls[0], calls[1]
|
||||
self.assertNotIn(
|
||||
"--single-transaction",
|
||||
preclean,
|
||||
"pre-clean must stay multi-statement or it exhausts max_locks on large schemas",
|
||||
)
|
||||
self.assertIn(
|
||||
"--single-transaction",
|
||||
replay,
|
||||
"dump replay must be atomic so a live concurrent writer cannot trip a duplicate-key abort",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user