Load tests with k6, for what many people at once do to the server (#133)
The browser tests drive one person against a handful of feeds, one request at a time, and cannot
show what production's load does. node tests/load/run.js builds a release binary, serves 1,500
generated feeds from itself, starts a daemon of its own under /tmp/ipx-load, seeds a hundred
listeners with thirty feeds each and four downloaded files, and runs four k6 scripts, with
`ipx status` -- the healthcheck -- run every second and failing the run at its 5s timeout:
browse 25 people at once: feed list, All Subscriptions, a feed, a search, the Directory,
a feed's page in it
listening 100 players saving positions every second and marking items read while scans write;
each reads its own state back, which must be as it left it
media 50 listeners seeking: every range checked byte for byte against the served file
signin a flood of wrong passwords while others browse, and the gap between refusing a
known name and an unknown one
Each budget is about twice what it measures now. Getting here found #135 (SQLite waiting for the
disk after every write, a first scan of 1,500 feeds estimated at 50 minutes, now 27s), #136
(SQLite's single connection, left open), #137 and #138 (fixed in the commit before this). k6 is
installed from its own signed apt repository by /src/install.sh, outside this repository.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
43
tests/load/signin.js
Normal file
43
tests/load/signin.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
import { Trend } from 'k6/metrics';
|
||||
import { BASE, as, me } from './lib.js';
|
||||
|
||||
// A flood of sign-in attempts with wrong passwords while everyone else goes on using the site.
|
||||
// Two things only many requests at once show: whether checking passwords, tens of milliseconds
|
||||
// of CPU each, starves the rest of the server, and whether a wrong password for a name that is
|
||||
// an account takes longer to refuse than one for a name that is not -- the answers read the
|
||||
// same, and the time would tell anyone which names are accounts here.
|
||||
const gap = new Trend('signin_name_gap', true);
|
||||
export const options = {
|
||||
scenarios: {
|
||||
attempts: { executor: 'constant-vus', vus: 40, duration: '40s', exec: 'attempt' },
|
||||
everyone: { executor: 'constant-vus', vus: 5, duration: '40s', exec: 'browse' },
|
||||
},
|
||||
thresholds: {
|
||||
http_req_failed: ['rate==0'],
|
||||
checks: ['rate==1'],
|
||||
// 57ms on 2026-10-05; 4.3s while password checks ran on the async workers (#137).
|
||||
'http_req_duration{name:meanwhile}': ['p(95)<300'],
|
||||
// Known name against unknown, one straight after the other: no gap but noise. 0.2ms on
|
||||
// 2026-10-05; 31ms while an unknown name was refused without a check (#138).
|
||||
signin_name_gap: ['med<10'],
|
||||
},
|
||||
};
|
||||
const refused = { headers: { 'Content-Type': 'application/json' }, responseCallback: http.expectedStatuses(401) };
|
||||
|
||||
export function attempt() {
|
||||
const known = http.post(`${BASE}/api/login`, JSON.stringify({ name: 'piper', password: 'not-the-password' }),
|
||||
{ ...refused, tags: { name: 'signin-known' } });
|
||||
const unknown = http.post(`${BASE}/api/login`, JSON.stringify({ name: `nobody-${__VU}-${__ITER}`, password: 'not-the-password' }),
|
||||
{ ...refused, tags: { name: 'signin-unknown' } });
|
||||
check(known, { 'a wrong password refused': r => r.status === 401 });
|
||||
check(unknown, { 'an unknown name refused': r => r.status === 401 });
|
||||
gap.add(known.timings.duration - unknown.timings.duration);
|
||||
}
|
||||
|
||||
export function browse() {
|
||||
const r = http.get(`${BASE}/api/feeds`, as(me(), 'meanwhile'));
|
||||
check(r, { 'the site still answers': r => r.status === 200 });
|
||||
sleep(0.5);
|
||||
}
|
||||
Reference in New Issue
Block a user