#!/usr/bin/env node /** * Lightweight soak/load probe for staging (Phase 3). * Does not require k6 — uses Node fetch against /health, /ready, /metrics. * * Usage: * API_BASE=https://staging.example.com node scripts/load/soak-probe.mjs * DURATION_SEC=60 CONCURRENCY=8 node scripts/load/soak-probe.mjs */ const base = (process.env.API_BASE ?? 'http://127.0.0.1:4000').replace(/\/$/, '') const durationSec = Number(process.env.DURATION_SEC ?? 30) const concurrency = Number(process.env.CONCURRENCY ?? 4) const paths = (process.env.SOAK_PATHS ?? '/health,/ready,/metrics').split(',').map((p) => p.trim()) const stats = { ok: 0, fail: 0, statuses: /** @type {Record} */ ({}), latencies: /** @type {number[]} */ ([]), } async function oneRequest(path) { const started = Date.now() try { const res = await fetch(`${base}${path}`, { method: 'GET' }) const ms = Date.now() - started stats.latencies.push(ms) const key = String(res.status) stats.statuses[key] = (stats.statuses[key] ?? 0) + 1 if (res.status >= 200 && res.status < 400) stats.ok += 1 else stats.fail += 1 } catch { stats.fail += 1 stats.statuses.error = (stats.statuses.error ?? 0) + 1 } } async function worker(deadline) { let i = 0 while (Date.now() < deadline) { const path = paths[i % paths.length] i += 1 await oneRequest(path) } } function percentile(sorted, p) { if (!sorted.length) return 0 const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1)) return sorted[idx] } const deadline = Date.now() + durationSec * 1000 console.log(`Soak probe ${base} duration=${durationSec}s concurrency=${concurrency} paths=${paths.join(',')}`) await Promise.all(Array.from({ length: concurrency }, () => worker(deadline))) const sorted = [...stats.latencies].sort((a, b) => a - b) const total = stats.ok + stats.fail console.log(JSON.stringify({ total, ok: stats.ok, fail: stats.fail, errorRate: total ? Number((stats.fail / total).toFixed(4)) : 0, p50_ms: percentile(sorted, 50), p95_ms: percentile(sorted, 95), p99_ms: percentile(sorted, 99), statuses: stats.statuses, }, null, 2)) if (stats.fail / Math.max(total, 1) > Number(process.env.MAX_ERROR_RATE ?? 0.05)) { console.error('Soak probe FAILED: error rate above MAX_ERROR_RATE') process.exit(1) } console.log('Soak probe PASSED')