fix(adoption): re-check donor growth at retire time, not just at export
Closes the TOCTOU window flagged in review on #93369 (merged via #93430): the divergence guard compared EXPORT-TIME message counts, but another backend can append donor messages between the export snapshot and the retire loop — that growth would be stamped behind the non-recoverable adopted_by_profile archive, the exact H2 class the guard exists to prevent, just via a narrower race. The retire loop now re-reads live donor vs local counts immediately before end_session and leaves the donor unretired (donor_retired=False, warn-logged) on any donor-ahead signal; the next resume's export-time guard then handles the divergence normally. Equal-count CONTENT divergence (donor rewind+rewrite) remains invisible to count comparison — documented as accepted: bytes stay in the donor store either way. New red-first-verified regression simulates the exact race by appending to the donor from inside an export_session_lineage wrapper. adoption+ownership suites: 25 passed; ruff clean.
This commit is contained in:
@@ -394,6 +394,26 @@ class SessionPortabilityMixin:
|
||||
if not seg_id:
|
||||
continue
|
||||
try:
|
||||
# TOCTOU close-out: the guard above compared EXPORT-TIME
|
||||
# counts, but another backend can append donor messages
|
||||
# between export and this loop. Re-read both stores right
|
||||
# before stamping; a donor-ahead signal here skips the
|
||||
# stamp so growth never lands behind a non-recoverable
|
||||
# archive. (Count comparison cannot see equal-count
|
||||
# CONTENT divergence — e.g. a donor rewind+rewrite; that
|
||||
# residual case is accepted: bytes stay in the donor
|
||||
# store either way, only reachability differs.)
|
||||
donor_now = len(donor_db.get_messages(seg_id))
|
||||
local_now = len(self.get_messages(seg_id))
|
||||
if donor_now > local_now:
|
||||
retire_ok = False
|
||||
logger.warning(
|
||||
"adoption divergence at retire time: donor "
|
||||
"segment %s grew to %d messages (local %d) — "
|
||||
"leaving donor unretired",
|
||||
seg_id, donor_now, local_now,
|
||||
)
|
||||
continue
|
||||
# First end_reason wins in end_session(); reopen first so
|
||||
# the adoption boundary is stamped even on ended segments
|
||||
# (e.g. 'compression' parents).
|
||||
|
||||
Reference in New Issue
Block a user