node tests/load/run.js --postgres puts the scratch daemon in IPX_TEST_DATABASE_URL, the database the Rust tests already use on Postgres, ipodderx_test on the databases project's server, in a schema of its own, ipx_load, dropped and made again each run as /tmp/ipx-load is wiped. The URL is in /src/.envrc, taken from ipx.env. It is production's server and role, so the harness refuses a database whose name does not end in _test, and hands psql the password in its environment, where no other process on Tower can read it. psql comes from /src/install.sh. After seeding it runs ANALYZE: a schema filled seconds before has no planner statistics until autovacuum gets there, which it did partway through a test, and the search's p95 swung between 40ms and 166ms from run to run. Analyzed first, three runs gave 37-38ms. Each budget now has a value for each database, about twice what it measures on Tower: Postgres answers the feed list at p95 54ms to SQLite's 242, having no one connection to queue for (#136). Running it found #140. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
45 lines
2.2 KiB
JavaScript
45 lines
2.2 KiB
JavaScript
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 on SQLite, 68 on Postgres; 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);
|
|
}
|