forked from xiongyuxing/tiku-backend.net
126 lines
3.8 KiB
JavaScript
126 lines
3.8 KiB
JavaScript
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.database || !readiness.redis?.configured || !readiness.redis?.ready) {
|
|
fail(`PostgreSQL and Redis must both be configured and 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;
|
|
}
|