// Нагрузочный smoke перед релизом (docs/testing.md#нагрузочный-smoke): 100 одновременных слушателей волны // и 20 запросов подсказок поиска в секунду в течение 3 минут; пороги — цели из docs/api.md и // docs/recommendations.md. Запуск (k6 ≥ 0.50): // k6 run -e BASE_URL=https://music.example.com -e PIPU_USER=loadtest -e PIPU_PASSWORD=… tests/load/smoke.js // Пользователь — обычный (role=user), заведённый для теста: его история и лайки засоряются. import { check, sleep } from 'k6'; import http from 'k6/http'; const BASE = (__ENV.BASE_URL || 'http://localhost:8080').replace(/\/$/, ''); const DURATION = __ENV.DURATION || '3m'; const LISTENERS = Number(__ENV.LISTENERS || 100); const SUGGEST_RPS = Number(__ENV.SUGGEST_RPS || 20); export const options = { scenarios: { listeners: { executor: 'constant-vus', vus: LISTENERS, duration: DURATION, exec: 'listen' }, suggest: { executor: 'constant-arrival-rate', rate: SUGGEST_RPS, timeUnit: '1s', duration: DURATION, preAllocatedVUs: 10, maxVUs: 40, exec: 'suggest', }, }, thresholds: { 'http_req_duration{name:suggest}': ['p(95)<50'], 'http_req_duration{name:wave_next}': ['p(95)<150'], 'http_req_duration{name:wave_start}': ['p(95)<300'], 'http_req_duration{name:stream}': ['p(95)<300'], // первые 64 КБ — время до начала звука 'http_req_duration{name:events}': ['p(95)<100'], http_req_failed: ['rate<0.01'], checks: ['rate>0.99'], }, }; const json = (token) => ({ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, }); export function setup() { const res = http.post( `${BASE}/api/v1/auth/login`, JSON.stringify({ username: __ENV.PIPU_USER, password: __ENV.PIPU_PASSWORD, client_type: 'android', device_name: 'k6 load smoke', }), { headers: { 'Content-Type': 'application/json' } }, ); if (res.status !== 200) throw new Error(`login: ${res.status} ${res.body}`); return { token: res.json('access_token') }; } function uuid() { // v4 — для id событий (дубли отбрасываются сервером, поэтому уникальность важна) return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16); }); } // Слушатель: волна → по каждому треку начало потока и события → следующая пачка с отзывами. export function listen(data) { const p = json(data.token); const start = http.post(`${BASE}/api/v1/wave/sessions`, JSON.stringify({}), { ...p, tags: { name: 'wave_start' } }); if (!check(start, { 'wave started': (r) => r.status === 200 || r.status === 201 })) { sleep(5); return; } const session = start.json('session_id'); let tracks = start.json('tracks') || []; for (let batch = 0; batch < 3 && tracks.length > 0; batch++) { const feedback = []; for (const t of tracks) { const s = http.get(`${BASE}${t.stream_url}`, { headers: { Range: 'bytes=0-65535' }, tags: { name: 'stream' } }); check(s, { 'stream starts': (r) => r.status === 206 || r.status === 200 }); const now = new Date(); const outcome = Math.random() < 0.3 ? 'skip_early' : 'complete'; const played = outcome === 'complete' ? 180000 : 8000; const ev = http.post( `${BASE}/api/v1/events`, JSON.stringify({ events: [ { id: uuid(), type: 'play_start', track_id: t.id, occurred_at: new Date(now - played).toISOString(), position_ms: 0, context_type: 'wave', context_id: session }, { id: uuid(), type: 'play_end', track_id: t.id, occurred_at: now.toISOString(), position_ms: played, played_ms: played, context_type: 'wave', context_id: session }, ], }), { ...p, tags: { name: 'events' } }, ); check(ev, { 'events accepted': (r) => r.status === 200 }); feedback.push({ track_id: t.id, outcome }); sleep(1 + Math.random() * 2); // «слушает» — запросы реального клиента редкие } const next = http.post(`${BASE}/api/v1/wave/sessions/${session}/next`, JSON.stringify({ feedback }), { ...p, tags: { name: 'wave_next' }, }); if (!check(next, { 'wave next': (r) => r.status === 200 })) break; tracks = next.json('tracks') || []; } } const QUERIES = ['ки', 'кино', 'mo', 'mozart', 'the', 'love', 'ghbdtn', 'a', 'нау', 'rock', 'би', 'queen', 'ле', 'jazz']; export function suggest(data) { const q = QUERIES[Math.floor(Math.random() * QUERIES.length)]; const r = http.get(`${BASE}/api/v1/search/suggest?q=${encodeURIComponent(q)}`, { headers: { Authorization: `Bearer ${data.token}` }, tags: { name: 'suggest' }, }); check(r, { 'suggest ok': (x) => x.status === 200 }); }