Skip to content

Commit 345926f

Browse files
committed
test(scripts): cover busy → timeout path via direct Redis manipulation
Script 15 exercises mutateWithFallback's safety-net cap: HSET-forces the entry into each of the three busy-triggering states (DRAINING, FAILED, materialised=true) and verifies the mutation API returns 503 within the ~2s safetyNetMs window. Also asserts the wait is bounded — fails if the response comes back faster than 1s (would imply busy wasn't hit) or slower than 5s (would imply the wait is unbounded). The remaining uncovered slice of busy is the "drainer succeeds mid-wait and pgMutation runs" branch, which requires injecting a PG row from outside the webapp during the wait window. Documented as unit-test-only.
1 parent c6f741a commit 345926f

2 files changed

Lines changed: 89 additions & 4 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env bash
2+
# 15 — mutateWithFallback "busy" path → safety-net timeout → 503.
3+
# When mutateSnapshot returns busy (entry DRAINING / FAILED /
4+
# materialised=true) the helper polls the PG writer for ~2s, then
5+
# 503s if the row never materialises. We force the busy state by
6+
# HSET-ing the entry hash directly, then call a mutation endpoint
7+
# and expect 503 within ~2.5s.
8+
#
9+
# Required: drainer OFF (so the entry stays in whatever state we set).
10+
# : redis-cli or `docker exec redis redis-cli`.
11+
12+
source "$(dirname "$0")/00-lib.sh"
13+
14+
header "mutateWithFallback busy → safety-net timeout"
15+
16+
if [[ -z "${REDIS_CLI:-}" ]]; then
17+
if command -v redis-cli >/dev/null 2>&1; then
18+
REDIS_CLI=(redis-cli)
19+
elif docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^redis$'; then
20+
REDIS_CLI=(docker exec -i redis redis-cli)
21+
else
22+
fail "no redis-cli; set REDIS_CLI='docker exec -i NAME redis-cli'"
23+
summary
24+
fi
25+
else
26+
read -ra REDIS_CLI <<< "$REDIS_CLI"
27+
fi
28+
29+
# Test each of the three "busy" trigger states. Each one buffers a fresh
30+
# run, mutates the entry into the target state via redis-cli, then calls
31+
# a mutation API and expects 503 (not 5xx, not 200 — explicit timeout).
32+
test_busy_state() {
33+
local label=$1 hset_args=("${@:2}")
34+
35+
BUFFERED_ID=$(capture_buffered_run_id)
36+
if [[ -z "$BUFFERED_ID" ]]; then
37+
fail "[$label] could not buffer a run"
38+
return
39+
fi
40+
41+
# Verify the entry is initially mutable.
42+
api POST "/api/v1/runs/$BUFFERED_ID/tags" '{"tags":["pre-busy"]}'
43+
if ! last_status_ok; then
44+
fail "[$label] pre-busy tags status=$(cat "$WORK/last.status")"
45+
return
46+
fi
47+
48+
# Force the busy state.
49+
"${REDIS_CLI[@]}" HSET "mollifier:entries:$BUFFERED_ID" "${hset_args[@]}" >/dev/null
50+
info "[$label] HSET ${hset_args[*]} on $BUFFERED_ID"
51+
52+
# Fire a mutation. Should 503 after ~2s of polling.
53+
local t0 t1
54+
t0=$(date +%s)
55+
api POST "/api/v1/runs/$BUFFERED_ID/tags" '{"tags":["during-busy"]}'
56+
t1=$(date +%s)
57+
local elapsed=$((t1 - t0))
58+
local status
59+
status=$(cat "$WORK/last.status")
60+
61+
if [[ "$status" == "503" ]]; then
62+
pass "[$label] returned 503 in ${elapsed}s (expected ~2s)"
63+
else
64+
fail "[$label] expected 503, got $status in ${elapsed}s — body=$(last_body | head -c 200)"
65+
fi
66+
67+
if (( elapsed >= 1 && elapsed <= 5 )); then
68+
pass "[$label] wait time in [1, 5]s window (safetyNetMs=2000)"
69+
else
70+
fail "[$label] wait time ${elapsed}s outside expected [1, 5]s window"
71+
fi
72+
}
73+
74+
header "busy state 1: status=DRAINING"
75+
test_busy_state "DRAINING" status DRAINING
76+
77+
header "busy state 2: status=FAILED"
78+
test_busy_state "FAILED" status FAILED
79+
80+
header "busy state 3: materialised=true"
81+
test_busy_state "materialised" materialised true
82+
83+
summary

scripts/mollifier-challenge/README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,12 @@ export TASK_ID=hello-world
4646
| 12 | `12-state3-replay.sh` | OFF + redis-cli | Direct Redis HSET status=FAILED to manufacture state 3 (Q2). Replay still produces a fresh run. |
4747
| 13 | `13-resume-parent-guard.sh` | OFF | triggerAndWait with an idempotency key matching a buffered run produces a fresh PG run (B6b guard). |
4848
| 14 | `14-dashboard-routes.sh` | OFF + session cookie | D1 cancel, D2 replay, D3 idempotencyKey reset via the `/resources/...` dashboard routes (session-cookie auth). |
49+
| 15 | `15-busy-timeout.sh` | OFF + redis-cli | Force entry into DRAINING / FAILED / materialised=true via direct HSET; verify the mutation API returns 503 after the ~2s safety-net wait. |
4950

5051
**Toggling the drainer:** restart the webapp with `TRIGGER_MOLLIFIER_DRAINER_ENABLED=1`
5152
for scripts that need it. Scripts 05 and 06 are the only ones that need it ON.
5253

53-
**Script 12 prerequisites:** sets `REDIS_CLI` env var, or has `redis-cli` on PATH,
54+
**Script 12 / 15 prerequisites:** sets `REDIS_CLI` env var, or has `redis-cli` on PATH,
5455
or a docker container named `redis` reachable via `docker exec`.
5556

5657
**Script 14 prerequisites:** session-cookie value (the `__session` cookie from a
@@ -72,9 +73,10 @@ These behaviours exist in the implementation but aren't covered by the bash
7273
suite. They're documented here so future readers know what's checked elsewhere
7374
vs what's genuinely uncovered.
7475

75-
- **`mutateWithFallback` "busy" wait-and-bounce path.** Triggers only when an
76-
entry is in DRAINING state — racy from bash since only the drainer can flip
77-
the status. Covered by unit tests in `apps/webapp/test/mollifierMutateWithFallback.test.ts`.
76+
- **`mutateWithFallback` busy → PG-row-arrives-mid-wait path.** Script 15 covers
77+
the timeout side of busy. The "drainer succeeds while the API is waiting"
78+
side requires injecting a PG row mid-flight; covered by unit tests in
79+
`apps/webapp/test/mollifierMutateWithFallback.test.ts`.
7880
- **Buffer outage / fail-open.** Stopping Redis takes down the run engine,
7981
queue, and locks too — the system can't function for a meaningful end-to-end
8082
scenario. Covered by unit tests that pass a buffer that throws.

0 commit comments

Comments
 (0)