132 lines
4.4 KiB
Bash
132 lines
4.4 KiB
Bash
#!/usr/bin/env bash
|
|
# test_concurrent_write.sh — verify multi-writer safety with wiki-lock.sh.
|
|
#
|
|
# The critical correctness gate from v1.7 §3.4. Spawns N background workers,
|
|
# each acquires a lock on the same file, appends a uniquely-tagged line, and
|
|
# releases. After all workers exit we verify:
|
|
# - the file received EXACTLY N appended lines (no losses)
|
|
# - every worker's tagged line is present (no silent dropping)
|
|
# - no orphaned lockfiles remain
|
|
# - clear-stale reports 0 leftovers
|
|
#
|
|
# Without wiki-lock.sh, concurrent appends to the same file via `echo >> file`
|
|
# can interleave and corrupt lines on some filesystems. With the lock, only
|
|
# one worker holds the file at a time, and atomic append-then-release prevents
|
|
# corruption.
|
|
#
|
|
# Hermetic: sandbox vault under mktemp, no network.
|
|
#
|
|
# Usage: bash tests/test_concurrent_write.sh
|
|
|
|
set -uo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
LOCK_SH="$ROOT/scripts/wiki-lock.sh"
|
|
|
|
WORKERS=10
|
|
TARGET_FILE_REL="wiki/concepts/Stress.md"
|
|
|
|
SANDBOX=$(mktemp -d /tmp/concurrent-write-test-XXXXXX)
|
|
trap 'rm -rf "$SANDBOX"' EXIT
|
|
mkdir -p "$SANDBOX/.vault-meta/locks" "$SANDBOX/wiki/concepts"
|
|
TARGET_ABS="$SANDBOX/$TARGET_FILE_REL"
|
|
echo "seed" > "$TARGET_ABS"
|
|
|
|
export WIKI_LOCK_VAULT="$SANDBOX"
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
assert_eq() {
|
|
if [ "$2" = "$3" ]; then
|
|
echo "OK $1"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo "FAIL $1: expected '$2', got '$3'"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
}
|
|
|
|
echo "=== test_concurrent_write.sh ==="
|
|
echo "sandbox: $SANDBOX"
|
|
echo "workers: $WORKERS"
|
|
echo "target: $TARGET_FILE_REL"
|
|
echo ""
|
|
|
|
# ── Worker function: acquire lock, append, release ──────────────────────────
|
|
worker() {
|
|
local id="$1"
|
|
local attempts=0
|
|
local max_attempts=50
|
|
# Random jitter so workers don't all hit at the same instant
|
|
local jitter=$(awk -v id="$id" 'BEGIN { srand(id); print int(rand()*100) }')
|
|
# POSIX-portable sub-second sleep via sleep(1) with fractional seconds (GNU/macOS supports it)
|
|
sleep "0.0${jitter}" 2>/dev/null || sleep 1
|
|
|
|
while [ "$attempts" -lt "$max_attempts" ]; do
|
|
if bash "$LOCK_SH" acquire "$TARGET_FILE_REL" >/dev/null 2>&1; then
|
|
# Append our line atomically
|
|
echo "worker-$id-tag" >> "$TARGET_ABS"
|
|
bash "$LOCK_SH" release "$TARGET_FILE_REL" >/dev/null 2>&1
|
|
return 0
|
|
fi
|
|
attempts=$((attempts + 1))
|
|
sleep "0.05" 2>/dev/null || sleep 1
|
|
done
|
|
echo "worker $id gave up after $attempts attempts" >&2
|
|
return 1
|
|
}
|
|
|
|
# ── Spawn workers in parallel ───────────────────────────────────────────────
|
|
PIDS=()
|
|
for i in $(seq 1 $WORKERS); do
|
|
worker "$i" &
|
|
PIDS+=("$!")
|
|
done
|
|
|
|
# Wait for all workers
|
|
FAILED_WORKERS=0
|
|
for pid in "${PIDS[@]}"; do
|
|
if ! wait "$pid"; then
|
|
FAILED_WORKERS=$((FAILED_WORKERS + 1))
|
|
fi
|
|
done
|
|
|
|
assert_eq "all workers completed (no give-ups)" "0" "$FAILED_WORKERS"
|
|
|
|
# ── Verify: file has seed + exactly N tagged lines ──────────────────────────
|
|
TOTAL_LINES=$(wc -l < "$TARGET_ABS")
|
|
assert_eq "total line count (seed + workers)" "$((WORKERS + 1))" "$TOTAL_LINES"
|
|
|
|
# Every worker tag must appear exactly once
|
|
for i in $(seq 1 $WORKERS); do
|
|
COUNT=$(grep -c "^worker-$i-tag$" "$TARGET_ABS" || echo 0)
|
|
if [ "$COUNT" != "1" ]; then
|
|
echo "FAIL worker-$i tag count: expected 1, got $COUNT"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
done
|
|
echo "OK every worker tag appears exactly once"
|
|
PASS=$((PASS + 1))
|
|
|
|
# ── Verify: no orphaned lockfiles ───────────────────────────────────────────
|
|
LIVE_LOCKS=$(bash "$LOCK_SH" list | wc -l)
|
|
assert_eq "no live lockfiles after workers exited" "0" "$LIVE_LOCKS"
|
|
|
|
# ── Verify: clear-stale reports 0 (nothing to reap) ─────────────────────────
|
|
REAPED=$(bash "$LOCK_SH" clear-stale --max-age 0)
|
|
assert_eq "clear-stale reaped count" "0" "$REAPED"
|
|
|
|
# ── Verify: file content sanity (no truncated/garbled lines) ────────────────
|
|
GARBLED=$(awk 'length > 100' "$TARGET_ABS" | wc -l)
|
|
assert_eq "no garbled (overlong) lines" "0" "$GARBLED"
|
|
|
|
echo ""
|
|
echo "Pass: $PASS Fail: $FAIL"
|
|
if [ $FAIL -gt 0 ]; then
|
|
echo "File contents:"
|
|
cat "$TARGET_ABS"
|
|
exit 1
|
|
fi
|
|
echo "All concurrent-write tests passed."
|