From 0c12af4e4331285851eb62671c93a01c9d188214 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 17:14:22 -0500 Subject: [PATCH] test(seed): pin reader_lock_acquire in hardlink_clone_into Adds scenario 11, which races a real hardlink_clone_into against a real, concurrent publish-snapshot.sh republish of the exact snapshot being cloned, and asserts the publisher's own log reports entering its reader-drain wait. Deleting reader_lock_acquire left every suite green at the identical 129 assertions (issue #10): the marker it writes is what the publisher's wait_for_readers checks, and with it gone the publisher sees zero readers on its first check and proceeds straight to reclaiming the rotated generation, silently. Kept as its own scenario rather than folded into 8a: 8a already pins exactly one property (the identity check) for exactly one mutant, and the suite's one-scenario-one-mutant diagonal across 8a to 8d and 10 is deliberate. --- scripts/seed-target-dir-selftest.sh | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/scripts/seed-target-dir-selftest.sh b/scripts/seed-target-dir-selftest.sh index dbf85f5..2411ed3 100755 --- a/scripts/seed-target-dir-selftest.sh +++ b/scripts/seed-target-dir-selftest.sh @@ -74,6 +74,17 @@ # onto one set of fingerprints. That is the silent stale-reuse bug the # whole scheme exists to prevent, so the failure has to abort the clone # rather than be swallowed. +# 11. THE CONSUMER'S MARKER ACTUALLY STOPS THE PUBLISHER — reader_lock_acquire +# is exercised (not faked, unlike publish-snapshot-selftest.sh's scenario +# 6) by a real hardlink_clone_into racing a real, concurrent +# publish-snapshot.sh republish of the exact snapshot being cloned. Pins +# that the publisher OBSERVABLY WAITS on this consumer's marker — its own +# log reports entering the drain wait — rather than only that the run +# succeeds, which stayed green with the marker call deleted (issue #10). +# Kept as its own scenario, not folded into 8a, because 8a already pins +# exactly one property (the identity check) for exactly one mutant, and +# the suite's one-scenario-one-mutant diagonal across 8a to 8d and 10 is +# deliberate. set -euo pipefail script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) . "$script_dir/cache-lib.sh" @@ -635,5 +646,81 @@ leftovers=$(find "$root" -maxdepth 1 \( -name '.stage-*' -o -name '.reading-*' \ ok "no staging or reader-marker scratch left behind" rm -f "$scratch/bin/cp" +echo +echo "=== 11: the consumer's marker actually stops the publisher ===" +# hardlink_clone_into's reader_lock_acquire call is exercised here, not +# faked. publish-snapshot-selftest.sh's scenario 6 stands a hand-written +# marker file in for "a consumer whose clone outlasts the grace period" — a +# deliberate simplification that does not need this consumer's clone code at +# all, so it cannot tell reader_lock_acquire apart from no marker existing. +# This scenario forces the two real scripts to race on the same snapshot: a +# consumer hardlink-cloning it, and a publisher republishing it out from +# under that clone, exactly as 8a does to force the identity check — but what +# is pinned here is not the consumer's response to the rotation (8a's +# property), it is that the PUBLISHER, reading the SAME marker this +# consumer's clone wrote, is observed to have entered its drain wait. Deleting +# reader_lock_acquire (issue #10) leaves the marker never written: the +# publisher's live_reader_count sees zero readers on its first check and +# proceeds straight to reclaiming the rotated generation, silently — the run +# still succeeds, and nothing about its own outcome says so, only the absence +# of a line in the publisher's log. +INTLK=$(cache_key feat/interlock-consumer) +INTLK_BASE=$(cache_key release/5) +TAG_INTLK=jobIntlk +rm -rf "$root/snapshot-$INTLK_BASE" "$root/target-$INTLK_BASE" +make_tree "$root/snapshot-$INTLK_BASE" intlk-gen1 +make_tree "$root/target-$INTLK_BASE" intlk-gen2 +intlk_gen1_inode=$(stat -c '%i' "$root/snapshot-$INTLK_BASE") + +cat > "$scratch/bin/cp" < "$scratch/firedIntlk" + rc=0; "$real_cp" "\$@" || rc=\$? + # Concurrently: a real, second publish of the exact snapshot this consumer + # is cloning — the republish that, without the marker this consumer's clone + # holds, would reclaim the generation out from under it. + ( CACHE_READ_GRACE_SECONDS=10 bash "$script_dir/publish-snapshot.sh" "$INTLK_BASE" "$root" pubIntlk \ + > "$scratch/logPubIntlk" 2>&1 + echo \$? > "$scratch/rcPubIntlk" ) & + # Hand control back only once the swap is on disk, so the identity read + # immediately after this cp is guaranteed to resolve to the new generation + # — the same technique 8a uses to force the interleaving rather than hope + # for it. reader_lock_release does not run until AFTER this script exits, + # so the marker stays live for the publisher's whole swap-and-scan. + deadline=\$(( \$(date +%s) + 60 )) + while [ "\$(stat -c '%i' "$root/snapshot-$INTLK_BASE" 2>/dev/null)" = "$intlk_gen1_inode" ]; do + [ "\$(date +%s)" -lt "\$deadline" ] || { echo "stub cp: the publisher never swapped the snapshot" >&2; exit 92; } + sleep 0.05 + done + exit \$rc +fi +exec "$real_cp" "\$@" +EOF +chmod +x "$scratch/bin/cp" + +rcIntlk=0 +seed_with_stub "$INTLK" "$INTLK_BASE" "$root" "$TAG_INTLK" > "$scratch/logIntlk" 2>&1 || rcIntlk=$? +[ -e "$scratch/firedIntlk" ] \ + || fail "the stubbed cp never fired: the consumer never raced the publisher, so this scenario proves nothing" +ok "the consumer's clone raced a real, concurrent republish of its own source" +wait_for_file "$scratch/rcPubIntlk" "the publisher never finished" +[ "$(cat "$scratch/rcPubIntlk")" = "0" ] || { tail -40 "$scratch/logPubIntlk"; fail "publish-snapshot.sh exited non-zero"; } +[ "$rcIntlk" = "0" ] || { tail -40 "$scratch/logIntlk"; fail "the seed exited non-zero"; } +ok "both sides of the race completed" + +# The property under test: not that the run succeeded, but that the publisher +# itself reports having found a live reader and waited on it. This is silent +# in the consumer's own log and in the run's exit status alike — only the +# publisher's log carries it. +grep -q "readers: waiting for 1 in-flight clone(s) of snapshot-${INTLK_BASE}" "$scratch/logPubIntlk" \ + || { tail -40 "$scratch/logPubIntlk"; fail "the publisher never reported waiting on the consumer's reader marker — the interlock did not observably engage"; } +ok "the publisher observably waited on the consumer's own reader marker before reclaiming the rotated snapshot generation" +rm -f "$scratch/bin/cp" + echo echo "seed-target-dir-selftest: ${pass_count} assertions passed"