docs(readme): describe how the concurrency scenarios actually work now
README's Development section stated the repo's methodology for writing concurrency scenarios as "gate the interfering step on observed progress of the step it interferes with, so the window is hit deterministically". That described the progress poll scenario 8 used, which this branch removes — and the property it claims is precisely what issue #3 records as false: observing that a walk has started says nothing about where it will be when the interference lands. Left standing it would tell the next contributor to build the next scenario the way this one had to be rewritten. Replaced with what the suites do: stub, on PATH, a command the code under test calls at a known point, so placement is a fact rather than a scheduling outcome; assert the stub fired; and assert which guard caught the fault where more than one could. Also: the seed suite's table row now names both tear modes, and publish-snapshot-selftest.sh's cross-reference points at 8a and 8b rather than a scenario 8 that no longer exists. (publish-snapshot.sh's similar mislabel predates this branch and is left alone.) The stub directory and the real-cp lookup move up next to seed_with_stub, so 8b no longer depends on setup buried in 8a's block and either scenario can be run or mutated alone.
This commit is contained in:
@@ -361,17 +361,35 @@ bash scripts/selftest.sh --fast # fixture-only suites, no compiler
|
|||||||
| suite | covers |
|
| suite | covers |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `hardlink-clone-selftest.sh` | that a build in a clone cannot mutate its source — with a control proving a raw `cp -al` does. Needs a real compiler. |
|
| `hardlink-clone-selftest.sh` | that a build in a clone cannot mutate its source — with a control proving a raw `cp -al` does. Needs a real compiler. |
|
||||||
| `seed-target-dir-selftest.sh` | seed-source preference, lock-file stripping, two jobs racing on one cache key, **and a seed racing a publisher's rotation of the source it is reading** — the race that actually truncates a tree |
|
| `seed-target-dir-selftest.sh` | seed-source preference, lock-file stripping, two jobs racing on one cache key, **and a seed whose source is rotated — or silently loses a subtree — underneath its clone**: the two ways a hardlink clone tears |
|
||||||
| `publish-snapshot-selftest.sh` | the atomic swap, that a live consumer survives a republish, and the publisher's side of the rotation race: deferred reclamation under a live reader, and its sweep once the reader is gone |
|
| `publish-snapshot-selftest.sh` | the atomic swap, that a live consumer survives a republish, and the publisher's side of the rotation race: deferred reclamation under a live reader, and its sweep once the reader is gone |
|
||||||
| `prune-cache-selftest.sh` | liveness, protection, locking, eviction order, self-clear, **and that a cache a job claims *inside* the check-to-unlink window survives it** — against a real scratch `origin` |
|
| `prune-cache-selftest.sh` | liveness, protection, locking, eviction order, self-clear, **and that a cache a job claims *inside* the check-to-unlink window survives it** — against a real scratch `origin` |
|
||||||
| `restore-mtimes-selftest.sh` | the merge hazard and the watermark that closes it, including the two-jobs-one-namespace case. Needs a real compiler. |
|
| `restore-mtimes-selftest.sh` | the merge hazard and the watermark that closes it, including the two-jobs-one-namespace case. Needs a real compiler. |
|
||||||
|
|
||||||
Every suite runs the actual script, not a reimplementation of its logic, and
|
Every suite runs the actual script, not a reimplementation of its logic, and
|
||||||
every fix scenario is paired with a control that reproduces the bug — a
|
every fix scenario is paired with a control that reproduces the bug — a
|
||||||
scenario that passes either way proves nothing. The concurrency scenarios race
|
scenario that passes either way proves nothing.
|
||||||
real processes rather than mocking the interleaving, and gate the interfering
|
|
||||||
step on *observed* progress of the step it interferes with, so the window is
|
The concurrency scenarios run the real scripts as real concurrent processes,
|
||||||
hit deterministically instead of on a fast machine's coin flip.
|
but they never race for the interleaving. The interfering step is placed
|
||||||
|
inside the window by stubbing, on `PATH`, a command the code under test calls
|
||||||
|
at a known point: `prune-cache-selftest.sh` scenario 12 stubs `du`, so the
|
||||||
|
pass's own measurement publishes a reader marker strictly between its check
|
||||||
|
and its unlink; `seed-target-dir-selftest.sh` scenarios 8a and 8b stub `cp`,
|
||||||
|
so the consumer's own clone is what rotates the snapshot underneath it, or
|
||||||
|
what loses a subtree of its own source, strictly inside the identity window.
|
||||||
|
Placement is then a fact rather than a scheduling outcome — and each stub
|
||||||
|
asserts that it fired, because a scenario whose interference silently did not
|
||||||
|
happen passes for the wrong reason.
|
||||||
|
|
||||||
|
Gating the interfering step on *observed progress* of the step it interferes
|
||||||
|
with was the earlier answer here, and it is not one: seeing that a walk has
|
||||||
|
started says nothing about where it will be when the interference lands, so
|
||||||
|
the assertion downstream held only some of the time (issue #3).
|
||||||
|
|
||||||
|
Where more than one guard could catch a fault, the scenario asserts *which*
|
||||||
|
one did — otherwise deleting the guard under test leaves the suite green
|
||||||
|
because a sibling fires in its place.
|
||||||
|
|
||||||
The action YAML holds no logic beyond wiring; everything testable lives in
|
The action YAML holds no logic beyond wiring; everything testable lives in
|
||||||
`scripts/`. A composite action needs `shell: bash` on every `run:` step, and
|
`scripts/`. A composite action needs `shell: bash` on every `run:` step, and
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
# that marker must still defer.
|
# that marker must still defer.
|
||||||
#
|
#
|
||||||
# The consumer's half of the same race — a seed catching a rotation mid-clone
|
# The consumer's half of the same race — a seed catching a rotation mid-clone
|
||||||
# — is in seed-target-dir-selftest.sh scenario 8.
|
# — is in seed-target-dir-selftest.sh scenarios 8a and 8b.
|
||||||
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"
|
||||||
|
|||||||
@@ -85,9 +85,13 @@ make_tree() {
|
|||||||
|
|
||||||
seed() { bash "$script_dir/seed-target-dir.sh" "$@" > "$scratch/log" 2>&1 || { tail -40 "$scratch/log"; fail "seed-target-dir.sh exited non-zero"; }; }
|
seed() { bash "$script_dir/seed-target-dir.sh" "$@" > "$scratch/log" 2>&1 || { tail -40 "$scratch/log"; fail "seed-target-dir.sh exited non-zero"; }; }
|
||||||
|
|
||||||
# The same script, with the scenario-8 stub directory ahead of the real
|
# The same script, with the stub directory scenarios 8a and 8b write into
|
||||||
# coreutils on PATH. Kept separate so no other scenario can pick a stub up by
|
# ahead of the real coreutils on PATH. Kept separate so no other scenario can
|
||||||
# accident, and so the caller keeps the exit status instead of aborting on it.
|
# pick a stub up by accident, and so the caller keeps the exit status instead
|
||||||
|
# of aborting on it. Both halves live here rather than in the first scenario
|
||||||
|
# that needs them, so either scenario can be run, moved or mutated alone.
|
||||||
|
mkdir -p "$scratch/bin"
|
||||||
|
real_cp=$(command -v cp)
|
||||||
seed_with_stub() { PATH="$scratch/bin:$PATH" bash "$script_dir/seed-target-dir.sh" "$@"; }
|
seed_with_stub() { PATH="$scratch/bin:$PATH" bash "$script_dir/seed-target-dir.sh" "$@"; }
|
||||||
|
|
||||||
# Always succeeds and always prints a number: a directory that does not exist
|
# Always succeeds and always prints a number: a directory that does not exist
|
||||||
@@ -256,8 +260,6 @@ gen1_inode=$(stat -c '%i' "$root/snapshot-$BASE_KEY")
|
|||||||
[ "$gen1_entries" -ne "$(tree_entries "$root/target-$BASE_KEY")" ] \
|
[ "$gen1_entries" -ne "$(tree_entries "$root/target-$BASE_KEY")" ] \
|
||||||
|| fail "both generations have ${gen1_entries} entries — this fixture cannot tell them apart"
|
|| fail "both generations have ${gen1_entries} entries — this fixture cannot tell them apart"
|
||||||
|
|
||||||
mkdir -p "$scratch/bin"
|
|
||||||
real_cp=$(command -v cp)
|
|
||||||
cat > "$scratch/bin/cp" <<EOF
|
cat > "$scratch/bin/cp" <<EOF
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Fires once, and only on the consumer's own top-level hardlink clone —
|
# Fires once, and only on the consumer's own top-level hardlink clone —
|
||||||
|
|||||||
Reference in New Issue
Block a user