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.
This commit is contained in:
@@ -74,6 +74,17 @@
|
|||||||
# onto one set of fingerprints. That is the silent stale-reuse bug the
|
# 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
|
# whole scheme exists to prevent, so the failure has to abort the clone
|
||||||
# rather than be swallowed.
|
# 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
|
set -euo pipefail
|
||||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
. "$script_dir/cache-lib.sh"
|
. "$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"
|
ok "no staging or reader-marker scratch left behind"
|
||||||
rm -f "$scratch/bin/cp"
|
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" <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Fires once, only on the consumer's own top-level hardlink clone — identified
|
||||||
|
# by its destination, the seed's private staging path. Every other cp in the
|
||||||
|
# process tree (the unshare copies, and the publisher's own staging clone)
|
||||||
|
# falls through to the real one.
|
||||||
|
if [ "\${@: -1}" = "$root/.stage-$TAG_INTLK" ] && [ ! -e "$scratch/firedIntlk" ]; then
|
||||||
|
: > "$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
|
||||||
echo "seed-target-dir-selftest: ${pass_count} assertions passed"
|
echo "seed-target-dir-selftest: ${pass_count} assertions passed"
|
||||||
|
|||||||
Reference in New Issue
Block a user