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:
kshitijk4poor
2026-08-24 14:58:17 +05:30
committed by kshitij
parent f93b350711
commit dc50f02090
2 changed files with 52 additions and 0 deletions
+20
View File
@@ -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).