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"