fix(recovery): support batched SQLite blob writes

This commit is contained in:
2026-08-19 19:20:05 +02:00
parent 21122e058e
commit ad55d47645
5 changed files with 374 additions and 65 deletions
+55 -3
View File
@@ -115,9 +115,10 @@ class StorageRecoveryTests(unittest.TestCase):
def put_bytes(backend_self, key, data, *, content_type=None):
with self.Session() as evidence_session:
operation = evidence_session.query(RecoveryOperation).one()
operation = evidence_session.query(RecoveryOperation).one_or_none()
observed_running_operation.append(
operation.status == RecoveryStatus.RUNNING.value
operation is not None
and operation.status == RecoveryStatus.RUNNING.value
and len(operation.request_sha256) == 64
)
put_bytes(key, data, content_type=content_type)
@@ -140,7 +141,14 @@ class StorageRecoveryTests(unittest.TestCase):
operation = self._only_operation()
self.assertEqual(RecoveryStatus.SUCCEEDED.value, operation.status)
self.assertEqual([True], observed_running_operation)
# SQLite uses an explicit caller-transaction mode because it permits
# only one writer. The intent becomes visible with the business commit;
# PostgreSQL retains the independent pre-effect commit guarantee.
self.assertEqual([False], observed_running_operation)
self.assertEqual(
"sqlite_caller_transaction",
operation.metadata_["durability_mode"],
)
self.assertTrue(self.backend.exists(blob.storage_key))
self.assertNotIn("private-name", blob.storage_key)
self.assertEqual(".blob", Path(blob.storage_key).suffix)
@@ -164,10 +172,54 @@ class StorageRecoveryTests(unittest.TestCase):
operation = self._only_operation()
self.assertEqual(RecoveryStatus.RECOVERED.value, operation.status)
self.assertEqual(
"sqlite_post_rollback_reconstruction",
operation.metadata_["durability_mode"],
)
self.assertFalse(self.backend.exists(storage_key))
with self.Session() as evidence_session:
self.assertIsNone(evidence_session.get(FileBlob, blob.id))
def test_multiple_blob_writes_share_one_sqlite_business_transaction(self) -> None:
with patch(
"govoplan_files.backend.storage.files.get_storage_backend",
return_value=self.backend,
):
first = _get_or_create_blob(
self.session,
tenant_id=TENANT_ID,
data=b"first archive member",
filename="first.txt",
content_type="text/plain",
actor_id=USER_ID,
)
second = _get_or_create_blob(
self.session,
tenant_id=TENANT_ID,
data=b"second archive member",
filename="second.txt",
content_type="text/plain",
actor_id=USER_ID,
)
self.session.commit()
self.assertTrue(self.backend.exists(first.storage_key))
self.assertTrue(self.backend.exists(second.storage_key))
with self.Session() as evidence_session:
operations = evidence_session.query(RecoveryOperation).all()
self.assertEqual(2, len(operations))
self.assertEqual(
{RecoveryStatus.SUCCEEDED.value},
{operation.status for operation in operations},
)
self.assertEqual(
{"sqlite_caller_transaction"},
{
operation.metadata_["durability_mode"]
for operation in operations
},
)
def test_post_write_tamper_is_quarantined_and_recovery_required(self) -> None:
with patch(
"govoplan_files.backend.storage.files.get_storage_backend",