// ----------------------------------------------------------------------------- // spike-test.js // Sprint: SPRINT_5100_0005_0001_router_chaos_suite // Task: T1 - Load Test Harness // Description: k6 load test for router spike testing and backpressure validation. // ----------------------------------------------------------------------------- import http from 'k6/http'; import { check, sleep } from 'k6'; import { Rate, Trend, Counter } from 'k6/metrics'; // Custom metrics for throttle behavior const throttledRate = new Rate('throttled_requests'); const retryAfterTrend = new Trend('retry_after_seconds'); const recoveryTime = new Trend('recovery_time_ms'); const throttle429Count = new Counter('throttle_429_count'); const throttle503Count = new Counter('throttle_503_count'); const successCount = new Counter('success_count'); export const options = { scenarios: { // Phase 1: Baseline load (normal operation) baseline: { executor: 'constant-arrival-rate', rate: 100, timeUnit: '1s', duration: '1m', preAllocatedVUs: 50, maxVUs: 100, }, // Phase 2: 10x spike spike_10x: { executor: 'constant-arrival-rate', rate: 1000, timeUnit: '1s', duration: '30s', startTime: '1m', preAllocatedVUs: 500, maxVUs: 1000, }, // Phase 3: 50x spike spike_50x: { executor: 'constant-arrival-rate', rate: 5000, timeUnit: '1s', duration: '30s', startTime: '2m', preAllocatedVUs: 2000, maxVUs: 5000, }, // Phase 4: Recovery observation recovery: { executor: 'constant-arrival-rate', rate: 100, timeUnit: '1s', duration: '2m', startTime: '3m', preAllocatedVUs: 50, maxVUs: 100, }, }, thresholds: { // At least 95% of requests should succeed OR return proper throttle response 'http_req_failed{expected_response:true}': ['rate<0.05'], // Throttled requests should have Retry-After header 'throttled_requests': ['rate>0'], // We expect some throttling during spike // Recovery should happen within reasonable time 'recovery_time_ms': ['p(95)<30000'], // 95% recover within 30s // Response time should be bounded even under load 'http_req_duration{expected_response:true}': ['p(95)<5000'], }, }; const ROUTER_URL = __ENV.ROUTER_URL || 'http://localhost:8080'; const API_ENDPOINT = __ENV.API_ENDPOINT || '/api/v1/scan'; export function setup() { console.log(`Testing router at: ${ROUTER_URL}${API_ENDPOINT}`); // Verify router is reachable const healthCheck = http.get(`${ROUTER_URL}/health`); if (healthCheck.status !== 200) { console.warn(`Router health check returned ${healthCheck.status}`); } return { startTime: new Date().toISOString(), routerUrl: ROUTER_URL, }; } export default function () { const payload = JSON.stringify({ image: 'alpine:latest', requestId: `spike-test-${__VU}-${__ITER}`, timestamp: new Date().toISOString(), }); const params = { headers: { 'Content-Type': 'application/json', 'X-Request-ID': `${__VU}-${__ITER}`, }, tags: { expected_response: 'true' }, timeout: '10s', }; const response = http.post(`${ROUTER_URL}${API_ENDPOINT}`, payload, params); // Handle throttle responses (429 Too Many Requests) if (response.status === 429) { throttledRate.add(1); throttle429Count.add(1); // Verify Retry-After header const retryAfter = response.headers['Retry-After']; check(response, { '429 has Retry-After header': (r) => r.headers['Retry-After'] !== undefined, 'Retry-After is valid number': (r) => { const val = r.headers['Retry-After']; return val && !isNaN(parseInt(val)); }, 'Retry-After is reasonable (1-300s)': (r) => { const val = parseInt(r.headers['Retry-After']); return val >= 1 && val <= 300; }, }); if (retryAfter) { retryAfterTrend.add(parseInt(retryAfter)); } } // Handle overload responses (503 Service Unavailable) else if (response.status === 503) { throttledRate.add(1); throttle503Count.add(1); check(response, { '503 has Retry-After header': (r) => r.headers['Retry-After'] !== undefined, }); const retryAfter = response.headers['Retry-After']; if (retryAfter) { retryAfterTrend.add(parseInt(retryAfter)); } } // Handle success responses else { throttledRate.add(0); successCount.add(1); check(response, { 'status is 200 or 202': (r) => r.status === 200 || r.status === 202, 'response has body': (r) => r.body && r.body.length > 0, 'response time < 5s': (r) => r.timings.duration < 5000, }); } // Track any errors if (response.status >= 500 && response.status !== 503) { check(response, { 'no unexpected 5xx errors': () => false, }); } } export function teardown(data) { console.log(`Test completed. Started at: ${data.startTime}`); console.log(`Router URL: ${data.routerUrl}`); } export function handleSummary(data) { const summary = { testRun: { startTime: new Date().toISOString(), routerUrl: ROUTER_URL, }, metrics: { totalRequests: data.metrics.http_reqs ? data.metrics.http_reqs.values.count : 0, throttled429: data.metrics.throttle_429_count ? data.metrics.throttle_429_count.values.count : 0, throttled503: data.metrics.throttle_503_count ? data.metrics.throttle_503_count.values.count : 0, successful: data.metrics.success_count ? data.metrics.success_count.values.count : 0, throttleRate: data.metrics.throttled_requests ? data.metrics.throttled_requests.values.rate : 0, retryAfterAvg: data.metrics.retry_after_seconds ? data.metrics.retry_after_seconds.values.avg : null, retryAfterP95: data.metrics.retry_after_seconds ? data.metrics.retry_after_seconds.values['p(95)'] : null, }, thresholds: data.thresholds, checks: data.metrics.checks ? { passes: data.metrics.checks.values.passes, fails: data.metrics.checks.values.fails, rate: data.metrics.checks.values.rate, } : null, }; return { 'results/spike-test-summary.json': JSON.stringify(summary, null, 2), stdout: textSummary(data, { indent: ' ', enableColors: true }), }; } function textSummary(data, options) { let output = '\n=== Router Spike Test Summary ===\n\n'; const totalReqs = data.metrics.http_reqs ? data.metrics.http_reqs.values.count : 0; const throttled429 = data.metrics.throttle_429_count ? data.metrics.throttle_429_count.values.count : 0; const throttled503 = data.metrics.throttle_503_count ? data.metrics.throttle_503_count.values.count : 0; const successful = data.metrics.success_count ? data.metrics.success_count.values.count : 0; output += `Total Requests: ${totalReqs}\n`; output += `Successful (2xx): ${successful}\n`; output += `Throttled (429): ${throttled429}\n`; output += `Overloaded (503): ${throttled503}\n`; output += `Throttle Rate: ${((throttled429 + throttled503) / totalReqs * 100).toFixed(2)}%\n`; if (data.metrics.retry_after_seconds) { output += `\nRetry-After Header:\n`; output += ` Avg: ${data.metrics.retry_after_seconds.values.avg.toFixed(2)}s\n`; output += ` P95: ${data.metrics.retry_after_seconds.values['p(95)'].toFixed(2)}s\n`; } output += '\nThreshold Results:\n'; for (const [name, result] of Object.entries(data.thresholds || {})) { output += ` ${result.ok ? 'PASS' : 'FAIL'}: ${name}\n`; } return output; }