feat(load-tests): add K6 load testing suite for critical API paths
K6 scripts for 4 critical paths: - Auth (100 VU): login, register, profile - Listings (500 VU): search with filters, detail view - Search (200 VU): full-text + geo search - Payments (50 VU): create payment, list transactions SLA thresholds: p50<200ms, p95<500ms, p99<1s, error<1%. CI: manual workflow_dispatch with suite selector. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
96
load-tests/scripts/search.js
Normal file
96
load-tests/scripts/search.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
import { Trend } from 'k6/metrics';
|
||||
import { BASE_URL, SLA_THRESHOLDS } from '../lib/config.js';
|
||||
|
||||
const textSearchDuration = new Trend('text_search_duration', true);
|
||||
const geoSearchDuration = new Trend('geo_search_duration', true);
|
||||
|
||||
export const options = {
|
||||
scenarios: {
|
||||
search_load: {
|
||||
executor: 'ramping-vus',
|
||||
startVUs: 0,
|
||||
stages: [
|
||||
{ duration: '30s', target: 50 },
|
||||
{ duration: '2m', target: 200 }, // peak: 200 concurrent
|
||||
{ duration: '30s', target: 0 },
|
||||
],
|
||||
gracefulRampDown: '10s',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
...SLA_THRESHOLDS,
|
||||
text_search_duration: ['p(95)<500'],
|
||||
geo_search_duration: ['p(95)<500'],
|
||||
},
|
||||
};
|
||||
|
||||
const TEXT_QUERIES = [
|
||||
'căn hộ',
|
||||
'nhà phố quận 1',
|
||||
'đất nền',
|
||||
'chung cư',
|
||||
'biệt thự',
|
||||
'apartment Ho Chi Minh',
|
||||
'house Hanoi',
|
||||
];
|
||||
|
||||
const GEO_SEARCHES = [
|
||||
{ lat: 10.7769, lng: 106.7009, radius: 5000 }, // HCM center
|
||||
{ lat: 21.0285, lng: 105.8542, radius: 5000 }, // Hanoi center
|
||||
{ lat: 16.0544, lng: 108.2022, radius: 10000 }, // Da Nang
|
||||
{ lat: 10.8231, lng: 106.6297, radius: 3000 }, // HCM Tan Binh
|
||||
{ lat: 21.0067, lng: 105.8400, radius: 2000 }, // Hanoi Ba Dinh
|
||||
];
|
||||
|
||||
export default function () {
|
||||
const iter = __ITER;
|
||||
|
||||
if (iter % 2 === 0) {
|
||||
// Full-text search
|
||||
const query = TEXT_QUERIES[iter % TEXT_QUERIES.length];
|
||||
const params = new URLSearchParams({
|
||||
q: query,
|
||||
limit: '20',
|
||||
offset: '0',
|
||||
});
|
||||
|
||||
const res = http.get(`${BASE_URL}/search?${params.toString()}`, {
|
||||
tags: { name: 'GET /search (text)' },
|
||||
});
|
||||
|
||||
textSearchDuration.add(res.timings.duration);
|
||||
check(res, {
|
||||
'text search: status 200|503': (r) => r.status === 200 || r.status === 503,
|
||||
'text search: valid response': (r) => {
|
||||
if (r.status === 503) return true; // Typesense unavailable — acceptable
|
||||
try { return JSON.parse(r.body).data !== undefined; } catch { return false; }
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Geo search
|
||||
const geo = GEO_SEARCHES[iter % GEO_SEARCHES.length];
|
||||
const params = new URLSearchParams({
|
||||
lat: String(geo.lat),
|
||||
lng: String(geo.lng),
|
||||
radius: String(geo.radius),
|
||||
limit: '20',
|
||||
});
|
||||
|
||||
const res = http.get(`${BASE_URL}/search/geo?${params.toString()}`, {
|
||||
tags: { name: 'GET /search/geo' },
|
||||
});
|
||||
|
||||
geoSearchDuration.add(res.timings.duration);
|
||||
check(res, {
|
||||
'geo search: status 200|503': (r) => r.status === 200 || r.status === 503,
|
||||
'geo search: valid response': (r) => {
|
||||
if (r.status === 503) return true;
|
||||
try { return JSON.parse(r.body).data !== undefined; } catch { return false; }
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
sleep(Math.random() * 1 + 0.5);
|
||||
}
|
||||
Reference in New Issue
Block a user