fix(tolaria): allow propagate_commit resume from PROPAGATING_TOLARIA crash-window
C5F canary commit parked in PROPAGATING_TOLARIA with objects already written to Tolaria (real write before crash). C5EEngine.replay() delegates the pending case to propagate_commit, whose entry guard only accepted READY/RETRY_PENDING, so the commit could never resume past the crash window and the ALREADY_AT_TARGET idempotency (pre_write_drift_check) was never reached. Minimal fix: accept PROPAGATING_TOLARIA as a resume entry state (idempotency still determined per-object via pre_write_drift_check -> no double write; read-back verify() remains the mandatory gate) and skip the READY->PROPAGATING_TOLARIA transition on resume (no self-transition entry exists in _ALLOWED_TRANSITIONS). Adds 2 regression tests covering the crash-window resume (already-at-target and pending-create). Full C5A-E suite: 190 tests, 0 failures.
This commit is contained in:
parent
c5b3db153a
commit
f451283276
2 changed files with 57 additions and 4 deletions
|
|
@ -584,12 +584,19 @@ class C5CPropagator:
|
|||
"reason_code": RC_UNKNOWN_OBJECT_ID}
|
||||
|
||||
cur = commit.get("status")
|
||||
# Nur aus READY oder RETRY_PENDING propagieren
|
||||
if cur not in (ST_READY, ST_RETRY_PENDING):
|
||||
# Resume-Einstieg: READY / RETRY_PENDING (Erst-/Retry-Propagation) UND
|
||||
# PROPAGATING_TOLARIA (Crash-Window-Resume: Commit steckte waehrend der
|
||||
# Propagation fest, Objekte evtl. teilweise propagiert). Idempotenz wird
|
||||
# pro Objekt via pre_write_drift_check (ALREADY_AT_TARGET) bestimmt,
|
||||
# kein Doppel-Write. Read-Back bleibt zwingend (verify / content_equal).
|
||||
if cur not in (ST_READY, ST_RETRY_PENDING, ST_PROPAGATING_TOLARIA):
|
||||
return {"commit_sha": commit_sha, "status": cur,
|
||||
"message": "Commit nicht im propagierbaren Zustand"}
|
||||
|
||||
# READY -> PROPAGATING_TOLARIA
|
||||
# READY -> PROPAGATING_TOLARIA (nur beim Erst-/Retry-Einstieg; beim
|
||||
# PROPAGATING_TOLARIA-Resume ist der State bereits gesetzt, kein
|
||||
# Self-Transition-Entry noetig, der nicht in _ALLOWED_TRANSITIONS ist)
|
||||
if cur != ST_PROPAGATING_TOLARIA:
|
||||
self.store.transition_commit(commit_sha, ST_PROPAGATING_TOLARIA)
|
||||
|
||||
parent_sha = commit.get("parent_sha")
|
||||
|
|
|
|||
|
|
@ -832,6 +832,52 @@ class TestCommitAtomicity(unittest.TestCase):
|
|||
self.assertEqual(res2["status"], ST_UPDATING_SEARCH)
|
||||
self.assertEqual(self.fake.write_count, writes_after_first) # kein Doppel-Write
|
||||
|
||||
def test_resume_from_propagating_tolaria_idempotent(self):
|
||||
# Crash-Window-Resume (C5E §4): Commit steckte in PROPAGATING_TOLARIA fest,
|
||||
# die Objekte sind bereits (vom echten Write vor dem Crash) in Tolaria am
|
||||
# Ziel. propagate_commit MUSS den Einstieg aus PROPAGATING_TOLARIA erlauben,
|
||||
# via pre_write_drift_check ALREADY_AT_TARGET erkennen (kein Doppel-Write)
|
||||
# und deterministisch VERIFYING_TOLARIA -> UPDATING_SEARCH erreichen.
|
||||
content = _fm(UUID_A, state="current") + "body"
|
||||
self.repo.write("modul-09.md", content)
|
||||
sha = self.repo.commit("create")
|
||||
_seed_commit(self.store, sha, None, [{
|
||||
"object_id": UUID_A, "operation": OP_CREATE,
|
||||
"path_before": None, "path_after": "modul-09.md",
|
||||
"content_hash_after": content_hash(parse_frontmatter(content)[1]),
|
||||
}])
|
||||
# Crash-Window simuliert: Commit bereits in PROPAGATING_TOLARIA, Objekt
|
||||
# bereits (vom echten Write vor dem Crash) am Ziel in Tolaria.
|
||||
self.store.transition_commit(sha, ST_PROPAGATING_TOLARIA)
|
||||
self.fake.vault["/app/vault/modul-09.md"] = content
|
||||
writes_before = self.fake.write_count
|
||||
res = self.prop.propagate_commit(sha)
|
||||
self.assertEqual(res["status"], ST_UPDATING_SEARCH,
|
||||
"Resume aus PROPAGATING_TOLARIA muss zum Suchschritt weiterlaufen")
|
||||
self.assertEqual(self.fake.write_count, writes_before, "kein Doppel-Write im Crash-Window")
|
||||
self.assertEqual(self.store.commit_status(sha), ST_UPDATING_SEARCH)
|
||||
|
||||
def test_resume_from_propagating_tolaria_with_pending_create(self):
|
||||
# Crash-Window-Resume mit noch NICHT geschriebenem Objekt: Commit in
|
||||
# PROPAGATING_TOLARIA, Objekt fehlt in Tolaria (CREATE-Fall) -> WRITE_ALLOWED,
|
||||
# wird nachgeholt, danach Read-Back -> VERIFYING_TOLARIA -> UPDATING_SEARCH.
|
||||
content = _fm(UUID_A, state="current") + "body"
|
||||
self.repo.write("modul-09.md", content)
|
||||
sha = self.repo.commit("create")
|
||||
_seed_commit(self.store, sha, None, [{
|
||||
"object_id": UUID_A, "operation": OP_CREATE,
|
||||
"path_before": None, "path_after": "modul-09.md",
|
||||
"content_hash_after": content_hash(parse_frontmatter(content)[1]),
|
||||
}])
|
||||
self.store.transition_commit(sha, ST_PROPAGATING_TOLARIA)
|
||||
# Tolaria enthaelt das Objekt noch NICHT (CREATE pending).
|
||||
writes_before = self.fake.write_count
|
||||
res = self.prop.propagate_commit(sha)
|
||||
self.assertEqual(res["status"], ST_UPDATING_SEARCH,
|
||||
"Resume mit pending CREATE muss nachpropagieren und abschliessen")
|
||||
self.assertEqual(self.fake.write_count, writes_before + 1, "pending Objekt wird nachgeschrieben")
|
||||
self.assertEqual(self.fake.vault["/app/vault/modul-09.md"], content)
|
||||
|
||||
|
||||
class TestGuarantees(unittest.TestCase):
|
||||
"""No-Search / No-Master-Write Guarantee (statisch)."""
|
||||
|
|
|
|||
Loading…
Reference in a new issue