-
Notifications
You must be signed in to change notification settings - Fork 29
Expand file tree
/
Copy pathk6-load-test.js
More file actions
260 lines (232 loc) · 9.23 KB
/
Copy pathk6-load-test.js
File metadata and controls
260 lines (232 loc) · 9.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { Rate, Trend, Counter } from 'k6/metrics';
// Custom metrics
const errorRate = new Rate('error_rate');
const rateLimited = new Counter('rate_limited');
const reqDuration = new Trend('req_duration', true);
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8081';
const API_KEY = __ENV.API_KEY || '';
// Multi-OLT: pass the SAME OLTS JSON the server uses (-e OLTS='[...]') and the
// test targets /api/v1/olt/{id}/board/... with each OLT's valid board/pon
// ranges. Empty OLTS → single-OLT bare /api/v1/board/... paths.
const OLT_TARGETS = parseOltTargets(__ENV.OLTS || '');
function parseBoardsSpec(spec, defPon) {
return String(spec || '1,2')
.split(',')
.map((s) => {
const [b, p] = s.split(':');
const board = parseInt(b, 10);
const ponMax = p ? parseInt(p, 10) : defPon;
return { board, ponMax };
})
.filter((t) => Number.isInteger(t.board) && t.board > 0 && t.ponMax > 0);
}
function parseOltTargets(js) {
if (!js.trim()) return [];
try {
const arr = JSON.parse(js);
if (!Array.isArray(arr)) return [];
return arr
.filter((o) => o && o.id)
.map((o) => ({ id: o.id, boards: parseBoardsSpec(o.boards, o.ponsPerBoard || 16) }));
} catch (e) {
return [];
}
}
// pickTarget returns { prefix, board, pon } — an OLT-scoped path prefix and a
// board/pon valid for that OLT (single-OLT fallback when OLTS is empty).
function pickTarget() {
if (OLT_TARGETS.length === 0) {
return { prefix: '', board: randomBoard(), pon: randomPon() };
}
const t = OLT_TARGETS[Math.floor(Math.random() * OLT_TARGETS.length)];
const card = t.boards[Math.floor(Math.random() * t.boards.length)];
return {
prefix: `/olt/${t.id}`,
board: card.board,
pon: Math.floor(Math.random() * card.ponMax) + 1,
key: OLT_KEYS[t.id] || API_KEY,
};
}
function apiBase(prefix) {
return `${BASE_URL}/api/v1${prefix || ''}`;
}
// Load test stages
export const options = {
stages: [
// Warm-up
{ duration: '10s', target: 10 },
// Ramp-up
{ duration: '20s', target: 50 },
// Sustained load
{ duration: '30s', target: 50 },
// Spike test
{ duration: '10s', target: 100 },
// Sustained spike
{ duration: '20s', target: 100 },
// Cool-down
{ duration: '10s', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<5000', 'p(99)<10000'],
error_rate: ['rate<0.05'], // real errors (not 429)
},
};
// Optional per-tenant API keys for OLT-scoped requests: JSON {oltId: apiKey}.
// Set when the server runs with per-tenant API_USERS; falls back to API_KEY.
const OLT_KEYS = (() => {
const raw = __ENV.OLT_KEYS || '';
if (!raw.trim()) return {};
try { const o = JSON.parse(raw); return o && typeof o === 'object' ? o : {}; } catch (e) { return {}; }
})();
function getHeaders(key) {
const headers = { 'Content-Type': 'application/json' };
const apiKey = key || API_KEY;
if (apiKey) {
headers['X-API-Key'] = apiKey;
}
return headers;
}
// 429 is expected under load (rate limiter), not a real error
// 429 (rate limited) and 404 are EXPECTED outcomes under load, not real errors:
// 429 = the global rate limiter doing its job; 404 = a valid board/pon that
// simply has no ONUs (common on sparsely-populated / lab OLTs). Only other 4xx/5xx
// (e.g. 400 validation, 401 auth, 500) count as real errors.
function isSuccess(status) { return status === 200 || status === 429 || status === 404; }
function isRealError(status) { return status >= 400 && status !== 429 && status !== 404; }
function trackResponse(res, name) {
const realError = isRealError(res.status);
errorRate.add(realError);
reqDuration.add(res.timings.duration);
if (res.status === 429) rateLimited.add(1);
}
// Random board/pon/onu generators
function randomBoard() { return Math.random() < 0.5 ? 1 : 2; }
function randomPon() { return Math.floor(Math.random() * 16) + 1; }
function randomOnu() { return Math.floor(Math.random() * 50) + 1; }
export default function () {
const headers = getHeaders();
const params = { headers, timeout: '60s' };
group('Health Check', function () {
const res = http.get(`${BASE_URL}/health`, params);
check(res, {
'health: status 200 or 429': (r) => isSuccess(r.status),
});
trackResponse(res);
// Multi-OLT: /readyz must expose a per-OLT probe snmp_<id> for each OLT.
if (OLT_TARGETS.length > 0) {
const ready = http.get(`${BASE_URL}/readyz`, params);
check(ready, {
'readyz: 200 or 503': (r) => r.status === 200 || r.status === 503,
'readyz: per-OLT snmp probes present': (r) => {
try {
const deps = JSON.parse(r.body).dependencies || {};
return OLT_TARGETS.every((t) => deps[`snmp_${t.id}`] !== undefined);
} catch (e) { return false; }
},
});
trackResponse(ready);
}
});
group('Get ONUs by Board/PON', function () {
const { prefix, board, pon, key } = pickTarget();
const res = http.get(`${apiBase(prefix)}/board/${board}/pon/${pon}/`, { headers: getHeaders(key), timeout: '60s' });
check(res, {
'onu-list: success': (r) => isSuccess(r.status),
'onu-list: has data': (r) => {
if (r.status === 429) return true; // skip check on rate limit
try { return r.json('data') !== null; } catch(e) { return false; }
},
'onu-list: response < 5s': (r) => r.timings.duration < 5000,
});
trackResponse(res);
});
group('Get ONU Detail', function () {
const { prefix, board, pon, key } = pickTarget();
const onuID = randomOnu();
const res = http.get(`${apiBase(prefix)}/board/${board}/pon/${pon}/onu/${onuID}`, { headers: getHeaders(key), timeout: '60s' });
check(res, {
'onu-detail: success': (r) => isSuccess(r.status),
'onu-detail: response < 10s': (r) => r.timings.duration < 10000,
});
trackResponse(res);
});
group('Get ONUs Paginated', function () {
const { prefix, board, pon, key } = pickTarget();
const page = Math.floor(Math.random() * 3) + 1;
const res = http.get(`${apiBase(prefix)}/paginate/board/${board}/pon/${pon}/?page=${page}&page_size=10`, { headers: getHeaders(key), timeout: '60s' });
check(res, {
'paginate: success': (r) => isSuccess(r.status),
'paginate: response < 5s': (r) => r.timings.duration < 5000,
});
trackResponse(res);
});
group('Get Empty ONU IDs', function () {
const { prefix, board, pon, key } = pickTarget();
const res = http.get(`${apiBase(prefix)}/board/${board}/pon/${pon}/onu_id/empty`, { headers: getHeaders(key), timeout: '60s' });
check(res, {
'empty-onu: success': (r) => isSuccess(r.status),
'empty-onu: response < 5s': (r) => r.timings.duration < 5000,
});
trackResponse(res);
});
group('Get ONU Serial Numbers', function () {
const { prefix, board, pon, key } = pickTarget();
const res = http.get(`${apiBase(prefix)}/board/${board}/pon/${pon}/onu_id_sn`, { headers: getHeaders(key), timeout: '60s' });
check(res, {
'onu-sn: success': (r) => isSuccess(r.status),
'onu-sn: response < 5s': (r) => r.timings.duration < 5000,
});
trackResponse(res);
});
group('Get ONU Serial Numbers (nocache)', function () {
// ?nocache=true bypasses + refreshes the serial-list cache — the
// write-olt-zte pre-write check path; always a live SNMP read.
const { prefix, board, pon, key } = pickTarget();
const res = http.get(`${apiBase(prefix)}/board/${board}/pon/${pon}/onu_id_sn?nocache=true`, { headers: getHeaders(key), timeout: '60s' });
check(res, {
'onu-sn-nocache: success': (r) => isSuccess(r.status),
'onu-sn-nocache: response < 30s': (r) => r.timings.duration < 30000,
});
trackResponse(res);
});
group('Uplink Auto-Detect', function () {
// ENTITY-MIB + IF-MIB topology discovery; detection-only, no writes.
const { prefix, key } = pickTarget();
const res = http.get(`${apiBase(prefix)}/uplinks`, { headers: getHeaders(key), timeout: '60s' });
check(res, {
'uplinks: success': (r) => isSuccess(r.status),
'uplinks: 200 has cards+ports': (r) => {
if (r.status !== 200) return true;
try {
const d = JSON.parse(r.body).data;
return d && Array.isArray(d.cards) && Array.isArray(d.ports);
} catch { return false; }
},
});
trackResponse(res);
});
group('Validation - Invalid Params', function () {
const res1 = http.get(`${BASE_URL}/api/v1/board/0/pon/1/`, params);
check(res1, {
'invalid-board: returns 400 or 429': (r) => r.status === 400 || r.status === 429,
});
const res2 = http.get(`${BASE_URL}/api/v1/board/1/pon/99/`, params);
check(res2, {
'invalid-pon: returns 400 or 429': (r) => r.status === 400 || r.status === 429,
});
const res3 = http.get(`${BASE_URL}/api/v1/board/1/pon/1/onu/0`, params);
check(res3, {
'invalid-onu: returns 400 or 429': (r) => r.status === 400 || r.status === 429,
});
// Multi-OLT: an unknown OLT id must 404.
if (OLT_TARGETS.length > 0) {
const res4 = http.get(`${BASE_URL}/api/v1/olt/__nope__/board/1/pon/1/`, params);
check(res4, {
'unknown-olt: returns 404 or 429': (r) => r.status === 404 || r.status === 429,
});
}
});
sleep(0.1);
}