import http from 'k6/http'; import { check, fail } from 'k6'; const baseUrl = (__ENV.TIKU_BASE_URL || 'http://localhost:5091').replace(/\/$/, ''); const tenantCode = __ENV.TIKU_TENANT_CODE || 'demo-crm-school'; const hostHeader = __ENV.TIKU_HOST_HEADER || ''; const mode = (__ENV.TIKU_MODE || 'mixed').toLowerCase(); const rate = positiveInteger('TIKU_RATE', 100); const duration = __ENV.TIKU_DURATION || '30s'; const preAllocatedVUs = positiveInteger('TIKU_PRE_ALLOCATED_VUS', Math.max(20, Math.ceil(rate / 20))); const maxVUs = positiveInteger('TIKU_MAX_VUS', Math.max(100, preAllocatedVUs * 4)); if (!['mixed', 'hot', 'cold', 'ready'].includes(mode)) { throw new Error(`Unsupported TIKU_MODE '${mode}'. Use mixed, hot, cold, or ready.`); } export const options = { discardResponseBodies: true, summaryTrendStats: ['avg', 'min', 'med', 'p(90)', 'p(95)', 'p(99)', 'max'], scenarios: { api: { executor: 'constant-arrival-rate', exec: 'apiTraffic', rate, timeUnit: '1s', duration, preAllocatedVUs, maxVUs, gracefulStop: '5s', }, }, thresholds: { checks: ['rate>0.99'], http_req_failed: ['rate<0.01'], http_req_duration: ['p(95)<500', 'p(99)<1000'], dropped_iterations: ['count==0'], }, }; const tenantHeaders = { 'x-tenant-code': tenantCode, Accept: 'application/json', ...(hostHeader ? { Host: hostHeader } : {}), }; const platformHeaders = hostHeader ? { Host: hostHeader } : {}; export function setup() { const ready = http.get(`${baseUrl}/api/health/ready`, { headers: platformHeaders, responseType: 'text', tags: { endpoint: 'ready', phase: 'setup' }, }); if (ready.status !== 200) { fail(`API dependencies are not ready: status=${ready.status} body=${ready.body}`); } let readiness; try { readiness = ready.json(); } catch (error) { fail(`Readiness endpoint did not return JSON: ${error}`); } if (readiness.status !== 'ready') { fail(`PostgreSQL and Redis must both be ready: ${JSON.stringify(readiness)}`); } const catalog = http.get(`${baseUrl}/api/catalog/regions`, { headers: tenantHeaders, tags: { endpoint: 'catalog_hot', phase: 'setup' }, }); if (catalog.status !== 200) { fail(`Tenant '${tenantCode}' is not usable: status=${catalog.status} body=${catalog.body}`); } } export function apiTraffic() { const selected = selectEndpoint(); let response; if (selected === 'ready') { response = http.get(`${baseUrl}/api/health/ready`, { headers: platformHeaders, tags: { endpoint: 'ready' }, }); } else if (selected === 'cold') { // A unique query string bypasses output-cache reuse while exercising the same // tenant resolution and PostgreSQL-backed catalog query as the hot request. const cacheBuster = `${__VU}-${__ITER}`; response = http.get(`${baseUrl}/api/catalog/regions?loadProbe=${cacheBuster}`, { headers: tenantHeaders, tags: { endpoint: 'catalog_cold' }, }); } else { response = http.get(`${baseUrl}/api/catalog/regions`, { headers: tenantHeaders, tags: { endpoint: 'catalog_hot' }, }); } check(response, { [`${selected} returned 200`]: (result) => result.status === 200, }); } function selectEndpoint() { if (mode !== 'mixed') return mode; // Stable 70/20/10 traffic mix: cached catalog / uncached catalog / dependency readiness. const bucket = (__ITER + __VU) % 10; if (bucket === 0) return 'ready'; if (bucket <= 2) return 'cold'; return 'hot'; } function positiveInteger(name, fallback) { const raw = __ENV[name]; if (raw === undefined || raw === '') return fallback; const value = Number(raw); if (!Number.isInteger(value) || value <= 0) { throw new Error(`${name} must be a positive integer, got '${raw}'.`); } return value; }