@pingpolls/redisq 0.1.0
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/.env.example +2 -0
- package/.prototools +1 -0
- package/LICENSE +201 -0
- package/README.md +784 -0
- package/app.test.ts +529 -0
- package/app.ts +774 -0
- package/benchmark/stress-worker.ts +50 -0
- package/benchmark/stress.ts +359 -0
- package/biome.json +80 -0
- package/compose.yml +20 -0
- package/package.json +31 -0
- package/redis.conf +1 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import RedisQueue from "../app";
|
|
2
|
+
|
|
3
|
+
interface WorkerMessage {
|
|
4
|
+
type: "start";
|
|
5
|
+
data: {
|
|
6
|
+
workerIndex: number;
|
|
7
|
+
messagesPerWorker: number;
|
|
8
|
+
testMessage: string;
|
|
9
|
+
qname: string;
|
|
10
|
+
redisConfig: {
|
|
11
|
+
host: string;
|
|
12
|
+
port: string;
|
|
13
|
+
namespace: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
self.addEventListener("message", async (event: MessageEvent<WorkerMessage>) => {
|
|
19
|
+
if (event.data.type === "start") {
|
|
20
|
+
const { messagesPerWorker, testMessage, qname, redisConfig } =
|
|
21
|
+
event.data.data;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const queue = new RedisQueue(redisConfig);
|
|
25
|
+
const latencies: number[] = [];
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < messagesPerWorker; i++) {
|
|
28
|
+
const start = performance.now();
|
|
29
|
+
await queue.sendMessage({
|
|
30
|
+
message: testMessage,
|
|
31
|
+
qname,
|
|
32
|
+
});
|
|
33
|
+
const latency = performance.now() - start;
|
|
34
|
+
latencies.push(latency);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await queue.close();
|
|
38
|
+
|
|
39
|
+
self.postMessage({
|
|
40
|
+
data: { latencies },
|
|
41
|
+
type: "result",
|
|
42
|
+
});
|
|
43
|
+
} catch (error) {
|
|
44
|
+
self.postMessage({
|
|
45
|
+
data: { error: (error as Error).message },
|
|
46
|
+
type: "error",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import type { Worker } from "bun";
|
|
4
|
+
import RedisQueue from "../app";
|
|
5
|
+
|
|
6
|
+
const COLORS = {
|
|
7
|
+
blue: "\x1b[34m",
|
|
8
|
+
bright: "\x1b[1m",
|
|
9
|
+
cyan: "\x1b[36m",
|
|
10
|
+
green: "\x1b[32m",
|
|
11
|
+
magenta: "\x1b[35m",
|
|
12
|
+
red: "\x1b[31m",
|
|
13
|
+
reset: "\x1b[0m",
|
|
14
|
+
yellow: "\x1b[33m",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const WORKER_COUNT = 16;
|
|
18
|
+
const CONCURRENCY = 50;
|
|
19
|
+
const MSG_COUNT = 100_000;
|
|
20
|
+
|
|
21
|
+
interface StressTestConfig {
|
|
22
|
+
messageCount: number;
|
|
23
|
+
workerCount: number;
|
|
24
|
+
testMessage: string;
|
|
25
|
+
concurrency: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface TestResults {
|
|
29
|
+
totalMessages: number;
|
|
30
|
+
duration: number;
|
|
31
|
+
throughput: number;
|
|
32
|
+
latencies: number[];
|
|
33
|
+
p50: number;
|
|
34
|
+
p95: number;
|
|
35
|
+
p99: number;
|
|
36
|
+
min: number;
|
|
37
|
+
max: number;
|
|
38
|
+
avg: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface WorkerMessage {
|
|
42
|
+
type: "start" | "result" | "error";
|
|
43
|
+
data?: {
|
|
44
|
+
workerIndex?: number;
|
|
45
|
+
messagesPerWorker?: number;
|
|
46
|
+
testMessage?: string;
|
|
47
|
+
qname?: string;
|
|
48
|
+
redisConfig?: {
|
|
49
|
+
host: string;
|
|
50
|
+
port: string;
|
|
51
|
+
namespace: string;
|
|
52
|
+
};
|
|
53
|
+
latencies?: number[];
|
|
54
|
+
error?: string;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function log(color: keyof typeof COLORS, message: string) {
|
|
59
|
+
console.log(`${COLORS[color]}${message}${COLORS.reset}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function calculatePercentile(sorted: number[], percentile: number): number {
|
|
63
|
+
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
|
|
64
|
+
return sorted[Math.max(0, index)] ?? 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function calculateStats(
|
|
68
|
+
latencies: number[],
|
|
69
|
+
): Omit<TestResults, "totalMessages" | "duration" | "throughput"> {
|
|
70
|
+
const sorted = [...latencies].sort((a, b) => a - b);
|
|
71
|
+
const sum = latencies.reduce((acc, val) => acc + val, 0);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
avg: sum / latencies.length,
|
|
75
|
+
latencies,
|
|
76
|
+
max: sorted[sorted.length - 1] ?? 0,
|
|
77
|
+
min: sorted[0] ?? 0,
|
|
78
|
+
p50: calculatePercentile(sorted, 50),
|
|
79
|
+
p95: calculatePercentile(sorted, 95),
|
|
80
|
+
p99: calculatePercentile(sorted, 99),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function printResults(testName: string, results: TestResults) {
|
|
85
|
+
log("bright", `\n${"=".repeat(60)}`);
|
|
86
|
+
log("cyan", ` ${testName}`);
|
|
87
|
+
log("bright", "=".repeat(60));
|
|
88
|
+
|
|
89
|
+
log("green", "\n📊 Overall Performance:");
|
|
90
|
+
console.log(
|
|
91
|
+
` Total Messages: ${results.totalMessages.toLocaleString()}`,
|
|
92
|
+
);
|
|
93
|
+
console.log(` Duration: ${results.duration.toFixed(2)}s`);
|
|
94
|
+
console.log(` Throughput: ${results.throughput.toFixed(2)} msg/s`);
|
|
95
|
+
|
|
96
|
+
log("yellow", "\n⚡ Latency Statistics (ms):");
|
|
97
|
+
console.log(` Min: ${results.min.toFixed(2)} ms`);
|
|
98
|
+
console.log(` Average: ${results.avg.toFixed(2)} ms`);
|
|
99
|
+
console.log(` Median (p50): ${results.p50.toFixed(2)} ms`);
|
|
100
|
+
console.log(` p95: ${results.p95.toFixed(2)} ms`);
|
|
101
|
+
console.log(` p99: ${results.p99.toFixed(2)} ms`);
|
|
102
|
+
console.log(` Max: ${results.max.toFixed(2)} ms`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function testRegularQueueParallel(
|
|
106
|
+
config: StressTestConfig,
|
|
107
|
+
): Promise<TestResults> {
|
|
108
|
+
const queue = new RedisQueue({
|
|
109
|
+
host: process.env.REDIS_HOST || "127.0.0.1",
|
|
110
|
+
namespace: "stress-test",
|
|
111
|
+
port: process.env.REDIS_PORT || "6379",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const qname = "stress-regular";
|
|
115
|
+
await queue.createQueue({ maxRetries: 0, qname, maxsize: 150_000 });
|
|
116
|
+
|
|
117
|
+
const allLatencies: number[] = [];
|
|
118
|
+
let receivedCount = 0;
|
|
119
|
+
const messagesPerWorker = Math.floor(
|
|
120
|
+
config.messageCount / config.workerCount,
|
|
121
|
+
);
|
|
122
|
+
const actualMessageCount = messagesPerWorker * config.workerCount;
|
|
123
|
+
|
|
124
|
+
const workerPromise = new Promise<void>((resolve) => {
|
|
125
|
+
queue.startWorker(
|
|
126
|
+
qname,
|
|
127
|
+
async () => {
|
|
128
|
+
receivedCount++;
|
|
129
|
+
if (receivedCount === actualMessageCount) {
|
|
130
|
+
resolve();
|
|
131
|
+
}
|
|
132
|
+
return { success: true };
|
|
133
|
+
},
|
|
134
|
+
{ concurrency: config.concurrency, silent: true },
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
log(
|
|
139
|
+
"blue",
|
|
140
|
+
`\n🚀 Spawning ${config.workerCount} workers to send ${actualMessageCount.toLocaleString()} messages...`,
|
|
141
|
+
);
|
|
142
|
+
const startTime = performance.now();
|
|
143
|
+
|
|
144
|
+
const workers: Worker[] = [];
|
|
145
|
+
const workerPromises: Promise<number[]>[] = [];
|
|
146
|
+
|
|
147
|
+
for (let i = 0; i < config.workerCount; i++) {
|
|
148
|
+
const worker = new Worker(
|
|
149
|
+
new URL("./stress-worker.ts", import.meta.url).href,
|
|
150
|
+
);
|
|
151
|
+
workers.push(worker);
|
|
152
|
+
|
|
153
|
+
const workerPromise = new Promise<number[]>((resolve, reject) => {
|
|
154
|
+
worker.addEventListener(
|
|
155
|
+
"message",
|
|
156
|
+
(event: MessageEvent<WorkerMessage>) => {
|
|
157
|
+
if (event.data.type === "result") {
|
|
158
|
+
resolve(event.data.data?.latencies || []);
|
|
159
|
+
worker.terminate();
|
|
160
|
+
} else if (event.data.type === "error") {
|
|
161
|
+
reject(
|
|
162
|
+
new Error(event.data.data?.error || "Worker error"),
|
|
163
|
+
);
|
|
164
|
+
worker.terminate();
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
worker.addEventListener("error", (error) => {
|
|
170
|
+
reject(error);
|
|
171
|
+
worker.terminate();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
worker.postMessage({
|
|
175
|
+
data: {
|
|
176
|
+
testMessage: config.testMessage,
|
|
177
|
+
messagesPerWorker,
|
|
178
|
+
qname,
|
|
179
|
+
redisConfig: {
|
|
180
|
+
host: process.env.REDIS_HOST || "127.0.0.1",
|
|
181
|
+
namespace: "stress-test",
|
|
182
|
+
port: process.env.REDIS_PORT || "6379",
|
|
183
|
+
},
|
|
184
|
+
workerIndex: i,
|
|
185
|
+
},
|
|
186
|
+
type: "start",
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
workerPromises.push(workerPromise);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const workerResults = await Promise.all(workerPromises);
|
|
194
|
+
|
|
195
|
+
for (const latencies of workerResults) {
|
|
196
|
+
allLatencies.push(...latencies);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const sendDuration = performance.now() - startTime;
|
|
200
|
+
log("green", `✓ All messages sent in ${sendDuration.toFixed(2)}ms`);
|
|
201
|
+
|
|
202
|
+
log("blue", "\n⏳ Waiting for all messages to be processed...");
|
|
203
|
+
await workerPromise;
|
|
204
|
+
|
|
205
|
+
const duration = (performance.now() - startTime) / 1000;
|
|
206
|
+
const throughput = actualMessageCount / duration;
|
|
207
|
+
|
|
208
|
+
await queue.close();
|
|
209
|
+
|
|
210
|
+
const stats = calculateStats(allLatencies);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
duration,
|
|
214
|
+
throughput,
|
|
215
|
+
totalMessages: actualMessageCount,
|
|
216
|
+
...stats,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function cleanup() {
|
|
221
|
+
log("blue", "\n🧹 Cleaning up test data...");
|
|
222
|
+
|
|
223
|
+
const redis = new Bun.RedisClient(
|
|
224
|
+
`redis://${process.env.REDIS_HOST || "127.0.0.1"}:${process.env.REDIS_PORT || "6379"}`,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
let cursor = "0";
|
|
228
|
+
const pattern = "stress-test:*";
|
|
229
|
+
let deletedCount = 0;
|
|
230
|
+
|
|
231
|
+
do {
|
|
232
|
+
const result = await redis.scan(cursor, "MATCH", pattern);
|
|
233
|
+
const [nextCursor, keys] = result as [string, string[]];
|
|
234
|
+
|
|
235
|
+
cursor = nextCursor;
|
|
236
|
+
|
|
237
|
+
if (keys.length > 0) {
|
|
238
|
+
await redis.del(...keys);
|
|
239
|
+
deletedCount += keys.length;
|
|
240
|
+
}
|
|
241
|
+
} while (cursor !== "0");
|
|
242
|
+
|
|
243
|
+
redis.close();
|
|
244
|
+
|
|
245
|
+
log("green", `✓ Cleaned up ${deletedCount} keys`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function main() {
|
|
249
|
+
log(
|
|
250
|
+
"bright",
|
|
251
|
+
"\n╔═══════════════════════════════════════════════════════════╗",
|
|
252
|
+
);
|
|
253
|
+
log(
|
|
254
|
+
"bright",
|
|
255
|
+
"║ RedisQ Stress Test & Benchmark Tool ║",
|
|
256
|
+
);
|
|
257
|
+
log(
|
|
258
|
+
"bright",
|
|
259
|
+
"╚═══════════════════════════════════════════════════════════╝",
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
await cleanup();
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
const mediumMessage = '{"batch_id":"batch_analytics_2201","source_system":"inventory_management_v2","generated_at":"2023-10-27T10:00:00Z","record_count":5,"data":[{"sku":"KEY-MECH-RGB-001","warehouse_id":"WH-NY-01","stock_level":450,"reserved":12,"incoming":100,"last_audit":"2023-10-25","dimensions":{"w":15,"h":5,"l":40,"unit":"cm"},"tags":["electronics","peripherals","hot_item"]},{"sku":"MON-4K-IPS-002","warehouse_id":"WH-CA-02","stock_level":85,"reserved":5,"incoming":0,"last_audit":"2023-10-26","dimensions":{"w":60,"h":40,"l":10,"unit":"cm"},"tags":["electronics","display","fragile"]},{"sku":"LAP-PRO-M2-003","warehouse_id":"WH-TX-05","stock_level":200,"reserved":45,"incoming":50,"last_audit":"2023-10-20","dimensions":{"w":30,"h":2,"l":20,"unit":"cm"},"tags":["computer","high_value","insured"]},{"sku":"MOU-ERG-WL-004","warehouse_id":"WH-NY-01","stock_level":1200,"reserved":0,"incoming":200,"last_audit":"2023-10-22","dimensions":{"w":8,"h":5,"l":12,"unit":"cm"},"tags":["peripherals","office","sale"]},{"sku":"CAB-HDMI-2M-005","warehouse_id":"WH-WA-03","stock_level":5000,"reserved":120,"incoming":1000,"last_audit":"2023-10-01","dimensions":{"w":10,"h":2,"l":10,"unit":"cm"},"tags":["accessory","cable","generic"]}],"system_logs":{"warnings":[],"errors":[{"code":404,"msg":"SKU-IMG-MISSING for CAB-HDMI-2M-005","timestamp":1678892500}],"performance_metrics":{"query_time_ms":145,"db_reads":25,"cpu_load_percent":12}}}'
|
|
266
|
+
|
|
267
|
+
const availableCPUs = navigator.hardwareConcurrency;
|
|
268
|
+
log("yellow", `\n💻 Detected ${availableCPUs} CPU cores`);
|
|
269
|
+
|
|
270
|
+
log("blue", `\n🔧 Using ${WORKER_COUNT} workers for tests`);
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
// Test 1: Small messages - Maximum throughput
|
|
274
|
+
log("cyan", "\n📋 Test 1: Small messages (100 bytes)");
|
|
275
|
+
const test1Results = await testRegularQueueParallel({
|
|
276
|
+
concurrency: CONCURRENCY,
|
|
277
|
+
messageCount: MSG_COUNT,
|
|
278
|
+
testMessage: '{"id":"msg_82103","type":"notification","timestamp":1678892301,"payload":{"user_id":4092,"event":"login_attempt","status":"success"}}',
|
|
279
|
+
workerCount:WORKER_COUNT,
|
|
280
|
+
});
|
|
281
|
+
printResults(
|
|
282
|
+
"Test 1: Small Messages (50K msgs, 100 bytes)",
|
|
283
|
+
test1Results,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
// Test 2: Medium messages
|
|
288
|
+
log("cyan", "\n📋 Test 2: Medium messages (1KB)");
|
|
289
|
+
const test2Results = await testRegularQueueParallel({
|
|
290
|
+
concurrency: CONCURRENCY,
|
|
291
|
+
messageCount: MSG_COUNT,
|
|
292
|
+
testMessage: mediumMessage,
|
|
293
|
+
workerCount: WORKER_COUNT,
|
|
294
|
+
});
|
|
295
|
+
printResults(
|
|
296
|
+
"Test 2: Medium Messages (50K msgs, 1KB)",
|
|
297
|
+
test2Results,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
// Test 3: Large messages
|
|
302
|
+
log("cyan", "\n📋 Test 3: Large messages (10KB)");
|
|
303
|
+
const test3Results = await testRegularQueueParallel({
|
|
304
|
+
concurrency: CONCURRENCY,
|
|
305
|
+
messageCount: MSG_COUNT,
|
|
306
|
+
testMessage: '[{"_id":"6973b4f29af00ad243d8dfb1","index":0,"guid":"3da3f9d7-2406-4f07-8cc5-20c77a18ade4","isActive":false,"balance":"$1,361.82","picture":"http://placehold.it/32x32","age":37,"eyeColor":"green","name":"Hart Booth","gender":"male","company":"SULTRAX","email":"hartbooth@sultrax.com","phone":"+1 (966) 498-3673","address":"843 Hall Street, Trona, Oregon, 1712","about":"Deserunt fugiat nisi voluptate quis ex nisi reprehenderit est eiusmod officia sunt quis elit ea. Quis non mollit consectetur amet nulla anim ipsum consequat aliqua reprehenderit tempor reprehenderit. Sint pariatur pariatur laboris sint dolor sint voluptate pariatur ut adipisicing officia Lorem deserunt. Ullamco minim duis sit consequat aliqua in sunt nostrud amet nisi deserunt voluptate pariatur. Qui ullamco aliquip sunt veniam occaecat nulla ex incididunt. Aute mollit incididunt ad in dolor culpa pariatur non commodo magna nostrud non.\r\n","registered":"2025-07-22T10:26:43 -07:00","latitude":-3.647469,"longitude":69.437957,"tags":["qui","est","aliqua","ullamco","eiusmod","eu","velit"],"friends":[{"id":0,"name":"Lakisha Turner"},{"id":1,"name":"Leonor Ewing"},{"id":2,"name":"Frazier Irwin"}],"greeting":"Hello, Hart Booth! You have 6 unread messages.","favoriteFruit":"banana"},{"_id":"6973b4f2021b2c498f984bb6","index":1,"guid":"25034ff5-fbc8-4d09-8c63-e60f919e9ad4","isActive":false,"balance":"$2,141.41","picture":"http://placehold.it/32x32","age":33,"eyeColor":"brown","name":"Bethany Davenport","gender":"female","company":"STROZEN","email":"bethanydavenport@strozen.com","phone":"+1 (818) 499-2890","address":"506 Quay Street, Springville, Texas, 5249","about":"Consectetur occaecat sunt consectetur culpa nulla qui sunt labore proident et irure fugiat eu nulla. Voluptate est dolore labore aliqua velit duis. Proident occaecat culpa esse deserunt nulla aliqua. Nulla ea minim esse consectetur fugiat nostrud in anim esse cillum. Quis dolor quis reprehenderit ex ex ad esse minim est.\r\n","registered":"2020-08-03T11:36:56 -07:00","latitude":-10.479417,"longitude":28.341411,"tags":["non","eiusmod","amet","cillum","incididunt","elit","exercitation"],"friends":[{"id":0,"name":"Phyllis Bryan"},{"id":1,"name":"Mallory Gill"},{"id":2,"name":"Schneider Mercado"}],"greeting":"Hello, Bethany Davenport! You have 9 unread messages.","favoriteFruit":"apple"},{"_id":"6973b4f2220a607ed78df07a","index":2,"guid":"871776e1-d133-4ae7-8343-4fca929bb75c","isActive":true,"balance":"$2,630.76","picture":"http://placehold.it/32x32","age":35,"eyeColor":"brown","name":"Bertie Peters","gender":"female","company":"PERKLE","email":"bertiepeters@perkle.com","phone":"+1 (841) 517-3179","address":"972 Nassau Street, Ventress, Wyoming, 585","about":"Fugiat reprehenderit excepteur deserunt magna laboris aute culpa. Voluptate amet nisi commodo nostrud ipsum do nisi consectetur sint deserunt qui sint proident. Consequat cupidatat deserunt aliquip aliqua occaecat velit ad nostrud deserunt qui. Sunt amet sunt voluptate reprehenderit nisi eiusmod deserunt ea cillum Lorem qui anim aute duis.\r\n","registered":"2025-08-27T03:50:35 -07:00","latitude":32.284355,"longitude":-78.324272,"tags":["consequat","id","voluptate","tempor","proident","non","occaecat"],"friends":[{"id":0,"name":"Foreman Quinn"},{"id":1,"name":"Rojas Kemp"},{"id":2,"name":"Hanson Buck"}],"greeting":"Hello, Bertie Peters! You have 3 unread messages.","favoriteFruit":"banana"},{"_id":"6973b4f2bd348f1a31aca9f8","index":3,"guid":"fc19928a-b4f9-441e-ade9-efd7bad779af","isActive":false,"balance":"$1,273.51","picture":"http://placehold.it/32x32","age":40,"eyeColor":"blue","name":"Rosales Sims","gender":"male","company":"CANOPOLY","email":"rosalessims@canopoly.com","phone":"+1 (869) 427-3088","address":"153 Guernsey Street, Ona, New Jersey, 183","about":"Laborum sit sunt ad deserunt velit nisi culpa pariatur est ea mollit. Velit excepteur nulla cupidatat reprehenderit laboris sint amet duis exercitation dolore voluptate commodo sit Lorem. Velit exercitation ea incididunt adipisicing voluptate occaecat.\r\n","registered":"2017-11-04T12:50:51 -07:00","latitude":0.771702,"longitude":82.991386,"tags":["cillum","qui","sint","consectetur","eiusmod","consequat","ex"],"friends":[{"id":0,"name":"Imogene Lamb"},{"id":1,"name":"Key Burris"},{"id":2,"name":"Reyna Mayer"}],"greeting":"Hello, Rosales Sims! You have 7 unread messages.","favoriteFruit":"apple"},{"_id":"6973b4f207f65e544265d355","index":4,"guid":"abc15ba6-3214-4ef8-ac30-8afb642b82c2","isActive":true,"balance":"$2,945.08","picture":"http://placehold.it/32x32","age":26,"eyeColor":"brown","name":"Gretchen Jordan","gender":"female","company":"ANOCHA","email":"gretchenjordan@anocha.com","phone":"+1 (948) 454-2346","address":"890 Monaco Place, Loomis, Alabama, 4341","about":"Nulla excepteur est consequat velit aute laborum sunt voluptate aute eu eiusmod. Anim sunt eu veniam in commodo sit eu. Anim commodo incididunt ad reprehenderit anim culpa sit ut. Laborum esse in pariatur velit. Ut elit adipisicing magna ut officia non magna exercitation incididunt enim esse magna ad veniam. Deserunt irure ut dolor dolore labore pariatur veniam ipsum voluptate sint enim dolor. Ad exercitation sint sit occaecat excepteur eiusmod et sit.\r\n","registered":"2022-03-30T04:45:03 -07:00","latitude":29.089418,"longitude":-41.417872,"tags":["minim","amet","aliqua","excepteur","irure","et","culpa"],"friends":[{"id":0,"name":"Sonia Hayden"},{"id":1,"name":"Levy Burks"},{"id":2,"name":"Wolfe Hancock"}],"greeting":"Hello, Gretchen Jordan! You have 5 unread messages.","favoriteFruit":"banana"},{"_id":"6973b4f250d546997251a3d3","index":5,"guid":"ec6af806-cb4c-4ecc-b208-68b4b3cafd93","isActive":false,"balance":"$2,535.09","picture":"http://placehold.it/32x32","age":33,"eyeColor":"brown","name":"Ruiz Tran","gender":"male","company":"AQUASURE","email":"ruiztran@aquasure.com","phone":"+1 (962) 478-3889","address":"705 Farragut Road, Broadlands, Michigan, 1955","about":"Sunt enim Lorem excepteur sint mollit deserunt. Aute commodo amet sit cillum voluptate mollit exercitation ad cillum aute fugiat mollit. Quis ad cupidatat veniam reprehenderit aute cupidatat quis. Minim laboris est fugiat et in cillum id nisi dolore reprehenderit fugiat minim. Nulla ut consequat do sit dolor laboris.\r\n","registered":"2018-09-27T01:51:44 -07:00","latitude":-59.151306,"longitude":143.667188,"tags":["laboris","anim","eiusmod","quis","nulla","aliqua","duis"],"friends":[{"id":0,"name":"Ramona Brock"},{"id":1,"name":"Kayla Webb"},{"id":2,"name":"Rutledge Mcclure"}],"greeting":"Hello, Ruiz Tran! You have 6 unread messages.","favoriteFruit":"apple"},{"_id":"6973b4f20e1b602ed3098386","index":6,"guid":"d63efc08-287a-487e-a159-bbb4f231c37a","isActive":false,"balance":"$3,941.09","picture":"http://placehold.it/32x32","age":37,"eyeColor":"blue","name":"Reba Oneal","gender":"female","company":"NIQUENT","email":"rebaoneal@niquent.com","phone":"+1 (994) 443-2343","address":"229 Clifton Place, Cochranville, Pennsylvania, 6468","about":"Officia veniam do minim elit duis in nulla. Deserunt veniam in ut esse duis nulla cillum magna ullamco dolore veniam exercitation Lorem. Eiusmod pariatur irure dolor aute dolore minim adipisicing sint ullamco. Ipsum id voluptate est ipsum adipisicing est. Do anim exercitation commodo mollit fugiat minim est ea.\r\n","registered":"2020-05-02T09:23:38 -07:00","latitude":-45.360857,"longitude":-17.113491,"tags":["laborum","dolor","incididunt","elit","adipisicing","adipisicing","veniam"],"friends":[{"id":0,"name":"Janine Russell"},{"id":1,"name":"Abby Short"},{"id":2,"name":"Savannah Grant"}],"greeting":"Hello, Reba Oneal! You have 7 unread messages.","favoriteFruit":"banana"},{"_id":"6973b4f208d0cb4b5c53bb93","index":7,"guid":"152d21f2-05cd-41d2-a048-ba499944b2bf","isActive":false,"balance":"$1,155.29","picture":"http://placehold.it/32x32","age":25,"eyeColor":"green","name":"Vanessa Rodriquez","gender":"female","company":"OPTYK","email":"vanessarodriquez@optyk.com","phone":"+1 (873) 492-2951","address":"102 Thatford Avenue, Keyport, Minnesota, 7404","about":"Adipisicing sit dolor reprehenderit Lorem et voluptate culpa ullamco cillum officia dolor culpa et sit. Lorem minim eu laboris nulla do est labore nulla eu occaecat. Est occaecat sit id irure. Enim sint consequat amet mollit occaecat in mollit duis. Eiusmod et tempor laborum est tempor fugiat anim ullamco.\r\n","registered":"2019-11-03T12:06:46 -07:00","latitude":30.194412,"longitude":155.798353,"tags":["aliqua","cupidatat","quis","eiusmod","ad","veniam","velit"],"friends":[{"id":0,"name":"June Little"},{"id":1,"name":"Lamb Marsh"},{"id":2,"name":"Richards Conley"}],"greeting":"Hello, Vanessa Rodriquez! You have 5 unread messages.","favoriteFruit":"banana"},{"_id":"6973b4f244d84ce83babb8e8","index":8,"guid":"2d743ccf-cffe-46ac-821a-1a32180f8401","isActive":true,"balance":"$1,531.94","picture":"http://placehold.it/32x32","age":33,"eyeColor":"brown","name":"Kathy Warren","gender":"female","company":"NURPLEX","email":"kathywarren@nurplex.com","phone":"+1 (958) 514-2617","address":"211 Cypress Court, Rosewood, Massachusetts, 2530","about":"Cillum nisi ad esse aliqua do amet eu dolor. Culpa culpa adipisicing officia nisi magna occaecat aliquip labore nostrud anim. Eu esse in aliquip exercitation reprehenderit cupidatat eu veniam deserunt cillum excepteur id. Id dolor reprehenderit do deserunt. Enim eiusmod nostrud exercitation fugiat consequat. Nulla deserunt cillum mollit excepteur ea ad veniam. Id nostrud laboris ipsum cupidatat ex non commodo ad sunt.\r\n","registered":"2017-06-14T06:01:20 -07:00","latitude":34.733711,"longitude":-163.804679,"tags":["reprehenderit","elit","exercitation","voluptate","aute","adipisicing","occaecat"],"friends":[{"id":0,"name":"Allie Holcomb"},{"id":1,"name":"Muriel Weaver"},{"id":2,"name":"Tommie Carr"}],"greeting":"Hello, Kathy Warren! You have 4 unread messages.","favoriteFruit":"apple"},{"_id":"6973b4f297dc750e1750eb58","index":9,"guid":"1ccffd92-ff08-429d-9232-e86c565e70da","isActive":true,"balance":"$1,662.22","picture":"http://placehold.it/32x32","age":31,"eyeColor":"green","name":"Hodges Richardson","gender":"male","company":"POSHOME","email":"hodgesrichardson@poshome.com","phone":"+1 (970) 590-2870","address":"686 Anna Court, Elizaville, Marshall Islands, 5762","about":"Ullamco cupidatat nisi do ut nostrud est pariatur labore dolore exercitation quis minim ipsum quis. Aute enim excepteur voluptate ullamco laboris adipisicing commodo. Do amet sunt occaecat id commodo. Consectetur veniam excepteur nisi in commodo. Laborum anim dolor velit deserunt culpa. Exercitation do amet laborum do ipsum voluptate commodo. Aliqua exercitation consequat nisi pariatur ea veniam officia nostrud excepteur tempor.\r\n","registered":"2016-07-28T02:54:58 -07:00","latitude":-14.492817,"longitude":134.965697,"tags":["qui","aliquip","pariatur","magna","ad","et","irure"],"friends":[{"id":0,"name":"Robyn Willis"},{"id":1,"name":"Gabrielle Holt"},{"id":2,"name":"Wilkerson Bailey"}],"greeting":"Hello, Hodges Richardson! You have 5 unread messages.","favoriteFruit":"strawberry"},{"_id":"6973b4f277b5d5924e85e34c","index":10,"guid":"24b6f2d6-f153-4826-9c2f-f50c42d907e5","isActive":true,"balance":"$1,715.09","picture":"http://placehold.it/32x32","age":33,"eyeColor":"blue","name":"Floyd Petty","gender":"male","company":"GINKOGENE","email":"floydpetty@ginkogene.com","phone":"+1 (832) 499-3944","address":"910 Canal Avenue, Mammoth, North Dakota, 1708","about":"Eiusmod velit proident sit proident ipsum irure. Excepteur dolore consequat labore duis tempor nulla laborum do amet labore. Amet nisi ullamco ad enim officia id exercitation aliquip in fugiat in. Anim quis et enim proident dolore magna mollit consectetur ea elit velit nostrud. Consectetur ea labore esse esse est nisi consectetur magna ut ullamco nulla ipsum exercitation.\r\n","registered":"2023-09-18T01:14:07 -07:00","latitude":-51.559233,"longitude":134.694377,"tags":["cupidatat","elit","commodo","ullamco","do","reprehenderit","nostrud"],"friends":[{"id":0,"name":"Naomi Garrison"},{"id":1,"name":"Betty English"},{"id":2,"name":"Prince Singleton"}],"greeting":"Hello, Floyd Petty! You have 7 unread messages.","favoriteFruit":"strawberry"},{"_id":"6973b4f2e7ad4e31bb2510c3","index":11,"guid":"74ccb6d9-7f24-4451-a3a0-e632bab385b3","isActive":true,"balance":"$1,490.53","picture":"http://placehold.it/32x32","age":36,"eyeColor":"blue","name":"Johnnie Simon","gender":"female","company":"JAMNATION","email":"johnniesimon@jamnation.com","phone":"+1 (868) 417-3009","address":"125 Chester Street, Whipholt, Maryland, 9466","about":"Dolor anim esse enim Lorem nostrud id officia incididunt amet laboris laboris sunt. Pariatur dolor aute Lorem aute magna. Laborum enim non voluptate commodo.\r\n","registered":"2015-03-03T08:28:39 -07:00","latitude":-19.882734,"longitude":-45.527604,"tags":["qui","sit","culpa","id","dolor","irure","id"],"friends":[{"id":0,"name":"Fry Ayers"},{"id":1,"name":"Donovan Foley"},{"id":2,"name":"Finch Dunn"}],"greeting":"Hello, Johnnie Simon! You have 9 unread messages.","favoriteFruit":"apple"},{"_id":"6973b4f24d0d75c705a54911","index":12,"guid":"f972a1ef-0d3a-41ab-b080-eaf7d26184f1","isActive":true,"balance":"$3,429.83","picture":"http://placehold.it/32x32","age":36,"eyeColor":"brown","name":"Rosalind Duran","gender":"female","company":"ZORROMOP","email":"rosalindduran@zorromop.com","phone":"+1 (987) 589-3940","address":"720 Mill Avenue, Gardiner, Nebraska, 6579","about":"Dolor deserunt nisi laborum eiusmod aliquip adipisicing ullamco eiusmod non qui consectetur est nostrud. Incididunt nostrud labore minim Lorem elit qui commodo consectetur ex culpa veniam. Quis ad quis cillum esse nisi ea ipsum consequat dolore. Non minim minim sint excepteur anim consectetur est voluptate laborum commodo. Mollit ea magna dolor sit ut magna voluptate ipsum reprehenderit id ex cillum deserunt.\r\n","registered":"2014-07-03T10:56:30 -07:00","latitude":-39.091253,"longitude":-131.671201,"tags":["labore","enim","nulla","est","exercitation","ipsum","deserunt"],"friends":[{"id":0,"name":"Emerson Dalton"},{"id":1,"name":"Owen Bass"},{"id":2,"name":"Bell Shannon"}],"greeting":"Hello, Rosalind Duran! You have 1 unread messages.","favoriteFruit":"banana"},{"_id":"6973b4f2edea35a5d2cea142","index":13,"guid":"18cf8f91-9735-4fa6-9381-ae8c1060890c","isActive":true,"balance":"$1,811.69","picture":"http://placehold.it/32x32","age":26,"eyeColor":"blue","name":"Pauline Cain","gender":"female","company":"ENAUT","email":"paulinecain@enaut.com","phone":"+1 (972) 408-3809","address":"508 Rapelye Street, Thynedale, Maine, 9391","about":"Qui enim fugiat do eu in eu aliqua elit cillum occaecat. Consectetur amet pariatur ea dolore Lorem eu ipsum veniam commodo commodo. Mollit dolore velit aliqua cillum elit consequat sunt nostrud in qui ipsum sit eu irure. Cupidatat reprehenderit labore in officia laborum.\r\n","registered":"2023-12-23T01:34:49 -07:00","latitude":-18.152332,"longitude":6.220963,"tags":["quis","sint","proident","officia","dolore","elit","quis"],"friends":[{"id":0,"name":"Kristie Shepherd"},{"id":1,"name":"Jensen Webster"},{"id":2,"name":"Langley Young"}],"greeting":"Hello, Pauline Cain! You have 7 unread messages.","favoriteFruit":"apple"}]',
|
|
307
|
+
workerCount: WORKER_COUNT,
|
|
308
|
+
});
|
|
309
|
+
printResults(
|
|
310
|
+
"Test 3: Large Messages (50K msgs, 10KB)",
|
|
311
|
+
test3Results,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
await cleanup();
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
// Summary
|
|
319
|
+
log("bright", `\n${"=".repeat(60)}`);
|
|
320
|
+
log("magenta", " 📈 BENCHMARK SUMMARY");
|
|
321
|
+
log("bright", "=".repeat(60));
|
|
322
|
+
|
|
323
|
+
console.log("\n Regular Queue Performance:");
|
|
324
|
+
console.log(
|
|
325
|
+
` - Small messages (100B): ${test1Results.throughput.toFixed(0)} msg/s (p50: ${test1Results.p50.toFixed(2)}ms)`,
|
|
326
|
+
);
|
|
327
|
+
console.log(
|
|
328
|
+
` - Medium messages (1KB): ${test2Results.throughput.toFixed(0)} msg/s (p50: ${test2Results.p50.toFixed(2)}ms)`,
|
|
329
|
+
);
|
|
330
|
+
console.log(
|
|
331
|
+
` - Large messages (100KB): ${test3Results.throughput.toFixed(0)} msg/s (p50: ${test3Results.p50.toFixed(2)}ms)`,
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
log("bright", "\n" + "=".repeat(60));
|
|
335
|
+
|
|
336
|
+
log("green", "\n✅ All stress tests completed successfully!");
|
|
337
|
+
|
|
338
|
+
log(
|
|
339
|
+
"yellow",
|
|
340
|
+
"\n💡 To update README.md Performance section, use these values:",
|
|
341
|
+
);
|
|
342
|
+
log(
|
|
343
|
+
"cyan",
|
|
344
|
+
` - Throughput: ~${Math.round(test1Results.throughput).toLocaleString()} messages/second`,
|
|
345
|
+
);
|
|
346
|
+
log("cyan", ` - Latency (p50): ${test1Results.p50.toFixed(2)} ms`);
|
|
347
|
+
log("cyan", ` - Latency (p95): ${test1Results.p95.toFixed(2)} ms`);
|
|
348
|
+
log("cyan", ` - Latency (p99): ${test1Results.p99.toFixed(2)} ms`);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
log(
|
|
351
|
+
"red",
|
|
352
|
+
`\n❌ Error during stress test: ${(error as Error).message}`,
|
|
353
|
+
);
|
|
354
|
+
log("red", (error as Error).stack || "");
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
main();
|
package/biome.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
3
|
+
"assist": {
|
|
4
|
+
"actions": {
|
|
5
|
+
"source": {
|
|
6
|
+
"organizeImports": "on",
|
|
7
|
+
"useSortedKeys": "on"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"css": {
|
|
12
|
+
"parser": {
|
|
13
|
+
"tailwindDirectives": true
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": {
|
|
17
|
+
"ignoreUnknown": false,
|
|
18
|
+
"includes": [
|
|
19
|
+
"./**",
|
|
20
|
+
"!**/build",
|
|
21
|
+
"!benchmark",
|
|
22
|
+
"!**/node_modules",
|
|
23
|
+
"!bun.lock",
|
|
24
|
+
"!bun.lockd"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"formatter": {
|
|
28
|
+
"attributePosition": "auto",
|
|
29
|
+
"enabled": true,
|
|
30
|
+
"indentStyle": "space",
|
|
31
|
+
"indentWidth": 4
|
|
32
|
+
},
|
|
33
|
+
"html": {
|
|
34
|
+
"experimentalFullSupportEnabled": true
|
|
35
|
+
},
|
|
36
|
+
"javascript": {
|
|
37
|
+
"formatter": {
|
|
38
|
+
"quoteStyle": "double",
|
|
39
|
+
"semicolons": "always",
|
|
40
|
+
"trailingCommas": "all"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"json": {
|
|
44
|
+
"formatter": {
|
|
45
|
+
"trailingCommas": "none"
|
|
46
|
+
},
|
|
47
|
+
"parser": {
|
|
48
|
+
"allowTrailingCommas": true
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"linter": {
|
|
52
|
+
"enabled": true,
|
|
53
|
+
"rules": {
|
|
54
|
+
"a11y": {},
|
|
55
|
+
"complexity": {},
|
|
56
|
+
"recommended": true,
|
|
57
|
+
"style": {
|
|
58
|
+
"noInferrableTypes": "error",
|
|
59
|
+
"noParameterAssign": "error",
|
|
60
|
+
"noUnusedTemplateLiteral": "error",
|
|
61
|
+
"noUselessElse": "error",
|
|
62
|
+
"useAsConstAssertion": "error",
|
|
63
|
+
"useDefaultParameterLast": "error",
|
|
64
|
+
"useEnumInitializers": "error",
|
|
65
|
+
"useNamingConvention": "off",
|
|
66
|
+
"useNumberNamespace": "error",
|
|
67
|
+
"useSelfClosingElements": "error",
|
|
68
|
+
"useSingleVarDeclarator": "error"
|
|
69
|
+
},
|
|
70
|
+
"suspicious": {
|
|
71
|
+
"noUnsafeDeclarationMerging": "off"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"vcs": {
|
|
76
|
+
"clientKind": "git",
|
|
77
|
+
"enabled": true,
|
|
78
|
+
"useIgnoreFile": false
|
|
79
|
+
}
|
|
80
|
+
}
|
package/compose.yml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: redisq
|
|
2
|
+
services:
|
|
3
|
+
redis:
|
|
4
|
+
image: "redis:alpine"
|
|
5
|
+
restart: always
|
|
6
|
+
expose:
|
|
7
|
+
- 6379
|
|
8
|
+
volumes:
|
|
9
|
+
- "redisvol:/data"
|
|
10
|
+
- ./redis.conf:/etc/sysctl.d/99-redis.conf
|
|
11
|
+
networks:
|
|
12
|
+
- redis-net
|
|
13
|
+
ports:
|
|
14
|
+
- 6379:6379
|
|
15
|
+
volumes:
|
|
16
|
+
redisvol:
|
|
17
|
+
driver: local
|
|
18
|
+
networks:
|
|
19
|
+
redis-net:
|
|
20
|
+
driver: bridge
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"author": "PingPolls",
|
|
3
|
+
"dependencies": {
|
|
4
|
+
"croner": "^9.1.0"
|
|
5
|
+
},
|
|
6
|
+
"description": "RedisQ is simple message queue that utilizes Bun runtime for high performance read/writes.",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@biomejs/biome": "https://pkg.pr.new/@biomejs/biome@0196c0e",
|
|
9
|
+
"@types/bun": "^1.3.6"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"redis",
|
|
13
|
+
"queue",
|
|
14
|
+
"message-queue",
|
|
15
|
+
"rsmq",
|
|
16
|
+
"bun"
|
|
17
|
+
],
|
|
18
|
+
"license": "Apache License 2.0",
|
|
19
|
+
"module": "app.ts",
|
|
20
|
+
"name": "@pingpolls/redisq",
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"typescript": "^5"
|
|
23
|
+
},
|
|
24
|
+
"private": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"check": "bun test --timeout 10000",
|
|
27
|
+
"stress": "bun benchmark/stress.ts"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"version": "0.1.0"
|
|
31
|
+
}
|
package/redis.conf
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vm.overcommit_memory=1
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false
|
|
28
|
+
}
|
|
29
|
+
}
|