Skip to content

Commit 6994449

Browse files
authored
Merge pull request #34 from rxcod9/feature/async-crud
Feature/async crud
2 parents af24392 + d12efe5 commit 6994449

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+2575
-392
lines changed

config/config.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,13 +150,6 @@
150150
*/
151151
'http_compression' => true,
152152

153-
/**
154-
* Ssl alpn protocols.
155-
*
156-
* @var string
157-
*/
158-
'ssl_alpn_protocols' => 'h2,http/1.1', // critical for HTTP/2
159-
160153
/**
161154
* Enable HTTP POST parsing.
162155
*
@@ -344,5 +337,10 @@
344337
'rateLimit' => [
345338
'throttle' => env('RATE_LIMIT_THROTTLE', 100),
346339
'skip_ip_patterns' => env('RATE_LIMIT_SKIP_IP_PATTERN', '/^(127\.0\.0\.1|::1|172\.\d{1,3}\.\d{1,3}\.\d{1,3}|10\.\d{1,3}\.\d{1,3}\.\d{1,3})$/'),
340+
],
341+
342+
'async' => [
343+
'channel_capacity' => (int) env('ASYNC_CHANNEL_CAPACITY', 1024),
344+
// 'channel_workers' => (int) env('ASYNC_CHANNEL_WORKERS', 4),
347345
]
348346
];

k6/lib/crud.js

Lines changed: 201 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,231 @@
11
/**
22
* @file lib/crud.js
3-
* @description Implements generic CRUD execution logic per entity.
3+
* @description Implements generic CRUD execution logic per entity with reduced complexity and parameter grouping.
44
*/
55

66
import http from 'k6/http';
7-
import { check } from 'k6';
87
import { ENV } from './env.js';
9-
import { secureRandomInt } from './utils.js';
8+
import { recordTrendAndCheck, secureRandomInt } from './utils.js';
109

