fashionistar-celery-queues / k6 /catalog_load_test.js
FASHIONISTAR CI/CD
🔄 Celery Queues Deploy: 4aae6106b1530c3cc7d7d64f5e3c3e1d5015691d [GitHub Actions]
27c799c
Raw
History Blame
7.52 kB
/**
* k6/catalog_load_test.js
* FASHIONISTAR — K6 Load Test Campaign
* Phase 10 / Criterion E: 100k RPS sustained load
*
* Stages:
* 0 → 1k VUs over 2 min (warm-up)
* 1k → 10k VUs over 5 min (ramp)
* 10k → 100k VUs over 10 min (peak)
* 100k → 0 VUs over 2 min (cool-down)
*
* Thresholds:
* p95 response time < 100ms
* p99 response time < 200ms
* Error rate < 0.1%
*
* Usage:
* k6 run --env API_BASE=https://api.fashionistar.ng k6/catalog_load_test.js
*/
import http from "k6/http";
import { check, group, sleep } from "k6";
import { Rate, Trend, Counter } from "k6/metrics";
// ── Custom metrics ────────────────────────────────────────────────────────────
const errorRate = new Rate("error_rate");
const authLatency = new Trend("auth_latency_ms", true);
const catalogLatency = new Trend("catalog_latency_ms", true);
const orderLatency = new Trend("order_latency_ms", true);
const failedRequests = new Counter("failed_requests");
// ── Configuration ─────────────────────────────────────────────────────────────
const API_BASE = __ENV.API_BASE || "http://localhost:8000";
export const options = {
stages: [
{ duration: "2m", target: 1_000 }, // Warm-up: 0 → 1k VUs
{ duration: "5m", target: 10_000 }, // Ramp: 1k → 10k VUs
{ duration: "10m", target: 100_000 }, // Peak: 10k → 100k VUs
{ duration: "2m", target: 0 }, // Cool-down
],
thresholds: {
// SLA targets
"http_req_duration": ["p(95)<100", "p(99)<200"],
"http_req_failed": ["rate<0.001"],
// Custom metrics
"catalog_latency_ms": ["p(95)<80"],
"auth_latency_ms": ["p(95)<150"],
"error_rate": ["rate<0.01"],
},
ext: {
loadimpact: {
projectID: 0,
name: "FASHIONISTAR — 100k RPS Campaign",
},
},
};
// ── Shared headers ────────────────────────────────────────────────────────────
const HEADERS = {
"Content-Type": "application/json",
"Accept": "application/json",
"X-Load-Test": "1",
};
// ── Auth token cache (per VU, refreshed every 50 iterations) ─────────────────
let _authToken = "";
let _tokenIteration = 0;
function getToken() {
const iteration = __ITER;
if (_authToken && iteration - _tokenIteration < 50) return _authToken;
const res = http.post(
`${API_BASE}/api/v1/auth/login/`,
JSON.stringify({
email: `loadtest+${__VU}@fashionistar.ng`,
password: "LoadTest!2026",
}),
{ headers: HEADERS, timeout: "10s" }
);
authLatency.add(res.timings.duration);
if (res.status === 200) {
try {
const body = JSON.parse(res.body);
_authToken = body.access || "";
_tokenIteration = iteration;
} catch (_) { /* silent */ }
}
return _authToken;
}
// ── Scenario: Anonymous catalog browsing ─────────────────────────────────────
function scenarioCatalog() {
group("Catalog — Anonymous", () => {
// Product list (most common request)
const listRes = http.get(
`${API_BASE}/api/v1/ninja/products/?page=1&page_size=20`,
{ headers: HEADERS, timeout: "5s" }
);
catalogLatency.add(listRes.timings.duration);
const ok = check(listRes, {
"product list 200": (r) => r.status === 200,
"product list < 100ms": (r) => r.timings.duration < 100,
});
if (!ok) { errorRate.add(1); failedRequests.add(1); } else { errorRate.add(0); }
// Featured products
const featRes = http.get(
`${API_BASE}/api/v1/ninja/products/?featured=true&page_size=10`,
{ headers: HEADERS, timeout: "5s" }
);
catalogLatency.add(featRes.timings.duration);
check(featRes, { "featured 200": (r) => r.status === 200 });
// Category tree
const catRes = http.get(
`${API_BASE}/api/v1/ninja/catalog/categories/`,
{ headers: HEADERS, timeout: "5s" }
);
check(catRes, { "categories 200": (r) => r.status === 200 });
// Health check (load balancer probe simulation)
const healthRes = http.get(
`${API_BASE}/api/v1/health/`,
{ headers: HEADERS, timeout: "3s" }
);
check(healthRes, { "health 200": (r) => r.status === 200 });
});
}
// ── Scenario: Authenticated user flow ────────────────────────────────────────
function scenarioAuthenticated() {
const token = getToken();
if (!token) return;
const authHeaders = { ...HEADERS, "Authorization": `Bearer ${token}` };
group("Authenticated — User Flow", () => {
// View cart
const cartRes = http.get(
`${API_BASE}/api/v1/ninja/cart/`,
{ headers: authHeaders, timeout: "5s" }
);
check(cartRes, { "cart 200": (r) => r.status === 200 });
// View orders
const orderRes = http.get(
`${API_BASE}/api/v1/ninja/orders/?page=1&page_size=10`,
{ headers: authHeaders, timeout: "5s" }
);
orderLatency.add(orderRes.timings.duration);
check(orderRes, {
"orders 200": (r) => r.status === 200,
"orders < 150ms": (r) => r.timings.duration < 150,
});
// View measurements
const measRes = http.get(
`${API_BASE}/api/v1/ninja/measurements/`,
{ headers: authHeaders, timeout: "5s" }
);
check(measRes, { "measurements 200": (r) => [200, 404].includes(r.status) });
});
}
// ── Main VU entrypoint ────────────────────────────────────────────────────────
export default function () {
// 70% anonymous catalog traffic, 30% authenticated
if (Math.random() < 0.7) {
scenarioCatalog();
} else {
scenarioAuthenticated();
}
// Realistic think time: 0.1–0.5s between requests
sleep(Math.random() * 0.4 + 0.1);
}
// ── Summary report ────────────────────────────────────────────────────────────
export function handleSummary(data) {
return {
"k6/reports/load_test_summary.json": JSON.stringify(data, null, 2),
"k6/reports/load_test_summary.txt": textSummary(data),
stdout: textSummary(data, { indent: " ", enableColors: true }),
};
}
function textSummary(data, opts = {}) {
const indent = opts.indent || "";
const lines = [
`${indent}FASHIONISTAR Load Test Summary`,
`${indent}==============================`,
`${indent}Duration: ${data.state?.testRunDurationMs ?? 0}ms`,
`${indent}VUs Max: ${data.metrics?.vus_max?.values?.max ?? 0}`,
`${indent}Requests: ${data.metrics?.http_reqs?.values?.count ?? 0}`,
`${indent}RPS: ${(data.metrics?.http_reqs?.values?.rate ?? 0).toFixed(0)}`,
`${indent}p95 latency: ${(data.metrics?.http_req_duration?.values?.["p(95)"] ?? 0).toFixed(2)}ms`,
`${indent}p99 latency: ${(data.metrics?.http_req_duration?.values?.["p(99)"] ?? 0).toFixed(2)}ms`,
`${indent}Error rate: ${((data.metrics?.http_req_failed?.values?.rate ?? 0) * 100).toFixed(3)}%`,
];
return lines.join("\n");
}