Skip to content

Commit 5a2fe88

Browse files
dzhelezovclaude
andauthored
test(chaos): fault-proxy campaign — byte-identical backfill under injected Portal faults (#149)
* harness: add fault proxy chaos campaign * harness(fault-proxy): committee candor fixes — honest head-scenario + AIMD labels, biome gate Addresses the blind 3-model REQUEST-CHANGES review of the P1 fault-proxy chaos campaign. The core correctness property (digest-identity + interval-tiling, fails closed) is preserved and unweakened; the fixes stop the head-* and AIMD *labels* from overclaiming. 1. biome gate: `biome format --write` the 11 scenario JSONs (semantically identical) and rename the unused `init` param at chaos-aimd-trace.cjs:66 → `_init`, so `biome@2.5.2 check chaos-aimd-trace.cjs scenarios/faultproxy/` exits 0 (error-level). 2. AIMD sparse candor (finding #3): a single gate sample can no longer assert `recovered`. `sparse` (samples < 3) is reported honestly ("sparse — insufficient samples"), never as recovery. AIMD stays SECONDARY — sparse does not fail a scenario whose digest+intervals pass; only a genuine bounds violation (concurrency outside [min,max]) still fails the AIMD gate. 3. head recovery elif false-pass (finding #4): recovery now runs ONLY for head-freeze and ONLY when phase 1 did not complete (done != 1). A failed head_recovery_check propagates instead of retaining a stale pass. `byte-identical-recovery` is reserved for a genuine stall+resume; an unexpected completion labels `byte-identical-completion`. 4. head-freeze label candor (finding #5): honest note — "stalled without corruption; clean restart on the same DB (fault removed) completed byte-identical" — no partial-progress-resume implication. 5. head-regression-100k & head-flap vacuity (findings #1/#2, unanimous): these manipulate the head ABOVE the pinned endBlock so it never intersects the fixed range [20529207,20579207]; a fixed-range historical backfill never rolls back an already-finalized+indexed block, so they prove nothing the clean baseline did. Marked NOT-APPLICABLE (distinct verdict, not a head-tolerance PASS, not a fail) with a note pointing head-reorg/rollback coverage to the stream-mode soak. head-freeze (frozen head BELOW endBlock) remains the one meaningful in-range head scenario. Chosen the ORCHESTRATOR-approved N/A fallback over an in-range redesign because a genuine in-range regression/flap is not achievable non-contrived: regression is anchored to the drifting live head, and an in-range flap only duplicates head-freeze without exercising rollback (a stream-mode property). Verified: biome check EXIT 0 on changed files; bash -n clean; head-freeze re-run on the throwaway PG (54329) PASS byte-identical-recovery, digest == baseline, intervals tile exactly, honest note emitted; N/A routing confirmed by code-trace; proxy.test.mjs 6/6. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * harness(fault-proxy): repo-relative RESULT_FILE/CX_SUMMARY paths (drop hardcoded absolute paths) Env-overridable defaults under $ART (the gitignored artifacts dir) so the harness is portable and carries no machine-specific absolute paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a49d11a commit 5a2fe88

15 files changed

Lines changed: 1027 additions & 6 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
node_modules/
22
harness/results.*.json
33
harness/bench/results.md
4+
harness/chaos/.faultproxy/
45
harness/euler/vaults.*.json
56
*.log
67
.DS_Store

harness/chaos/chaos-aimd-trace.cjs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
const fs = require('node:fs');
2+
3+
const traceFile =
4+
process.env.CHAOS_AIMD_TRACE_FILE || process.env.AIMD_TRACE_FILE || '';
5+
let seq = 0;
6+
let inFlight = 0;
7+
let maxInFlight = 0;
8+
9+
function append(event) {
10+
if (!traceFile) return;
11+
const record = {
12+
ts: new Date().toISOString(),
13+
pid: process.pid,
14+
run: Number(process.env.CHAOS_RUN || 0),
15+
attempt: Number(process.env.CHAOS_ATTEMPT || 0),
16+
seq: ++seq,
17+
...event,
18+
};
19+
try {
20+
fs.appendFileSync(traceFile, `${JSON.stringify(record)}\n`);
21+
} catch {
22+
// Evidence-only hook. Never perturb the app under test.
23+
}
24+
}
25+
26+
function parseGateLine(args) {
27+
const line = args.map((a) => (typeof a === 'string' ? a : '')).join(' ');
28+
const m = line.match(
29+
/\[portalGate\]\s+concurrency_limit=(\d+)\s+active=(\d+)\s+buffered_rows=(\d+)/,
30+
);
31+
if (!m) return;
32+
append({
33+
event: 'aimd-gate-log',
34+
source: 'portalGate-log',
35+
concurrency: Number(m[1]),
36+
limit: Number(m[1]),
37+
active: Number(m[2]),
38+
rows: Number(m[3]),
39+
});
40+
}
41+
42+
const originalLog = console.log;
43+
console.log = function aimdConsoleLog(...args) {
44+
try {
45+
parseGateLine(args);
46+
} catch {
47+
// Ignore trace parse errors.
48+
}
49+
return originalLog.apply(this, args);
50+
};
51+
52+
function isFinalizedStream(input) {
53+
const url =
54+
typeof input === 'string'
55+
? input
56+
: input instanceof URL
57+
? input.href
58+
: input?.url;
59+
return typeof url === 'string' && url.includes('/finalized-stream');
60+
}
61+
62+
const originalFetch = globalThis.fetch;
63+
if (typeof originalFetch !== 'function') {
64+
append({ event: 'aimd-env', error: 'global fetch is unavailable' });
65+
} else {
66+
globalThis.fetch = async function aimdFetch(input, _init) {
67+
const traced = isFinalizedStream(input);
68+
if (!traced) {
69+
return originalFetch.apply(this, arguments);
70+
}
71+
72+
inFlight += 1;
73+
maxInFlight = Math.max(maxInFlight, inFlight);
74+
append({
75+
event: 'aimd-fetch-start',
76+
source: 'fetch-derived',
77+
inFlight,
78+
concurrency: inFlight,
79+
maxInFlight,
80+
});
81+
82+
try {
83+
const response = await originalFetch.apply(this, arguments);
84+
append({
85+
event: 'aimd-fetch-end',
86+
source: 'fetch-derived',
87+
status: response?.status ?? null,
88+
ok: response?.ok ?? null,
89+
inFlight,
90+
concurrency: inFlight,
91+
maxInFlight,
92+
});
93+
return response;
94+
} catch (error) {
95+
append({
96+
event: 'aimd-fetch-error',
97+
source: 'fetch-derived',
98+
error: error instanceof Error ? error.name : 'Error',
99+
message: error instanceof Error ? error.message : String(error),
100+
inFlight,
101+
concurrency: inFlight,
102+
maxInFlight,
103+
});
104+
throw error;
105+
} finally {
106+
inFlight = Math.max(0, inFlight - 1);
107+
append({
108+
event: 'aimd-fetch-release',
109+
source: 'fetch-derived',
110+
inFlight,
111+
concurrency: inFlight,
112+
maxInFlight,
113+
});
114+
}
115+
};
116+
117+
append({
118+
event: 'aimd-env',
119+
source: 'hook',
120+
min: Number(process.env.PORTAL_MIN_CONCURRENCY || 1),
121+
max: Number(process.env.PORTAL_MAX_CONCURRENCY || 32),
122+
start: Number(process.env.PORTAL_START_CONCURRENCY || 4),
123+
});
124+
}
125+
126+
process.on('exit', () => {
127+
append({
128+
event: 'aimd-exit',
129+
source: 'hook',
130+
inFlight,
131+
concurrency: inFlight,
132+
maxInFlight,
133+
});
134+
});

harness/chaos/chaos-pg-driver.sh

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1813,9 +1813,11 @@ backend_label_cmd () {
18131813
echo "$label"
18141814
}
18151815

1816-
case "${1:-campaign}" in
1817-
selftest) selftest ;;
1818-
campaign) campaign ;;
1819-
backend-label) backend_label_cmd ;;
1820-
*) echo "usage: chaos-pg-driver.sh [campaign|selftest|backend-label]"; exit 2 ;;
1821-
esac
1816+
if [ -z "${CHAOS_LIB_ONLY:-}" ]; then
1817+
case "${1:-campaign}" in
1818+
selftest) selftest ;;
1819+
campaign) campaign ;;
1820+
backend-label) backend_label_cmd ;;
1821+
*) echo "usage: chaos-pg-driver.sh [campaign|selftest|backend-label]"; exit 2 ;;
1822+
esac
1823+
fi

0 commit comments

Comments
 (0)