1110
/**
12-
* Performs a single CRUD operation based on allowedOps from ENV.
13-
*
14-
* @param {object} params
15-
* @param {string[]} params.vuIds - IDs accessible to this VU
16-
* @param {string[]} params.hotIds - Hot IDs for read-heavy or update-heavy ops
17-
* @param {string[]} params.coolIds - Cool IDs to skip for certain ops
18-
* @param {string} params.entity - Entity name (e.g., users, items)
19-
* @param {Function} params.generateFn - Function to generate new entity payloads
20-
* @param {Record<string, import('k6/metrics').Trend>} params.trends - Trend metrics for the entity
21-
* @param {string[]} params.allowedOps - Allowed operations (e.g., list,read,create,update,delete)
22-
* @returns {void}
11+
* Helper: Selects an entity ID based on hot/cool/available sets.
12+
* @param {string[]} hotIds
13+
* @param {string[]} vuIds
14+
* @param {string[]} coolIds
15+
* @param {boolean} preferHot
16+
* @returns {string|null}
2317
*/
24-
export function performCrudAction({
25-
vuIds,
26-
hotIds,
27-
coolIds,
28-
entity,
29-
generateFn,
30-
trends,
31-
allowedOps
32-
}) {
33-
// Choose random operation based on allowedOps
34-
const op = allowedOps[secureRandomInt(0, allowedOps.length)];
35-
const baseUrl = `${ENV.BASE_URL}/${entity}`;
18+
function selectTargetId(hotIds, vuIds, coolIds, preferHot = true) {
19+
const pool = preferHot && hotIds.length ? hotIds : vuIds;
20+
if (!pool.length) return null;
3621

22+
for (let i = 0; i < 3; i++) { // try a few times
23+
const id = pool[secureRandomInt(0, pool.length)];
24+
if (!coolIds.includes(id)) return id;
25+
}
26+
27+
// fallback to any id
28+
// return vuIds.length ? vuIds[secureRandomInt(0, vuIds.length)] : null;
29+
return null;
30+
}
31+
32+
/**
33+
* Executes a CRUD HTTP request for the given operation.
34+
* Reduced parameters by grouping logically related args into objects.
35+
*
36+
* @param {string} op
37+
* @param {string} baseUrl
38+
* @param {string} entity
39+
* @param {{ generateFn: Function, trends: Record<string, import('k6/metrics').Trend>, contentType: string }} context
40+
* @param {{ vuIds: string[], hotIds: string[], coolIds: string[] }} idSets
41+
*/
42+
function executeCrudOp(op, baseUrl, entity, context, idSets) {
3743
switch (op) {
3844
case 'list': {
39-
const res = http.get(`${baseUrl}`);
40-
trends.list?.add(res.timings.duration);
41-
check(res, { [`${entity} LIST success`]: r => r.status === 200 });
45+
executeList(baseUrl, entity, context);
4246
break;
4347
}
44-
4548
case 'read': {
46-
const id = hotIds.length
47-
? hotIds[secureRandomInt(0, hotIds.length)]
48-
: vuIds[secureRandomInt(0, vuIds.length)];
49-
if (!id || coolIds.includes(id)) return;
50-
const res = http.get(`${baseUrl}/${id}`);
51-
trends.read?.add(res.timings.duration);
52-
check(res, { [`${entity} READ success`]: r => r.status === 200 });
49+
executeRead(baseUrl, entity, context, idSets);
5350
break;
5451
}
55-
5652
case 'create': {
57-
const obj = generateFn(secureRandomInt(0, 1000000));
58-
const res = http.post(baseUrl, JSON.stringify(obj), {
59-
headers: { 'Content-Type': 'application/json' }
60-
});
61-
trends.create?.add(res.timings.duration);
62-
check(res, { [`${entity} CREATE success`]: r => r.status === 201 });
63-
if (res.status === 201) {
64-
try {
65-
const parsed = JSON.parse(res.body);
66-
if (parsed?.id) vuIds.push(parsed.id);
67-
} catch { }
68-
}
53+
executeCreate(baseUrl, entity, context, idSets);
6954
break;
7055
}
71-
7256
case 'update': {
73-
const id = hotIds.length
74-
? hotIds[secureRandomInt(0, hotIds.length)]
75-
: vuIds[secureRandomInt(0, vuIds.length)];
76-
if (!id || coolIds.includes(id)) return;
77-
const obj = generateFn(id);
78-
const res = http.put(`${baseUrl}/${id}`, JSON.stringify(obj), {
79-
headers: { 'Content-Type': 'application/json' }
80-
});
81-
trends.update?.add(res.timings.duration);
82-
check(res, { [`${entity} UPDATE success`]: r => r.status === 200 });
57+
executeUpdate(baseUrl, entity, context, idSets);
8358
break;
8459
}
85-
8660
case 'delete': {
87-
const id = coolIds[secureRandomInt(0, coolIds.length)];
88-
if (!id) return;
89-
const res = http.del(`${baseUrl}/${id}`);
90-
trends.delete?.add(res.timings.duration);
91-
check(res, { [`${entity} DELETE success`]: r => [200, 204].includes(r.status) });
61+
executeDelete(baseUrl, entity, context, idSets);
9262
break;
9363
}
94-
9564
default:
9665
console.warn(`[WARN] Unsupported CRUD operation: ${op}`);
9766
}
9867
}
68+
69+
/**
70+
* Executes a CRUD HTTP request for the given operation.
71+
* Reduced parameters by grouping logically related args into objects.
72+
*
73+
* @param {string} baseUrl
74+
* @param {string} entity
75+
* @param {{ generateFn: Function, trends: Record<string, import('k6/metrics').Trend>, contentType: string }} context
76+
*/
77+
function executeList(baseUrl, entity, context) {
78+
const { trends } = context;
79+
80+
const res = http.get(baseUrl);
81+
recordTrendAndCheck(res, entity, "list", trends.list, 200);
82+
}
83+
84+
/**
85+
* Executes a CRUD HTTP request for the given operation.
86+
* Reduced parameters by grouping logically related args into objects.
87+
*
88+
* @param {string} baseUrl
89+
* @param {string} entity
90+
* @param {{ generateFn: Function, trends: Record<string, import('k6/metrics').Trend>, contentType: string }} context
91+
* @param {{ vuIds: string[], hotIds: string[], coolIds: string[] }} idSets
92+
*/
93+
function executeRead(baseUrl, entity, context, idSets) {
94+
const { trends } = context;
95+
const { vuIds, hotIds, coolIds } = idSets;
96+
97+
const id = selectTargetId(hotIds, vuIds, coolIds, true);
98+
if (!id) {
99+
console.log("Skipping read no id");
100+
return;
101+
}
102+
const res = http.get(`${baseUrl}/${id}`);
103+
recordTrendAndCheck(res, entity, "read", trends.read, 200);
104+
}
105+
106+
/**
107+
* Executes a CRUD HTTP request for the given operation.
108+
* Reduced parameters by grouping logically related args into objects.
109+
*
110+
* @param {string} baseUrl
111+
* @param {string} entity
112+
* @param {{ generateFn: Function, trends: Record<string, import('k6/metrics').Trend>, contentType: string }} context
113+
* @param {{ vuIds: string[], hotIds: string[], coolIds: string[] }} idSets
114+
*/
115+
function executeCreate(baseUrl, entity, context, idSets) {
116+
const { generateFn, trends, contentType = 'json' } = context;
117+
const { vuIds } = idSets;
118+
119+
const obj = generateFn(secureRandomInt(0, 1000000));
120+
121+
// choose encoding based on contentType
122+
let body, headers;
123+
if (contentType === 'form') {
124+
body = encodeFormData(obj);
125+
headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
126+
} else {
127+
body = JSON.stringify(obj);
128+
headers = { 'Content-Type': 'application/json' };
129+
}
130+
131+
const res = http.post(baseUrl, body, { headers });
132+
recordTrendAndCheck(res, entity, "create", trends.create, [200, 201, 202]);
133+
134+
if (res.status === 201) {
135+
try {
136+
const parsed = JSON.parse(res.body);
137+
if (parsed?.id) vuIds.push(parsed.id);
138+
} catch {
139+
// ignore malformed response
140+
}
141+
}
142+
}
143+
144+
/**
145+
* Executes a CRUD HTTP request for the given operation.
146+
* Reduced parameters by grouping logically related args into objects.
147+
*
148+
* @param {string} baseUrl
149+
* @param {string} entity
150+
* @param {{ generateFn: Function, trends: Record<string, import('k6/metrics').Trend>, contentType: string }} context
151+
* @param {{ vuIds: string[], hotIds: string[], coolIds: string[] }} idSets
152+
*/
153+
function executeUpdate(baseUrl, entity, context, idSets) {
154+
const { generateFn, trends, contentType = 'json' } = context;
155+
const { vuIds, hotIds, coolIds } = idSets;
156+
157+
const id = selectTargetId(hotIds, vuIds, coolIds, false);
158+
if (!id) {
159+
console.log("Skipping update no id");
160+
return;
161+
}
162+
const obj = generateFn(id);
163+
164+
// choose encoding based on contentType
165+
let body, headers;
166+
if (contentType === 'form') {
167+
body = encodeFormData(obj);
168+
headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
169+
} else {
170+
body = JSON.stringify(obj);
171+
headers = { 'Content-Type': 'application/json' };
172+
}
173+
174+
const res = http.put(`${baseUrl}/${id}`, body, { headers });
175+
recordTrendAndCheck(res, entity, "update", trends.update, [200, 202]);
176+
}
177+
178+
/**
179+
* Executes a CRUD HTTP request for the given operation.
180+
* Reduced parameters by grouping logically related args into objects.
181+
*
182+
* @param {string} baseUrl
183+
* @param {string} entity
184+
* @param {{ generateFn: Function, trends: Record<string, import('k6/metrics').Trend>, contentType: string }} context
185+
* @param {{ vuIds: string[], hotIds: string[], coolIds: string[] }} idSets
186+
*/
187+
function executeDelete(baseUrl, entity, context, idSets) {
188+
const { trends } = context;
189+
const { coolIds } = idSets;
190+
191+
const id = coolIds[secureRandomInt(0, coolIds.length)];
192+
if (!id) {
193+
console.log("Skipping delete no id");
194+
return;
195+
}
196+
const res = http.del(`${baseUrl}/${id}`);
197+
recordTrendAndCheck(res, entity, "delete", trends.delete, [200, 202, 204]);
198+
}
199+
200+
/**
201+
* Performs a single CRUD operation based on allowedOps from ENV.
202+
*
203+
* @param {object} params
204+
* @param {string[]} params.vuIds
205+
* @param {string[]} params.hotIds
206+
* @param {string[]} params.coolIds
207+
* @param {string} params.entity
208+
* @param {Function} params.generateFn
209+
* @param {Record<string, import('k6/metrics').Trend>} params.trends
210+
* @param {string[]} params.allowedOps
211+
* @param {bool} params.async
212+
* @returns {void}
213+
*/
214+
export function performCrudAction({
215+
vuIds,
216+
hotIds,
217+
coolIds,
218+
entity,
219+
generateFn,
220+
trends,
221+
allowedOps,
222+
contentType = 'json' // can be 'json' or 'form'
223+
}) {
224+
const op = allowedOps[secureRandomInt(0, allowedOps.length)];
225+
const baseUrl = `${ENV.BASE_URL}/${entity}`;
226+
227+
const idSets = { vuIds, hotIds, coolIds };
228+
const context = { generateFn, trends, contentType };
229+
230+
executeCrudOp(op, baseUrl, entity, context, idSets);
231+
}

k6/lib/env.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,16 @@ export const ENV = {
4040
BASE_URL: __ENV.BASE_URL || 'http://localhost:9501',
4141

4242
// Allow comma-separated entities (e.g., users,items,orders)
43-
ENTITIES: parseList(__ENV.ENTITIES, ['users', 'items']),
43+
ENTITIES: parseList(__ENV.ENTITIES, ['users', 'items', 'async-users']),
4444

4545
// Allow comma-separated CRUD operations (e.g., list,read,create,update,delete)
4646
CRUD: parseList(__ENV.CRUD, ['list', 'read', 'create', 'update']),
4747

48-
TOTAL_ENTITIES: Number(__ENV.TOTAL_ENTITIES) || 200,
48+
TOTAL_ENTITIES: Number(__ENV.TOTAL_ENTITIES) || 2000,
4949
HOT_PERCENT: Number(__ENV.HOT_PERCENT) || 0.1,
5050
COOL_PERCENT: Number(__ENV.COOL_PERCENT) || 0.1,
51-
TOTAL_EXECUTIONS: Number(__ENV.TOTAL_EXECUTIONS) || 2000,
52-
MAX_VUS: Number(__ENV.MAX_VUS) || 50,
51+
TOTAL_EXECUTIONS: Number(__ENV.TOTAL_EXECUTIONS) || 20000,
52+
MAX_VUS: Number(__ENV.MAX_VUS) || 200,
5353
MAX_DURATION: __ENV.MAX_DURATION || '10m'
5454
};
5555

k6/lib/metrics.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { Trend } from 'k6/metrics';
88
import { ENV } from './env.js';
9+
import { toUpperSnake } from './utils.js';
910

1011
/**
1112
* Registry for all metrics, grouped by entity and operation.
@@ -21,16 +22,16 @@ import { ENV } from './env.js';
2122
export const METRICS_REGISTRY = {};
2223

2324
// Ensure fallbacks to safe defaults if ENV misfires.
24-
const entities = Array.isArray(ENV.ENTITIES) ? ENV.ENTITIES : ['users', 'items'];
25+
const entities = Array.isArray(ENV.ENTITIES) ? ENV.ENTITIES : ['users', 'items', 'async-users'];
2526
const crudOps = Array.isArray(ENV.CRUD) ? ENV.CRUD : ['list', 'read', 'create', 'update'];
2627

2728
for (const entity of entities) {
28-
const upper = entity.toUpperCase();
29+
const upperSnake = toUpperSnake(entity);
2930
METRICS_REGISTRY[entity] = {};
3031

3132
for (const op of crudOps) {
3233
const opUpper = op.toUpperCase();
33-
const metricName = `${upper}_${opUpper}_latency_ms`;
34+
const metricName = `${upperSnake}_${opUpper}_latency_ms`;
3435

3536
// Each Trend metric tracks latency for specific entity and CRUD operation.
3637
METRICS_REGISTRY[entity][op] = new Trend(metricName);
@@ -60,7 +61,7 @@ export function buildThresholds() {
6061

6162
for (const entity of Object.keys(METRICS_REGISTRY)) {
6263
for (const op of Object.keys(METRICS_REGISTRY[entity])) {
63-
const metric = `${entity.toUpperCase()}_${op.toUpperCase()}_latency_ms`;
64+
const metric = `${toUpperSnake(entity)}_${op.toUpperCase()}_latency_ms`;
6465
thresholds[metric] = defaultRules[op] || ['avg<200'];
6566
}
6667
}

0 commit comments

Comments
 (0)