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>
97 lines
2.7 KiB
JavaScript
97 lines
2.7 KiB
JavaScript
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);
|
|
}
|