@assertrx/dino 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/prompts/generate-bdd-from-url.md +5 -1
- package/prompts/generate-k6-test-from-endpoint.md +68 -0
- package/prompts/generate-k6-test-from-openapi.md +85 -0
- package/prompts/setup-k6.md +130 -0
- package/scripts/api-server.mjs +32 -2
- package/skills/analyzing-test-results/SKILL.md +150 -0
- package/skills/analyzing-test-results/reference/bottleneck-patterns.md +130 -0
- package/skills/analyzing-test-results/reference/metrics.md +134 -0
- package/skills/analyzing-test-results/reference/visualization.md +152 -0
- package/skills/designing-test-scenarios/SKILL.md +247 -0
- package/skills/designing-test-scenarios/reference/executors.md +215 -0
- package/skills/designing-test-scenarios/reference/test-types.md +215 -0
- package/skills/designing-test-scenarios/reference/thresholds.md +208 -0
- package/skills/generating-api-load-tests/SKILL.md +199 -0
- package/skills/generating-api-load-tests/reference/data-parameterization.md +343 -0
- package/skills/generating-api-load-tests/reference/grpc.md +315 -0
- package/skills/generating-api-load-tests/reference/http.md +380 -0
- package/skills/generating-api-load-tests/reference/websocket.md +369 -0
- package/skills/generating-browser-tests/SKILL.md +269 -0
- package/skills/generating-browser-tests/reference/browser-api.md +251 -0
- package/skills/generating-browser-tests/reference/web-vitals.md +148 -0
- package/skills/generating-tests-from-code/SKILL.md +142 -0
- package/skills/generating-tests-from-code/reference/auth-analysis.md +220 -0
- package/skills/generating-tests-from-code/reference/framework-patterns.md +242 -0
- package/skills/generating-tests-from-openapi/SKILL.md +234 -0
- package/skills/generating-tests-from-openapi/reference/openapi-auth-mapping.md +247 -0
- package/skills/generating-tests-from-openapi/reference/openapi-parsing.md +194 -0
- package/skills/operating-k6-in-ci-cd/SKILL.md +98 -0
- package/skills/operating-k6-in-ci-cd/reference/ci-cd-pipelines.md +265 -0
- package/skills/operating-k6-in-ci-cd/reference/extensions.md +144 -0
- package/skills/operating-k6-in-ci-cd/reference/k6-operator.md +214 -0
- package/skills/testing-resilience/SKILL.md +230 -0
- package/skills/testing-resilience/reference/fault-injection.md +153 -0
- package/skills/testing-resilience/reference/resilience-patterns.md +500 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
# Resilience Test Patterns Reference
|
|
2
|
+
|
|
3
|
+
## Pattern 1: Graceful Degradation
|
|
4
|
+
|
|
5
|
+
Verify the system degrades gracefully under partial failure instead of cascading.
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
import http from 'k6/http';
|
|
9
|
+
import { check, sleep } from 'k6';
|
|
10
|
+
import { ServiceDisruptor } from 'k6/x/disruptor';
|
|
11
|
+
|
|
12
|
+
export const options = {
|
|
13
|
+
scenarios: {
|
|
14
|
+
load: {
|
|
15
|
+
executor: 'constant-arrival-rate',
|
|
16
|
+
rate: 100,
|
|
17
|
+
timeUnit: '1s',
|
|
18
|
+
duration: '3m',
|
|
19
|
+
preAllocatedVUs: 50,
|
|
20
|
+
maxVUs: 100,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
thresholds: {
|
|
24
|
+
http_req_failed: ['rate<0.5'], // Allow up to 50% errors
|
|
25
|
+
http_req_duration: ['p(95)<5000'], // 5s max during degradation
|
|
26
|
+
'http_req_duration{phase:recovery}': ['p(95)<500'], // Back to normal after
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function setup() {
|
|
31
|
+
const disruptor = new ServiceDisruptor('backend-api', 'default');
|
|
32
|
+
disruptor.injectHTTPFaults({
|
|
33
|
+
averageDelay: '1000ms',
|
|
34
|
+
delayVariation: '200ms',
|
|
35
|
+
errorRate: 0.3,
|
|
36
|
+
errorCode: 503,
|
|
37
|
+
exclude: '/health',
|
|
38
|
+
}, '90s'); // Fault for 90s, test runs 3m → 90s recovery
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function () {
|
|
42
|
+
const res = http.get('http://backend-api.default.svc.cluster.local:8080/api/data');
|
|
43
|
+
check(res, {
|
|
44
|
+
'returns valid response': (r) => r.status === 200 || r.status === 503,
|
|
45
|
+
'no cascading failure': (r) => r.status !== 502,
|
|
46
|
+
'responds within timeout': (r) => r.timings.duration < 10000,
|
|
47
|
+
});
|
|
48
|
+
sleep(0.1);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Success criteria:**
|
|
53
|
+
- System returns 503 (expected) not 502 (cascade)
|
|
54
|
+
- Response times degrade but stay within upper bound
|
|
55
|
+
- No complete service outage
|
|
56
|
+
- Metrics return to baseline during recovery phase
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Pattern 2: Circuit Breaker Validation
|
|
61
|
+
|
|
62
|
+
Verify circuit breaker activates under sustained errors and returns fallback responses.
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
import http from 'k6/http';
|
|
66
|
+
import { check, sleep } from 'k6';
|
|
67
|
+
import { Counter, Trend } from 'k6/metrics';
|
|
68
|
+
import { ServiceDisruptor } from 'k6/x/disruptor';
|
|
69
|
+
|
|
70
|
+
const circuitOpenResponses = new Counter('circuit_open_responses');
|
|
71
|
+
const responseTime = new Trend('response_time_trend');
|
|
72
|
+
|
|
73
|
+
export const options = {
|
|
74
|
+
scenarios: {
|
|
75
|
+
load: {
|
|
76
|
+
executor: 'constant-arrival-rate',
|
|
77
|
+
rate: 50,
|
|
78
|
+
timeUnit: '1s',
|
|
79
|
+
duration: '4m',
|
|
80
|
+
preAllocatedVUs: 30,
|
|
81
|
+
maxVUs: 60,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
thresholds: {
|
|
85
|
+
circuit_open_responses: ['count>0'], // Circuit breaker MUST activate
|
|
86
|
+
http_req_duration: ['p(95)<3000'], // Fast fallback expected
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export function setup() {
|
|
91
|
+
const disruptor = new ServiceDisruptor('downstream-service', 'default');
|
|
92
|
+
// High error rate to trigger circuit breaker
|
|
93
|
+
disruptor.injectHTTPFaults({
|
|
94
|
+
errorRate: 0.8,
|
|
95
|
+
errorCode: 500,
|
|
96
|
+
averageDelay: '3000ms',
|
|
97
|
+
}, '120s'); // 2 min of faults, then 2 min recovery
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default function () {
|
|
101
|
+
const res = http.get('http://api-gateway.default.svc.cluster.local:8080/api/data');
|
|
102
|
+
responseTime.add(res.timings.duration);
|
|
103
|
+
|
|
104
|
+
// Detect circuit breaker activation via fallback response
|
|
105
|
+
const isFallback = res.status === 200 && res.json() &&
|
|
106
|
+
(res.json().source === 'cache' || res.json().fallback === true);
|
|
107
|
+
|
|
108
|
+
if (isFallback) {
|
|
109
|
+
circuitOpenResponses.add(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
check(res, {
|
|
113
|
+
'not a timeout': (r) => r.timings.duration < 10000,
|
|
114
|
+
'valid response or fallback': (r) => r.status === 200 || r.status === 503,
|
|
115
|
+
});
|
|
116
|
+
sleep(0.1);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Success criteria:**
|
|
121
|
+
- `circuit_open_responses` count > 0 (circuit breaker activated)
|
|
122
|
+
- Response times drop after circuit opens (fast fallback)
|
|
123
|
+
- After fault removal, circuit closes and normal responses resume
|
|
124
|
+
|
|
125
|
+
**Phases observed:**
|
|
126
|
+
```
|
|
127
|
+
|-- Closed --|-- Open (fallback) --|-- Half-Open --|-- Closed --|
|
|
128
|
+
| Errors | Fast fallback | Trial reqs | Normal |
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Pattern 3: Recovery Time Measurement
|
|
134
|
+
|
|
135
|
+
Measure how quickly the system recovers after faults are removed.
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
import http from 'k6/http';
|
|
139
|
+
import { check, sleep } from 'k6';
|
|
140
|
+
import { Trend, Rate } from 'k6/metrics';
|
|
141
|
+
import { ServiceDisruptor } from 'k6/x/disruptor';
|
|
142
|
+
|
|
143
|
+
const recoveryLatency = new Trend('recovery_phase_latency');
|
|
144
|
+
const recoveryErrors = new Rate('recovery_phase_errors');
|
|
145
|
+
|
|
146
|
+
export const options = {
|
|
147
|
+
scenarios: {
|
|
148
|
+
load: {
|
|
149
|
+
executor: 'constant-arrival-rate',
|
|
150
|
+
rate: 50,
|
|
151
|
+
timeUnit: '1s',
|
|
152
|
+
duration: '5m',
|
|
153
|
+
preAllocatedVUs: 30,
|
|
154
|
+
maxVUs: 60,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
thresholds: {
|
|
158
|
+
recovery_phase_latency: ['p(95)<500'], // Must recover to baseline
|
|
159
|
+
recovery_phase_errors: ['rate<0.01'], // Near-zero errors after recovery
|
|
160
|
+
http_req_duration: ['p(95)<5000'], // Overall upper bound
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export function setup() {
|
|
165
|
+
const disruptor = new ServiceDisruptor('my-service', 'default');
|
|
166
|
+
// Fault for 2 min → 3 min recovery observation
|
|
167
|
+
disruptor.injectHTTPFaults({
|
|
168
|
+
averageDelay: '2000ms',
|
|
169
|
+
delayVariation: '500ms',
|
|
170
|
+
errorRate: 0.5,
|
|
171
|
+
errorCode: 503,
|
|
172
|
+
}, '120s');
|
|
173
|
+
|
|
174
|
+
return { faultEndTime: Date.now() + 120000 };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export default function (data) {
|
|
178
|
+
const res = http.get('http://my-service.default.svc.cluster.local:8080/api');
|
|
179
|
+
const isRecoveryPhase = Date.now() > data.faultEndTime;
|
|
180
|
+
|
|
181
|
+
if (isRecoveryPhase) {
|
|
182
|
+
recoveryLatency.add(res.timings.duration);
|
|
183
|
+
recoveryErrors.add(res.status >= 500 ? 1 : 0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
check(res, {
|
|
187
|
+
'status ok or expected error': (r) => r.status === 200 || r.status === 503,
|
|
188
|
+
});
|
|
189
|
+
sleep(0.1);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Success criteria:**
|
|
194
|
+
- `recovery_phase_latency` p(95) returns to baseline (< 500ms)
|
|
195
|
+
- `recovery_phase_errors` rate drops to near zero
|
|
196
|
+
- Recovery happens within acceptable window (e.g., 30 seconds)
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Pattern 4: Pod Failure and Auto-Scaling
|
|
201
|
+
|
|
202
|
+
Test system behavior when pods are terminated and verify Kubernetes auto-scaling replaces them.
|
|
203
|
+
|
|
204
|
+
```javascript
|
|
205
|
+
import http from 'k6/http';
|
|
206
|
+
import { check, sleep } from 'k6';
|
|
207
|
+
import { Rate, Trend } from 'k6/metrics';
|
|
208
|
+
import { ServiceDisruptor } from 'k6/x/disruptor';
|
|
209
|
+
|
|
210
|
+
const errorRate = new Rate('error_rate');
|
|
211
|
+
const latency = new Trend('request_latency');
|
|
212
|
+
|
|
213
|
+
export const options = {
|
|
214
|
+
scenarios: {
|
|
215
|
+
load: {
|
|
216
|
+
executor: 'constant-arrival-rate',
|
|
217
|
+
rate: 100,
|
|
218
|
+
timeUnit: '1s',
|
|
219
|
+
duration: '5m',
|
|
220
|
+
preAllocatedVUs: 50,
|
|
221
|
+
maxVUs: 100,
|
|
222
|
+
},
|
|
223
|
+
disrupt: {
|
|
224
|
+
executor: 'shared-iterations',
|
|
225
|
+
iterations: 1,
|
|
226
|
+
vus: 1,
|
|
227
|
+
exec: 'disrupt',
|
|
228
|
+
startTime: '30s', // Start disruption after 30s baseline
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
thresholds: {
|
|
232
|
+
error_rate: ['rate<0.3'], // Allow transient errors
|
|
233
|
+
request_latency: ['p(99)<10000'], // Upper bound during disruption
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export function disrupt() {
|
|
238
|
+
const disruptor = new ServiceDisruptor('my-service', 'default');
|
|
239
|
+
// Terminate 1 pod every 30 seconds
|
|
240
|
+
disruptor.terminatePods({
|
|
241
|
+
count: 1,
|
|
242
|
+
interval: '30s',
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export default function () {
|
|
247
|
+
const res = http.get('http://my-service.default.svc.cluster.local:8080/api');
|
|
248
|
+
errorRate.add(res.status >= 500 || res.status === 0 ? 1 : 0);
|
|
249
|
+
latency.add(res.timings.duration);
|
|
250
|
+
|
|
251
|
+
check(res, {
|
|
252
|
+
'service available': (r) => r.status !== 0,
|
|
253
|
+
'not server error': (r) => r.status < 500,
|
|
254
|
+
});
|
|
255
|
+
sleep(0.1);
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Success criteria:**
|
|
260
|
+
- Error rate stays below threshold despite pod termination
|
|
261
|
+
- Kubernetes replaces terminated pods (check with `kubectl get pods -w`)
|
|
262
|
+
- Service remains available throughout (no total outage)
|
|
263
|
+
- Response times stabilize after replacement pods are ready
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Pattern 5: Failover Testing
|
|
268
|
+
|
|
269
|
+
Test traffic routing to healthy instances when some become unavailable.
|
|
270
|
+
|
|
271
|
+
```javascript
|
|
272
|
+
import http from 'k6/http';
|
|
273
|
+
import { check, sleep } from 'k6';
|
|
274
|
+
import { ServiceDisruptor } from 'k6/x/disruptor';
|
|
275
|
+
|
|
276
|
+
export const options = {
|
|
277
|
+
scenarios: {
|
|
278
|
+
baseline: {
|
|
279
|
+
executor: 'constant-arrival-rate',
|
|
280
|
+
rate: 50,
|
|
281
|
+
timeUnit: '1s',
|
|
282
|
+
duration: '1m',
|
|
283
|
+
preAllocatedVUs: 30,
|
|
284
|
+
exec: 'loadTest',
|
|
285
|
+
startTime: '0s',
|
|
286
|
+
},
|
|
287
|
+
failover: {
|
|
288
|
+
executor: 'constant-arrival-rate',
|
|
289
|
+
rate: 50,
|
|
290
|
+
timeUnit: '1s',
|
|
291
|
+
duration: '2m',
|
|
292
|
+
preAllocatedVUs: 30,
|
|
293
|
+
exec: 'loadTest',
|
|
294
|
+
startTime: '1m', // Start after baseline
|
|
295
|
+
},
|
|
296
|
+
inject: {
|
|
297
|
+
executor: 'shared-iterations',
|
|
298
|
+
iterations: 1,
|
|
299
|
+
vus: 1,
|
|
300
|
+
exec: 'injectFault',
|
|
301
|
+
startTime: '1m', // Inject at same time as failover phase
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
thresholds: {
|
|
305
|
+
'http_req_failed{scenario:baseline}': ['rate<0.01'], // Baseline: near-zero errors
|
|
306
|
+
'http_req_failed{scenario:failover}': ['rate<0.1'], // Failover: some errors ok
|
|
307
|
+
'http_req_duration{scenario:failover}': ['p(95)<2000'],
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export function injectFault() {
|
|
312
|
+
const disruptor = new ServiceDisruptor('primary-service', 'default');
|
|
313
|
+
// Make primary service return errors
|
|
314
|
+
disruptor.injectHTTPFaults({
|
|
315
|
+
errorRate: 1.0,
|
|
316
|
+
errorCode: 503,
|
|
317
|
+
}, '120s');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function loadTest() {
|
|
321
|
+
// Request goes through a load balancer or gateway that should failover
|
|
322
|
+
const res = http.get('http://api-gateway.default.svc.cluster.local:8080/api/data');
|
|
323
|
+
check(res, {
|
|
324
|
+
'response received': (r) => r.status !== 0,
|
|
325
|
+
'successful response': (r) => r.status === 200,
|
|
326
|
+
});
|
|
327
|
+
sleep(0.1);
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Success criteria:**
|
|
332
|
+
- Baseline phase has near-zero errors
|
|
333
|
+
- After primary failure, traffic routes to secondary/healthy instances
|
|
334
|
+
- Error rate during failover stays within threshold
|
|
335
|
+
- No sustained outage period
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Pattern 6: Baseline Comparison with SKIP_FAULTS
|
|
340
|
+
|
|
341
|
+
Run the same test with and without faults to measure exact degradation impact.
|
|
342
|
+
|
|
343
|
+
```javascript
|
|
344
|
+
import http from 'k6/http';
|
|
345
|
+
import { check, sleep } from 'k6';
|
|
346
|
+
import { ServiceDisruptor } from 'k6/x/disruptor';
|
|
347
|
+
|
|
348
|
+
const SKIP_FAULTS = (__ENV.SKIP_FAULTS === '1');
|
|
349
|
+
|
|
350
|
+
export const options = {
|
|
351
|
+
scenarios: {
|
|
352
|
+
load: {
|
|
353
|
+
executor: 'constant-arrival-rate',
|
|
354
|
+
rate: 100,
|
|
355
|
+
timeUnit: '1s',
|
|
356
|
+
duration: '30s',
|
|
357
|
+
preAllocatedVUs: 10,
|
|
358
|
+
maxVUs: 100,
|
|
359
|
+
},
|
|
360
|
+
disrupt: {
|
|
361
|
+
executor: 'shared-iterations',
|
|
362
|
+
iterations: 1,
|
|
363
|
+
vus: 1,
|
|
364
|
+
exec: 'disrupt',
|
|
365
|
+
startTime: '0s',
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
export function disrupt() {
|
|
371
|
+
if (SKIP_FAULTS) {
|
|
372
|
+
console.log('Skipping fault injection (baseline run)');
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const disruptor = new ServiceDisruptor('my-app', 'default');
|
|
377
|
+
disruptor.injectHTTPFaults({
|
|
378
|
+
averageDelay: '50ms',
|
|
379
|
+
errorCode: 500,
|
|
380
|
+
errorRate: 0.1,
|
|
381
|
+
}, '30s');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export default function () {
|
|
385
|
+
const res = http.get('http://my-app.default.svc.cluster.local:8080/api');
|
|
386
|
+
check(res, {
|
|
387
|
+
'status is 200': (r) => r.status === 200,
|
|
388
|
+
});
|
|
389
|
+
sleep(0.1);
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Usage:**
|
|
394
|
+
```bash
|
|
395
|
+
# Run baseline (no faults)
|
|
396
|
+
SKIP_FAULTS=1 k6 run test.js --out json=baseline.json
|
|
397
|
+
|
|
398
|
+
# Run with faults
|
|
399
|
+
k6 run test.js --out json=fault.json
|
|
400
|
+
|
|
401
|
+
# Compare results
|
|
402
|
+
# Baseline: ~100ms p95, 0% errors
|
|
403
|
+
# With faults: ~150ms p95, 10% errors
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**What to compare:**
|
|
407
|
+
| Metric | Baseline | With Faults | Acceptable Delta |
|
|
408
|
+
|--------|----------|-------------|------------------|
|
|
409
|
+
| p(95) latency | 100ms | 150ms | < 2x baseline |
|
|
410
|
+
| Error rate | 0% | 10% | = injected rate |
|
|
411
|
+
| Throughput | 100 req/s | 90 req/s | > 70% baseline |
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Multi-Scenario Architecture
|
|
416
|
+
|
|
417
|
+
Use separate `exec` functions for load generation and fault injection in the same test:
|
|
418
|
+
|
|
419
|
+
```javascript
|
|
420
|
+
export const options = {
|
|
421
|
+
scenarios: {
|
|
422
|
+
// Load generation runs the entire test
|
|
423
|
+
load: {
|
|
424
|
+
executor: 'constant-arrival-rate',
|
|
425
|
+
rate: 100,
|
|
426
|
+
timeUnit: '1s',
|
|
427
|
+
duration: '5m',
|
|
428
|
+
preAllocatedVUs: 50,
|
|
429
|
+
maxVUs: 100,
|
|
430
|
+
exec: 'default',
|
|
431
|
+
},
|
|
432
|
+
// Fault injection runs once at a specific time
|
|
433
|
+
disrupt: {
|
|
434
|
+
executor: 'shared-iterations',
|
|
435
|
+
iterations: 1,
|
|
436
|
+
vus: 1,
|
|
437
|
+
exec: 'disrupt',
|
|
438
|
+
startTime: '30s', // Start after 30s baseline
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
**Timeline:**
|
|
445
|
+
```
|
|
446
|
+
0s 30s 2m30s 5m
|
|
447
|
+
|---------|---------------------|------------------|
|
|
448
|
+
Baseline Fault Active Recovery
|
|
449
|
+
(clean) (degraded) (back to normal)
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
This pattern is preferred over `setup()` injection because:
|
|
453
|
+
- Captures clean baseline metrics before faults
|
|
454
|
+
- Fault injection timing is precise via `startTime`
|
|
455
|
+
- Load and disruption scenarios run independently
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Threshold Strategy for Resilience Tests
|
|
460
|
+
|
|
461
|
+
### Relaxed Overall + Strict Recovery
|
|
462
|
+
|
|
463
|
+
```javascript
|
|
464
|
+
export const options = {
|
|
465
|
+
thresholds: {
|
|
466
|
+
// Overall: relaxed (includes chaos phase)
|
|
467
|
+
http_req_failed: ['rate<0.5'],
|
|
468
|
+
http_req_duration: ['p(95)<5000'],
|
|
469
|
+
|
|
470
|
+
// Tag-based: strict for recovery phase
|
|
471
|
+
'http_req_duration{phase:recovery}': ['p(95)<500'],
|
|
472
|
+
'http_req_failed{phase:recovery}': ['rate<0.01'],
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Abort on Catastrophic Failure
|
|
478
|
+
|
|
479
|
+
```javascript
|
|
480
|
+
export const options = {
|
|
481
|
+
thresholds: {
|
|
482
|
+
// Abort if system completely fails
|
|
483
|
+
http_req_failed: [{
|
|
484
|
+
threshold: 'rate<0.95',
|
|
485
|
+
abortOnFail: true,
|
|
486
|
+
delayAbortEval: '10s',
|
|
487
|
+
}],
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Recommended Thresholds by Pattern
|
|
493
|
+
|
|
494
|
+
| Pattern | Error Rate | p(95) Latency | Notes |
|
|
495
|
+
|---------|-----------|---------------|-------|
|
|
496
|
+
| Graceful Degradation | < 50% | < 5s | Errors expected, no cascade |
|
|
497
|
+
| Circuit Breaker | < 30% | < 3s | Fast fallback expected |
|
|
498
|
+
| Recovery | < 1% (recovery) | < 500ms (recovery) | Strict after fault removal |
|
|
499
|
+
| Pod Failure | < 30% | < 10s | Transient errors during reschedule |
|
|
500
|
+
| Failover | < 10% | < 2s | Quick reroute expected |
|