mirror of
https://github.com/kevinveenbirkenbach/docker-volume-backup.git
synced 2026-07-21 07:54:15 +00:00
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>
45 lines
1.4 KiB
Python
45 lines
1.4 KiB
Python
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()
|