Skip to content

Commit 100f0e1

Browse files
committed
Feat: Stress tests on prod
1 parent a617ef3 commit 100f0e1

File tree

3 files changed

+252
-1
lines changed

3 files changed

+252
-1
lines changed
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import http from 'k6/http';
2+
import { check, group, sleep } from 'k6';
3+
import { SharedArray } from 'k6/data';
4+
5+
const baseUrl = __ENV.BASE_URL || "http://localhost:9999";
6+
7+
// Helper functions
8+
const randomClienteId = () => Math.floor(Math.random() * 5) + 1;
9+
const randomValorTransacao = () => Math.floor(Math.random() * 10000) + 1;
10+
const randomDescricao = () => {
11+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
12+
return Array.from({ length: 10 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
13+
};
14+
15+
// Validation function: checks that saldo is not below (limite * -1)
16+
const validateSaldoLimite = (saldo, limite) => {
17+
return saldo >= limite * -1;
18+
};
19+
20+
// Shared initial client data (same as used in the validacoes folder)
21+
const saldosIniciaisClientes = new SharedArray('clientes', () => [
22+
{ id: 1, limite: 1000 * 100 },
23+
{ id: 2, limite: 800 * 100 },
24+
{ id: 3, limite: 10000 * 100 },
25+
{ id: 4, limite: 100000 * 100 },
26+
{ id: 5, limite: 5000 * 100 },
27+
]);
28+
29+
export const options = {
30+
scenarios: {
31+
// Validation scenarios start at 0s to use fresh state
32+
validacoes: {
33+
executor: 'per-vu-iterations',
34+
vus: saldosIniciaisClientes.length,
35+
iterations: 1,
36+
startTime: '0s',
37+
exec: 'validacoes',
38+
},
39+
cliente_nao_encontrado: {
40+
executor: 'per-vu-iterations',
41+
vus: 1,
42+
iterations: 1,
43+
startTime: '0s',
44+
exec: 'cliente_nao_encontrado',
45+
},
46+
// Load scenarios for debitos and creditos start after 10s
47+
debitos: {
48+
executor: 'ramping-vus',
49+
startVUs: 1,
50+
stages: [
51+
{ duration: '2m', target: 220 },
52+
{ duration: '2m', target: 220 },
53+
],
54+
startTime: '10s',
55+
exec: 'debitos',
56+
},
57+
creditos: {
58+
executor: 'ramping-vus',
59+
startVUs: 1,
60+
stages: [
61+
{ duration: '2m', target: 110 },
62+
{ duration: '2m', target: 110 },
63+
],
64+
startTime: '10s',
65+
exec: 'creditos',
66+
},
67+
// The extratos scenario is now executed only once with exactly 10 VUs
68+
extratos: {
69+
executor: 'per-vu-iterations',
70+
vus: 10,
71+
iterations: 1,
72+
startTime: '10s',
73+
exec: 'extratos',
74+
},
75+
},
76+
};
77+
78+
export function debitos() {
79+
const payload = JSON.stringify({
80+
valor: randomValorTransacao(),
81+
tipo: 'd',
82+
descricao: randomDescricao(),
83+
});
84+
85+
const res = http.post(
86+
`${baseUrl}/clientes/${randomClienteId()}/transacoes`,
87+
payload,
88+
{ headers: { 'Content-Type': 'application/json' } }
89+
);
90+
91+
check(res, {
92+
'status 200 or 422': (r) => [200, 422].includes(r.status),
93+
});
94+
95+
if (res.status === 200) {
96+
check(res, {
97+
'Consistência saldo/limite': (r) => {
98+
try {
99+
const saldo = r.json('saldo');
100+
const limite = r.json('limite');
101+
return validateSaldoLimite(saldo, limite);
102+
} catch (e) {
103+
return false;
104+
}
105+
}
106+
});
107+
}
108+
}
109+
110+
export function creditos() {
111+
const payload = JSON.stringify({
112+
valor: randomValorTransacao(),
113+
tipo: 'c',
114+
descricao: randomDescricao(),
115+
});
116+
117+
const res = http.post(
118+
`${baseUrl}/clientes/${randomClienteId()}/transacoes`,
119+
payload,
120+
{ headers: { 'Content-Type': 'application/json' } }
121+
);
122+
123+
check(res, {
124+
'status 200': (r) => r.status === 200,
125+
});
126+
127+
if (res.status === 200) {
128+
check(res, {
129+
'Consistência saldo/limite': (r) => {
130+
try {
131+
const saldo = r.json('saldo');
132+
const limite = r.json('limite');
133+
return validateSaldoLimite(saldo, limite);
134+
} catch (e) {
135+
return false;
136+
}
137+
}
138+
});
139+
}
140+
}
141+
142+
export function extratos() {
143+
const res = http.get(`${baseUrl}/clientes/${randomClienteId()}/extrato`);
144+
145+
check(res, {
146+
'status 200': (r) => r.status === 200,
147+
});
148+
149+
if (res.status === 200) {
150+
check(res, {
151+
'Consistência extrato': (r) => {
152+
try {
153+
return validateSaldoLimite(
154+
r.json('saldo.total'),
155+
r.json('saldo.limite')
156+
);
157+
} catch (e) {
158+
return false;
159+
}
160+
}
161+
});
162+
}
163+
}
164+
165+
export function validacoes() {
166+
// Each VU uses a client from the shared array based on __VU index.
167+
const index = (__VU - 1) % saldosIniciaisClientes.length;
168+
const cliente = saldosIniciaisClientes[index];
169+
170+
group('Validações cliente', () => {
171+
// Confirm initial client state (saldo.total should be 0)
172+
let res = http.get(`${baseUrl}/clientes/${cliente.id}/extrato`);
173+
check(res, {
174+
'status 200': (r) => r.status === 200,
175+
'limite correto': (r) => r.json('saldo.limite') === cliente.limite,
176+
'saldo inicial 0': (r) => r.json('saldo.total') === 0,
177+
});
178+
179+
// Execute 2 transactions in sequence: a credit ("toma") then a debit ("devolve")
180+
res = http.post(
181+
`${baseUrl}/clientes/${cliente.id}/transacoes`,
182+
JSON.stringify({ valor: 1, tipo: 'c', descricao: 'toma' }),
183+
{ headers: { 'Content-Type': 'application/json' } }
184+
);
185+
check(res, {
186+
'status 200': (r) => r.status === 200,
187+
'Consistência saldo/limite': (r) => validateSaldoLimite(r.json('saldo'), r.json('limite')),
188+
});
189+
190+
res = http.post(
191+
`${baseUrl}/clientes/${cliente.id}/transacoes`,
192+
JSON.stringify({ valor: 1, tipo: 'd', descricao: 'devolve' }),
193+
{ headers: { 'Content-Type': 'application/json' } }
194+
);
195+
check(res, {
196+
'status 200': (r) => r.status === 200,
197+
'Consistência saldo/limite': (r) => validateSaldoLimite(r.json('saldo'), r.json('limite')),
198+
});
199+
200+
// Allow a pause for transactions to be properly processed
201+
sleep(1);
202+
203+
// Validate that the latest extrato shows the two transactions in the expected order.
204+
res = http.get(`${baseUrl}/clientes/${cliente.id}/extrato`);
205+
check(res, {
206+
'transações recentes': (r) => {
207+
const transacoes = r.json('ultimas_transacoes');
208+
return transacoes &&
209+
transacoes.length >= 2 &&
210+
transacoes[0].descricao === 'devolve' &&
211+
transacoes[0].tipo === 'd' &&
212+
transacoes[1].descricao === 'toma' &&
213+
transacoes[1].tipo === 'c';
214+
},
215+
});
216+
217+
// Execute invalid requests; allow either 422 or 400 as valid responses
218+
const invalidRequests = [
219+
{ valor: 1.2, tipo: 'd', descricao: 'devolve', expectedStatus: 422 },
220+
{ valor: 1, tipo: 'x', descricao: 'devolve', expectedStatus: 422 },
221+
{ valor: 1, tipo: 'c', descricao: '123456789 e mais', expectedStatus: 422 },
222+
{ valor: 1, tipo: 'c', descricao: '', expectedStatus: 422 },
223+
{ valor: 1, tipo: 'c', descricao: null, expectedStatus: 422 },
224+
];
225+
226+
invalidRequests.forEach((req) => {
227+
res = http.post(
228+
`${baseUrl}/clientes/${cliente.id}/transacoes`,
229+
JSON.stringify(req),
230+
{ headers: { 'Content-Type': 'application/json' } }
231+
);
232+
check(res, {
233+
[`status ${req.expectedStatus}`]: (r) =>
234+
r.status === req.expectedStatus || r.status === 400,
235+
});
236+
});
237+
});
238+
}
239+
240+
export function cliente_nao_encontrado() {
241+
const res = http.get(`${baseUrl}/clientes/6/extrato`);
242+
check(res, {
243+
'status 404': (r) => r.status === 404,
244+
});
245+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/sh
2+
3+
echo "Tests will start in 15 seconds..."
4+
sleep 15
5+
6+
k6 run rinha-test.js -o xk6-influxdb

prod/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ services:
7070
- BASE_URL=http://nginx:9999
7171
- K6_WEB_DASHBOARD=true
7272
volumes:
73-
- "./test/stress-test:/app"
73+
- "./conf/stress-test:/app"
7474
working_dir: /app
7575
tty: true
7676
stdin_open: true

0 commit comments

Comments
 (0)