Files
el/engram/test/run_vindex_concurrency_tests.sh
T
bigmerge e99a4640e2 test: regression harness for the vindex concurrency crash
Promotes the two throwaway sanitizer harnesses used to diagnose the
2026-08-16 soul crash into engram/test/ so the bug cannot silently regress.

The harness has two halves and the PAIR is the point — it is what localises
the defect to concurrency rather than to HNSW logic:

  single      3000 clustered vectors, one thread, ASan+UBSan. The CONTROL.
              Must always be clean. During diagnosis this cleared all 13,820
              real dim-768 vectors from the live store, which DISPROVED an
              inspection-derived hypothesis about an out-of-bounds
              reverse-link write at engram_vindex.c:340.

  concurrent  writer + reader on one shared index, TSan. Currently reports a
              race at engram_vindex.c:195 (visited_reset) reached from both
              vindex_search and vindex_insert, because VIndex still owns its
              visited[]/visit_epoch scratch — so even two concurrent READS
              corrupt each other's traversal.

Verified: half 1 passes, half 2 reproduces the race.

Gated on EXPECT_RACE, default 1, so the concurrent half documents the known
defect without failing the suite today. When the visited set moves to a
per-query checkout pool (hnswlib VisitedListPool style — NOT thread_local,
since http_worker is a thread per connection and a __thread buffer would leak
~55KB per connection), flip EXPECT_RACE=0 and it becomes a real gate.
2026-08-16 11:29:17 -05:00

72 lines
2.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# run_vindex_concurrency_tests.sh — regression harness for the 2026-08-16 soul crash.
#
# Runs two halves. The PAIR is the point: the ASan control localises the defect to
# concurrency rather than to HNSW logic. See test_vindex_concurrency.c for the full
# story (SIGSEGV at ASCII address "gramNode", heap corruption in xzm_realloc, etc).
#
# 1. single — ASan+UBSan, one thread. MUST be clean. Always a hard failure.
# 2. concurrent — TSan, writer + reader on one index. Currently EXPECTED to report a
# race at visited_reset, because VIndex still owns its visited[] +
# visit_epoch scratch. Once that moves to a per-query checkout pool
# this must go clean; flip EXPECT_RACE=0 then and it becomes a real
# regression gate.
#
# usage: run_vindex_concurrency_tests.sh
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RUNTIME="$(cd "$HERE/../../lang/runtime" && pwd)"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
SRC="$HERE/test_vindex_concurrency.c"
VINDEX="$RUNTIME/engram_vindex.c"
# Flip to 0 once the visited set is per-query; the concurrent half then becomes a gate.
EXPECT_RACE="${EXPECT_RACE:-1}"
fail=0
echo "== [1/2] single-threaded control under AddressSanitizer =="
cc -std=c11 -g -O1 -fsanitize=address,undefined -fno-omit-frame-pointer \
-I"$RUNTIME" -o "$WORK/single" "$SRC" "$VINDEX" -lm || { echo "BUILD FAILED"; exit 2; }
if ASAN_OPTIONS=detect_leaks=0 "$WORK/single" single; then
echo " -> OK"
else
echo " -> FAIL: the single-threaded control must always be clean."
echo " If this fails the bug is NOT (only) concurrency — look for a real"
echo " out-of-bounds or lifetime error in engram_vindex.c."
fail=1
fi
echo
echo "== [2/2] concurrent writer+reader under ThreadSanitizer =="
cc -std=c11 -g -O1 -fsanitize=thread -fno-omit-frame-pointer \
-I"$RUNTIME" -o "$WORK/conc" "$SRC" "$VINDEX" -lm || { echo "BUILD FAILED"; exit 2; }
tsan_log="$WORK/tsan.log"
TSAN_OPTIONS="halt_on_error=0" "$WORK/conc" concurrent >"$tsan_log" 2>&1
if grep -q "ThreadSanitizer: data race" "$tsan_log"; then
echo " -> RACE DETECTED:"
grep -m1 -A6 "ThreadSanitizer: data race" "$tsan_log" | sed 's/^/ /'
if [ "$EXPECT_RACE" = "1" ]; then
echo " -> EXPECTED (VIndex still owns the shared visited set). Not a failure yet."
echo " Fix = per-query visited buffer (hnswlib VisitedListPool style), then"
echo " re-run with EXPECT_RACE=0."
else
echo " -> REGRESSION: the visited set was supposed to be per-query."
fail=1
fi
else
echo " -> clean"
if [ "$EXPECT_RACE" = "1" ]; then
echo " -> NOTE: no race reported, but EXPECT_RACE=1. Either the fix landed"
echo " (set EXPECT_RACE=0) or the test did not actually interleave."
fi
fi
echo
[ "$fail" -eq 0 ] && echo "RESULT: PASS" || echo "RESULT: FAIL"
exit "$fail"