feat: improve session limit handling and add k6 load testing
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
import http from 'k6/http';
|
||||
import { check, fail, sleep } from 'k6';
|
||||
import exec from 'k6/execution';
|
||||
import { Counter, Rate, Trend } from 'k6/metrics';
|
||||
|
||||
const BASE_URL = (__ENV.BASE_URL || 'https://stend.4mont.ru').replace(/\/$/, '');
|
||||
const SERVICE_SLUGS = (__ENV.SERVICE_SLUGS || 'vmmanager')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const HEARTBEATS = Number(__ENV.HEARTBEATS || 3);
|
||||
const THINK_TIME = Number(__ENV.THINK_TIME || 1);
|
||||
const CLOSE_SESSION = (__ENV.CLOSE_SESSION || '1') !== '0';
|
||||
const PROFILE = (__ENV.PROFILE || 'smoke').toLowerCase();
|
||||
|
||||
const usersFromCsv = parseUsersCsv(__ENV.USERS_CSV || '');
|
||||
const singleUser = {
|
||||
username: __ENV.USERNAME || '',
|
||||
password: __ENV.PASSWORD || '',
|
||||
};
|
||||
|
||||
const openAttempts = new Counter('open_attempts');
|
||||
const openSuccess = new Counter('open_success');
|
||||
const openRejected = new Counter('open_rejected');
|
||||
const limitRedirects = new Counter('limit_redirects');
|
||||
const touchRejected = new Counter('touch_rejected');
|
||||
const closeCalls = new Counter('close_calls');
|
||||
const loginFailures = new Counter('login_failures');
|
||||
const flowErrors = new Rate('flow_errors');
|
||||
const openLatency = new Trend('open_latency_ms');
|
||||
|
||||
const PROFILE_OPTIONS = {
|
||||
smoke: {
|
||||
vus: 5,
|
||||
duration: '1m',
|
||||
},
|
||||
load: {
|
||||
vus: 30,
|
||||
duration: '10m',
|
||||
},
|
||||
stress: {
|
||||
stages: [
|
||||
{ duration: '2m', target: 30 },
|
||||
{ duration: '5m', target: 80 },
|
||||
{ duration: '2m', target: 0 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const selected = PROFILE_OPTIONS[PROFILE] || PROFILE_OPTIONS.smoke;
|
||||
|
||||
export const options = {
|
||||
...selected,
|
||||
insecureSkipTLSVerify: (__ENV.INSECURE_TLS || '0') === '1',
|
||||
thresholds: {
|
||||
http_req_failed: ['rate<0.02'],
|
||||
http_req_duration: ['p(95)<2000'],
|
||||
flow_errors: ['rate<0.05'],
|
||||
},
|
||||
};
|
||||
|
||||
let loggedInKey = '';
|
||||
|
||||
function parseUsersCsv(raw) {
|
||||
if (!raw.trim()) return [];
|
||||
return raw
|
||||
.split(';')
|
||||
.map((pair) => pair.trim())
|
||||
.filter(Boolean)
|
||||
.map((pair) => {
|
||||
const idx = pair.indexOf(':');
|
||||
if (idx <= 0) return null;
|
||||
return {
|
||||
username: pair.slice(0, idx).trim(),
|
||||
password: pair.slice(idx + 1).trim(),
|
||||
};
|
||||
})
|
||||
.filter((u) => u && u.username && u.password);
|
||||
}
|
||||
|
||||
function currentCredentials() {
|
||||
if (usersFromCsv.length > 0) {
|
||||
const vu = exec.vu.idInTest || 1;
|
||||
return usersFromCsv[(vu - 1) % usersFromCsv.length];
|
||||
}
|
||||
return singleUser;
|
||||
}
|
||||
|
||||
function ensureLoggedIn() {
|
||||
const creds = currentCredentials();
|
||||
if (!creds.username || !creds.password) {
|
||||
fail('Set USERNAME/PASSWORD or USERS_CSV (username:password;username2:password2)');
|
||||
}
|
||||
|
||||
const userKey = `${creds.username}:${creds.password}`;
|
||||
if (loggedInKey === userKey) return creds;
|
||||
|
||||
const jar = http.cookieJar();
|
||||
jar.clear(BASE_URL);
|
||||
|
||||
const landing = http.get(`${BASE_URL}/`, { redirects: 0, tags: { name: 'GET /' } });
|
||||
const csrfFromCookie = (jar.cookiesForURL(BASE_URL).csrf_token || [])[0] || '';
|
||||
const csrf = csrfFromCookie || ((landing.cookies.csrf_token || [])[0] || {}).value || '';
|
||||
if (!csrf) {
|
||||
loginFailures.add(1);
|
||||
fail('CSRF cookie not found on GET /');
|
||||
}
|
||||
|
||||
const payload = [
|
||||
`username=${encodeURIComponent(creds.username)}`,
|
||||
`password=${encodeURIComponent(creds.password)}`,
|
||||
`csrf_token=${encodeURIComponent(csrf)}`,
|
||||
].join('&');
|
||||
|
||||
const loginRes = http.post(`${BASE_URL}/login`, payload, {
|
||||
redirects: 0,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
tags: { name: 'POST /login' },
|
||||
});
|
||||
|
||||
const ok = check(loginRes, {
|
||||
'login status is redirect': (r) => r.status === 303,
|
||||
});
|
||||
|
||||
const hasAuth = (jar.cookiesForURL(BASE_URL).portal_auth || []).length > 0;
|
||||
if (!ok || !hasAuth) {
|
||||
loginFailures.add(1);
|
||||
fail(`Login failed for ${creds.username}: status=${loginRes.status}`);
|
||||
}
|
||||
|
||||
loggedInKey = userKey;
|
||||
return creds;
|
||||
}
|
||||
|
||||
function pickSlug() {
|
||||
return SERVICE_SLUGS[Math.floor(Math.random() * SERVICE_SLUGS.length)];
|
||||
}
|
||||
|
||||
function extractSessionId(location) {
|
||||
const match = (location || '').match(/\/s\/([0-9a-fA-F-]{36})\//);
|
||||
return match ? match[1] : '';
|
||||
}
|
||||
|
||||
export default function () {
|
||||
ensureLoggedIn();
|
||||
|
||||
const slug = pickSlug();
|
||||
openAttempts.add(1);
|
||||
|
||||
const openRes = http.get(`${BASE_URL}/go/${slug}`, {
|
||||
redirects: 0,
|
||||
tags: { name: 'GET /go/:slug' },
|
||||
});
|
||||
openLatency.add(openRes.timings.duration);
|
||||
|
||||
const location = openRes.headers.Location || '';
|
||||
const sessionId = extractSessionId(location);
|
||||
|
||||
const opened = check(openRes, {
|
||||
'open returns redirect': (r) => r.status === 303,
|
||||
});
|
||||
|
||||
if (!opened) {
|
||||
openRejected.add(1);
|
||||
flowErrors.add(1);
|
||||
sleep(THINK_TIME);
|
||||
return;
|
||||
}
|
||||
|
||||
if (location.includes('launch_error=max_services')) {
|
||||
limitRedirects.add(1);
|
||||
flowErrors.add(1);
|
||||
sleep(THINK_TIME);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sessionId) {
|
||||
openRejected.add(1);
|
||||
flowErrors.add(1);
|
||||
sleep(THINK_TIME);
|
||||
return;
|
||||
}
|
||||
|
||||
openSuccess.add(1);
|
||||
|
||||
for (let i = 0; i < HEARTBEATS; i += 1) {
|
||||
const touchRes = http.post(`${BASE_URL}/api/sessions/${sessionId}/touch`, null, {
|
||||
redirects: 0,
|
||||
tags: { name: 'POST /api/sessions/:id/touch' },
|
||||
});
|
||||
|
||||
if (touchRes.status === 410) {
|
||||
touchRejected.add(1);
|
||||
break;
|
||||
}
|
||||
|
||||
const touchOk = check(touchRes, {
|
||||
'touch status 200': (r) => r.status === 200,
|
||||
});
|
||||
if (!touchOk) {
|
||||
flowErrors.add(1);
|
||||
break;
|
||||
}
|
||||
sleep(0.5);
|
||||
}
|
||||
|
||||
if (CLOSE_SESSION) {
|
||||
const closeRes = http.post(`${BASE_URL}/api/sessions/${sessionId}/close`, null, {
|
||||
redirects: 0,
|
||||
tags: { name: 'POST /api/sessions/:id/close' },
|
||||
});
|
||||
closeCalls.add(1);
|
||||
check(closeRes, {
|
||||
'close status 200': (r) => r.status === 200,
|
||||
});
|
||||
}
|
||||
|
||||
flowErrors.add(0);
|
||||
sleep(THINK_TIME);
|
||||
}
|
||||
Reference in New Issue
Block a user