queasy 0.2.0 → 0.3.1
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/.github/workflows/check.yml +3 -0
- package/.github/workflows/publish.yml +3 -0
- package/CLAUDE.md +5 -4
- package/Readme.md +9 -4
- package/biome.json +5 -1
- package/dist/client.d.ts +33 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +199 -0
- package/dist/client.js.map +1 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -0
- package/{src → dist}/constants.js +2 -10
- package/dist/constants.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/{src → dist}/errors.js +1 -13
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/manager.d.ts +19 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +67 -0
- package/dist/manager.js.map +1 -0
- package/dist/pool.d.ts +29 -0
- package/dist/pool.d.ts.map +1 -0
- package/{src → dist}/pool.js +23 -82
- package/dist/pool.js.map +1 -0
- package/dist/queasy.lua +390 -0
- package/dist/queue.d.ts +22 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +81 -0
- package/dist/queue.js.map +1 -0
- package/dist/types.d.ts +92 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +24 -0
- package/dist/utils.js.map +1 -0
- package/dist/worker.d.ts +2 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +42 -0
- package/dist/worker.js.map +1 -0
- package/docker-compose.yml +0 -2
- package/fuzztest/Readme.md +185 -0
- package/fuzztest/fuzz.ts +356 -0
- package/fuzztest/handlers/cascade-a.ts +90 -0
- package/fuzztest/handlers/cascade-b.ts +71 -0
- package/fuzztest/handlers/fail-handler.ts +47 -0
- package/fuzztest/handlers/periodic.ts +89 -0
- package/fuzztest/process.ts +100 -0
- package/fuzztest/shared/chaos.ts +29 -0
- package/fuzztest/shared/stream.ts +40 -0
- package/package.json +8 -7
- package/plans/redis-options.md +279 -0
- package/src/client.ts +246 -0
- package/src/constants.ts +33 -0
- package/src/errors.ts +13 -0
- package/src/index.ts +2 -0
- package/src/manager.ts +78 -0
- package/src/pool.ts +129 -0
- package/src/queasy.lua +2 -3
- package/src/queue.ts +108 -0
- package/src/types.ts +16 -0
- package/src/{utils.js → utils.ts} +3 -20
- package/src/{worker.js → worker.ts} +5 -12
- package/test/{client.test.js → client.test.ts} +6 -7
- package/test/{errors.test.js → errors.test.ts} +1 -1
- package/test/fixtures/always-fail-handler.ts +5 -0
- package/test/fixtures/data-logger-handler.ts +11 -0
- package/test/fixtures/failure-handler.ts +6 -0
- package/test/fixtures/permanent-error-handler.ts +6 -0
- package/test/fixtures/slow-handler.ts +6 -0
- package/test/fixtures/success-handler.js +0 -5
- package/test/fixtures/success-handler.ts +6 -0
- package/test/fixtures/with-failure-handler.ts +5 -0
- package/test/{guards.test.js → guards.test.ts} +21 -34
- package/test/{manager.test.js → manager.test.ts} +26 -34
- package/test/{pool.test.js → pool.test.ts} +14 -16
- package/test/{queue.test.js → queue.test.ts} +21 -21
- package/test/{redis-functions.test.js → redis-functions.test.ts} +14 -20
- package/test/{utils.test.js → utils.test.ts} +1 -1
- package/tsconfig.json +20 -0
- package/jsconfig.json +0 -17
- package/src/client.js +0 -258
- package/src/index.js +0 -2
- package/src/manager.js +0 -94
- package/src/queue.js +0 -154
- package/test/fixtures/always-fail-handler.js +0 -8
- package/test/fixtures/data-logger-handler.js +0 -19
- package/test/fixtures/failure-handler.js +0 -9
- package/test/fixtures/permanent-error-handler.js +0 -10
- package/test/fixtures/slow-handler.js +0 -9
- package/test/fixtures/with-failure-handler.js +0 -8
- /package/test/fixtures/{no-handle-handler.js → no-handle-handler.ts} +0 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { RedisClientOptions, RedisClusterOptions } from 'redis';
|
|
2
|
+
type SingleNodeOptions = Pick<RedisClientOptions, 'url' | 'socket' | 'username' | 'password' | 'database'>;
|
|
3
|
+
export type RedisOptions = SingleNodeOptions | {
|
|
4
|
+
rootNodes: SingleNodeOptions[];
|
|
5
|
+
defaults?: Partial<SingleNodeOptions>;
|
|
6
|
+
nodeAddressMap?: RedisClusterOptions['nodeAddressMap'];
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Core job identification and data
|
|
10
|
+
*/
|
|
11
|
+
export interface JobCoreOptions {
|
|
12
|
+
/** Job ID (auto-generated if not provided) */
|
|
13
|
+
id?: string;
|
|
14
|
+
/** Job data (any JSON-serializable value) */
|
|
15
|
+
data?: any;
|
|
16
|
+
/** Wall clock timestamp (ms) before which job must not run */
|
|
17
|
+
runAt?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Update behavior flags
|
|
21
|
+
*/
|
|
22
|
+
export interface JobUpdateOptions {
|
|
23
|
+
/** Whether to replace data of waiting job with same ID */
|
|
24
|
+
updateData?: boolean;
|
|
25
|
+
/** How to update runAt */
|
|
26
|
+
updateRunAt?: boolean | 'if_later' | 'if_earlier';
|
|
27
|
+
/** Whether to reset retry_count and stall_count to 0 */
|
|
28
|
+
resetCounts?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Complete options accepted by dispatch()
|
|
32
|
+
*/
|
|
33
|
+
export type JobOptions = JobCoreOptions & JobUpdateOptions;
|
|
34
|
+
/**
|
|
35
|
+
* Job runtime state
|
|
36
|
+
*/
|
|
37
|
+
export interface JobState {
|
|
38
|
+
/** Number of times this job has been retried */
|
|
39
|
+
retryCount: number;
|
|
40
|
+
/** Number of times this job has stalled */
|
|
41
|
+
stallCount: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Complete job representation passed to handlers
|
|
45
|
+
*/
|
|
46
|
+
export type Job = Required<JobCoreOptions> & JobState;
|
|
47
|
+
/**
|
|
48
|
+
* Handler options
|
|
49
|
+
*/
|
|
50
|
+
export interface HandlerOptions {
|
|
51
|
+
/** Maximum number of retries before permanent failure */
|
|
52
|
+
maxRetries?: number;
|
|
53
|
+
/** Maximum number of stalls before permanent failure */
|
|
54
|
+
maxStalls?: number;
|
|
55
|
+
/** Minimum backoff in milliseconds */
|
|
56
|
+
minBackoff?: number;
|
|
57
|
+
/** Maximum backoff in milliseconds */
|
|
58
|
+
maxBackoff?: number;
|
|
59
|
+
/** Size of the job (as a percent of total worker capacity) */
|
|
60
|
+
size?: number;
|
|
61
|
+
/** Maximum processing duration before considering stalled */
|
|
62
|
+
timeout?: number;
|
|
63
|
+
/** Priority of this queue (vs other queues) */
|
|
64
|
+
priority?: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Options for listen() - queue-level retry strategy
|
|
68
|
+
*/
|
|
69
|
+
export interface ListenOptions extends HandlerOptions {
|
|
70
|
+
/** Path to failure handler module (optional) */
|
|
71
|
+
failHandler?: string;
|
|
72
|
+
/** Retry options of the fail job */
|
|
73
|
+
failRetryOptions?: HandlerOptions;
|
|
74
|
+
}
|
|
75
|
+
export type ExecMessage = {
|
|
76
|
+
op: 'exec';
|
|
77
|
+
queue: string;
|
|
78
|
+
handlerPath: string;
|
|
79
|
+
job: Job;
|
|
80
|
+
};
|
|
81
|
+
export type DoneMessage = {
|
|
82
|
+
op: 'done';
|
|
83
|
+
jobId: string;
|
|
84
|
+
error?: {
|
|
85
|
+
name: string;
|
|
86
|
+
message: string;
|
|
87
|
+
retryAt?: number;
|
|
88
|
+
kind?: 'retriable' | 'permanent' | 'stall';
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
export {};
|
|
92
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAErE,KAAK,iBAAiB,GAAG,IAAI,CACzB,kBAAkB,EAClB,KAAK,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,UAAU,CAC1D,CAAC;AAEF,MAAM,MAAM,YAAY,GAClB,iBAAiB,GACjB;IACI,SAAS,EAAE,iBAAiB,EAAE,CAAC;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACtC,cAAc,CAAC,EAAE,mBAAmB,CAAC,gBAAgB,CAAC,CAAC;CAC1D,CAAC;AAER;;GAEG;AACH,MAAM,WAAW,cAAc;IAC3B,8CAA8C;IAC9C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,6CAA6C;IAE7C,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC7B,0DAA0D;IAC1D,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,0BAA0B;IAC1B,WAAW,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,YAAY,CAAC;IAClD,wDAAwD;IACxD,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,cAAc,GAAG,gBAAgB,CAAC;AAE3D;;GAEG;AACH,MAAM,WAAW,QAAQ;IACrB,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,MAAM,GAAG,GAAG,QAAQ,CAAC,cAAc,CAAC,GAAG,QAAQ,CAAC;AAEtD;;GAEG;AACH,MAAM,WAAW,cAAc;IAC3B,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,cAAc;IACjD,gDAAgD;IAChD,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,oCAAoC;IACpC,gBAAgB,CAAC,EAAE,cAAc,CAAC;CACrC;AAED,MAAM,MAAM,WAAW,GAAG;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,GAAG,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QACJ,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,WAAW,GAAG,WAAW,GAAG,OAAO,CAAC;KAC9C,CAAC;CACL,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,UAAU,CAAC,MAAM,SAAK,GAAG,MAAM,CAO9C;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,EAAE,CAIzE;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAMlE"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function generateId(length = 20) {
|
|
2
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
3
|
+
let id = '';
|
|
4
|
+
for (let i = 0; i < length; i++) {
|
|
5
|
+
id += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
6
|
+
}
|
|
7
|
+
return id;
|
|
8
|
+
}
|
|
9
|
+
export function parseVersion(version) {
|
|
10
|
+
const parsed = String(version).split('.').map(Number);
|
|
11
|
+
if (parsed.some((n) => Number.isNaN(n)))
|
|
12
|
+
return [0];
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
export function compareSemver(a, b) {
|
|
16
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
17
|
+
if (a[i] !== b[i])
|
|
18
|
+
return a[i] < b[i] ? -1 : 1;
|
|
19
|
+
}
|
|
20
|
+
if (a.length !== b.length)
|
|
21
|
+
return a.length < b.length ? -1 : 1;
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,UAAU,CAAC,MAAM,GAAG,EAAE;IAClC,MAAM,KAAK,GAAG,gEAAgE,CAAC;IAC/E,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC9B,EAAE,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,EAAE,CAAC;AACd,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,OAAkC;IAC3D,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACtD,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACpD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,CAAW,EAAE,CAAW;IAClD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACpD,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACnD,CAAC;IACD,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/D,OAAO,CAAC,CAAC;AACb,CAAC"}
|
package/dist/worker.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":""}
|
package/dist/worker.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { pathToFileURL } from 'node:url';
|
|
10
|
+
import { parentPort, setEnvironmentData } from 'node:worker_threads';
|
|
11
|
+
import { PermanentError } from "./errors.js";
|
|
12
|
+
if (!parentPort)
|
|
13
|
+
throw new Error('Worker cannot be executed directly.');
|
|
14
|
+
setEnvironmentData('queasy_worker_context', true);
|
|
15
|
+
parentPort.on('message', async (msg) => {
|
|
16
|
+
const { handlerPath, job } = msg;
|
|
17
|
+
try {
|
|
18
|
+
const mod = await import(__rewriteRelativeImportExtension(pathToFileURL(handlerPath).href));
|
|
19
|
+
if (typeof mod.handle !== 'function') {
|
|
20
|
+
throw new Error(`Unable to load handler ${handlerPath}`);
|
|
21
|
+
}
|
|
22
|
+
await mod.handle(job.data, job);
|
|
23
|
+
send({ op: 'done', jobId: job.id });
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
const { message, name, retryAt } = err;
|
|
27
|
+
send({
|
|
28
|
+
op: 'done',
|
|
29
|
+
jobId: job.id,
|
|
30
|
+
error: {
|
|
31
|
+
name,
|
|
32
|
+
message,
|
|
33
|
+
retryAt,
|
|
34
|
+
kind: err instanceof PermanentError ? 'permanent' : 'retriable',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
function send(message) {
|
|
40
|
+
parentPort?.postMessage(message);
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":";;;;;;;;AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG7C,IAAI,CAAC,UAAU;IAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;AACxE,kBAAkB,CAAC,uBAAuB,EAAE,IAAI,CAAC,CAAC;AAElD,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,EAAE,GAAgB,EAAE,EAAE;IAChD,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC;IACjC,IAAI,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,MAAM,kCAAC,aAAa,CAAC,WAAW,CAAC,CAAC,IAAI,EAAC,CAAC;QAC1D,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,0BAA0B,WAAW,EAAE,CAAC,CAAC;QAC7D,CAAC;QAED,MAAM,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAChC,IAAI,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACX,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,GAAmC,CAAC;QAEvE,IAAI,CAAC;YACD,EAAE,EAAE,MAAM;YACV,KAAK,EAAE,GAAG,CAAC,EAAE;YACb,KAAK,EAAE;gBACH,IAAI;gBACJ,OAAO;gBACP,OAAO;gBACP,IAAI,EAAE,GAAG,YAAY,cAAc,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW;aAClE;SACJ,CAAC,CAAC;IACP,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,SAAS,IAAI,CAAC,OAAoB;IAC9B,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;AACrC,CAAC"}
|
package/docker-compose.yml
CHANGED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Queasy Fuzz Test Plan
|
|
2
|
+
|
|
3
|
+
A long-running end-to-end fuzz test that simulates random failures and continuously verifies core system invariants.
|
|
4
|
+
|
|
5
|
+
## Invariants Verified
|
|
6
|
+
|
|
7
|
+
1. **Mutual exclusion**: Two jobs with the same Job ID are never processed by different clients or worker threads simultaneously.
|
|
8
|
+
2. **No re-processing of successful jobs**: A job that has succeeded is never processed again.
|
|
9
|
+
3. **Scheduling**: No job is processed before its `run_at` time.
|
|
10
|
+
4. **Priority ordering within a queue**: No job starts processing while another job in the same queue with a lower `run_at` is still waiting (i.e., eligible jobs are dequeued in order).
|
|
11
|
+
5. **Fail handler completeness**: If a fail handler is registered, every job that does not eventually succeed MUST result in the fail handler being invoked.
|
|
12
|
+
6. **Queue progress (priority starvation prevention)**: Non-empty queues at the highest priority level always make progress. When they drain, queues at the next priority level begin making progress.
|
|
13
|
+
|
|
14
|
+
## Structure Overview
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
fuzztest/
|
|
18
|
+
Readme.md # This file
|
|
19
|
+
fuzz.js # Orchestrator: spawns child processes, monitors shared state
|
|
20
|
+
process.js # Child process: sets up clients and listens on all queues
|
|
21
|
+
handlers/
|
|
22
|
+
periodic.js # Re-queues itself; dispatches cascade jobs; occasionally stalls/crashes
|
|
23
|
+
cascade-a.js # Dispatched by periodic; dispatches into cascade-b
|
|
24
|
+
cascade-b.js # Dispatched by cascade-a; final handler
|
|
25
|
+
fail-handler.js # Shared fail handler for all queues; records invocations
|
|
26
|
+
shared/
|
|
27
|
+
state.js # In-process shared state helpers (for the orchestrator)
|
|
28
|
+
log.js # Structured logger (writes to fuzz-output.log, never throws)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Process Architecture
|
|
32
|
+
|
|
33
|
+
The orchestrator (`fuzz.js`) spawns **N child processes** (default: 4). Each child process creates one Redis client and calls `listen()` on every queue. The orchestrator itself does not process jobs — it only monitors invariants and manages the lifecycle.
|
|
34
|
+
|
|
35
|
+
Handlers write events (job start, finish, fail, stall) directly to a Redis stream (`fuzz:events`). The orchestrator reads from this stream and maintains a shared in-memory log of events, checking invariants after each one. Child processes do not need to forward events to the orchestrator themselves — the stream is the shared channel.
|
|
36
|
+
|
|
37
|
+
Child processes are deliberately killed and restarted periodically to simulate crashes. A killed process' checked-out jobs will be swept and retried/failed by the remaining processes.
|
|
38
|
+
|
|
39
|
+
## Queue Configuration
|
|
40
|
+
|
|
41
|
+
Three queues at different priority levels, all listened on by every child process. Parameters are kept small to produce many events quickly:
|
|
42
|
+
|
|
43
|
+
| Parameter | `{fuzz}:periodic` | `{fuzz}:cascade-a` | `{fuzz}:cascade-b` |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| Handler | `periodic.js` | `cascade-a.js` | `cascade-b.js` |
|
|
46
|
+
| Priority | 300 | 200 | 100 |
|
|
47
|
+
| `maxRetries` | 3 | 3 | 3 |
|
|
48
|
+
| `maxStalls` | 2 | 2 | 2 |
|
|
49
|
+
| `minBackoff` | 200 ms | 200 ms | 200 ms |
|
|
50
|
+
| `maxBackoff` | 2 000 ms | 2 000 ms | 2 000 ms |
|
|
51
|
+
| `timeout` | 3 000 ms | 3 000 ms | 3 000 ms |
|
|
52
|
+
| `size` | 10 | 10 | 10 |
|
|
53
|
+
| `failHandler` | `fail-handler.js` | `fail-handler.js` | `fail-handler.js` |
|
|
54
|
+
| `failRetryOptions.maxRetries` | 5 | 5 | 5 |
|
|
55
|
+
| `failRetryOptions.minBackoff` | 200 ms | 200 ms | 200 ms |
|
|
56
|
+
|
|
57
|
+
The short `timeout` (3 s) means stalling jobs are detected and swept quickly. The short `minBackoff` / `maxBackoff` window (200 ms – 2 s) means retries cycle fast. With `maxRetries: 3` and `maxStalls: 2`, most failed jobs reach the fail handler within seconds.
|
|
58
|
+
|
|
59
|
+
## Periodic Jobs (Seed)
|
|
60
|
+
|
|
61
|
+
A fixed set of periodic job IDs (e.g., `periodic-0` through `periodic-4`) are dispatched by the orchestrator at startup. Each periodic handler:
|
|
62
|
+
|
|
63
|
+
1. Records the current processing event by writing `{ type: 'start', queue, id, threadId, clientId, startedAt }` to the `fuzz:events` Redis stream.
|
|
64
|
+
2. Optionally sleeps for a random short delay.
|
|
65
|
+
3. Dispatches a cascade-a job with a unique ID and a `runAt` randomly up to 2 seconds in the future.
|
|
66
|
+
4. Re-dispatches itself (same job ID, `updateRunAt: true`) with a delay of 1–5 seconds, so the job continues to fire periodically.
|
|
67
|
+
5. On success, writes `{ type: 'finish', queue, id, threadId, clientId, finishedAt }` to the `fuzz:events` stream.
|
|
68
|
+
|
|
69
|
+
The fail handler for periodic jobs also re-dispatches the same periodic job ID (with a delay), ensuring periodic jobs survive permanent failures. This lets the orchestrator assert that periodic jobs keep running indefinitely.
|
|
70
|
+
|
|
71
|
+
## Cascade Jobs
|
|
72
|
+
|
|
73
|
+
`cascade-a.js`:
|
|
74
|
+
- Records start/finish events.
|
|
75
|
+
- Dispatches one or two `cascade-b` jobs with unique IDs.
|
|
76
|
+
- Subject to all chaos behaviors (see below).
|
|
77
|
+
|
|
78
|
+
`cascade-b.js`:
|
|
79
|
+
- Records start/finish events.
|
|
80
|
+
- Terminal handler; does not dispatch further jobs.
|
|
81
|
+
- Subject to all chaos behaviors (see below).
|
|
82
|
+
|
|
83
|
+
## Chaos Behaviors
|
|
84
|
+
|
|
85
|
+
All handlers are subject to all chaos behaviors. The probabilities below are per-invocation and apply uniformly across `periodic.js`, `cascade-a.js`, and `cascade-b.js`:
|
|
86
|
+
|
|
87
|
+
| Behavior | Probability | Notes |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| Normal completion | ~65% | Dispatches downstream jobs (if any), then returns |
|
|
90
|
+
| Retriable error (throws `Error`) | ~15% | No downstream dispatch |
|
|
91
|
+
| Permanent error (throws `PermanentError`) | ~5% | No downstream dispatch |
|
|
92
|
+
| Stall (returns a never-resolving promise) | ~10% | Detected after `timeout` (3 s); counts as a stall |
|
|
93
|
+
| CPU spin (blocks the worker thread) | ~3% | Tight loop until the process detects the hang and kills the thread (via `timeout`) |
|
|
94
|
+
| Crash (causes the child process to exit) | ~2% | Handler writes a "crash-me" flag to Redis; main thread polls and exits |
|
|
95
|
+
|
|
96
|
+
With `timeout: 3000`, stalling and spinning jobs are swept within ~3–13 seconds (timeout + heartbeat sweep interval). With `maxStalls: 2`, two stalls exhaust the stall budget and the job is sent to the fail handler, cycling fast.
|
|
97
|
+
|
|
98
|
+
When a child process crashes, the orchestrator detects the exit event and restarts a new child process after a short delay.
|
|
99
|
+
|
|
100
|
+
## Event Logging and Invariant Checking
|
|
101
|
+
|
|
102
|
+
The orchestrator maintains an append-only in-memory event log. Each entry contains:
|
|
103
|
+
```js
|
|
104
|
+
{ type, queue, id, threadId, clientId, timestamp }
|
|
105
|
+
```
|
|
106
|
+
where `type` is one of: `start`, `finish`, `fail`, `stall`, `cancel`.
|
|
107
|
+
|
|
108
|
+
After each event is appended, the orchestrator runs incremental invariant checks:
|
|
109
|
+
|
|
110
|
+
### Invariant 1: Mutual Exclusion
|
|
111
|
+
Maintain a `Map<jobId, { clientId, threadId, startedAt }>` of currently-active jobs. On `start`, check that the job ID is not already in the map. On `finish`/`fail`/`stall`, remove it.
|
|
112
|
+
|
|
113
|
+
If a `start` event arrives for a job ID already in the map → **VIOLATION**.
|
|
114
|
+
|
|
115
|
+
### Invariant 2: No Re-processing of Succeeded Jobs
|
|
116
|
+
Maintain a `Set<jobId>` of successfully finished job IDs. On `start`, check that the ID is not in this set.
|
|
117
|
+
|
|
118
|
+
If a `start` event arrives for a job ID in the succeeded set → **VIOLATION**.
|
|
119
|
+
|
|
120
|
+
Note: Re-processing after a stall or retry is expected and must not be flagged.
|
|
121
|
+
|
|
122
|
+
### Invariant 3: Scheduling (No Early Processing)
|
|
123
|
+
Each `start` event includes `startedAt` (wall clock). Each job dispatch records an intended `runAt`. On `start`, verify `startedAt >= runAt - CLOCK_TOLERANCE_MS`.
|
|
124
|
+
|
|
125
|
+
If `startedAt < runAt - CLOCK_TOLERANCE_MS` → **VIOLATION**.
|
|
126
|
+
|
|
127
|
+
`CLOCK_TOLERANCE_MS` accounts for clock skew between the orchestrator, child processes, and Redis (default: 100ms).
|
|
128
|
+
|
|
129
|
+
### Invariant 4: Priority Ordering
|
|
130
|
+
Track the earliest-known `runAt` for jobs dispatched into each queue but not yet started. When a `start` event arrives for a job in that queue, verify no other eligible job (with `runAt <= now`) in the same queue has a lower `runAt` that has been waiting longer.
|
|
131
|
+
|
|
132
|
+
This invariant is best-effort and checked with a configurable lag (e.g., 200ms) to account for the inherent race between dequeue polling and dispatch. A violation is only flagged when the ordering difference exceeds this lag.
|
|
133
|
+
|
|
134
|
+
### Invariant 5: Fail Handler Completeness
|
|
135
|
+
Track every job that has been dispatched (by ID). When a job exceeds its `maxRetries` or receives a permanent error, a fail event should be observed. Maintain a map `{ jobId → { exhausted: bool, failSeen: bool } }`. After a configurable drain period (e.g., 30 seconds after a queue goes quiet), check that every exhausted job has a corresponding `fail` event.
|
|
136
|
+
|
|
137
|
+
### Invariant 6: Queue Progress
|
|
138
|
+
The orchestrator monitors the time since the last `start` event per queue. If a queue is known to be non-empty (based on dispatched vs finished counts) and no `start` event has been seen for more than a configurable `STALL_THRESHOLD_MS` (e.g., 30 seconds), flag a progress violation.
|
|
139
|
+
|
|
140
|
+
Priority starvation is checked by verifying that the low-priority queue does not process jobs while the high-priority queue has outstanding jobs older than the dequeue poll interval.
|
|
141
|
+
|
|
142
|
+
## Output and Reporting
|
|
143
|
+
|
|
144
|
+
Violations are logged to stdout and to `fuzz-output.log` with full context. The process does **not** exit on a violation — it logs and continues, accumulating a count of violations. A summary is printed periodically (every 60 seconds) and on `SIGINT`.
|
|
145
|
+
|
|
146
|
+
Log format (newline-delimited JSON):
|
|
147
|
+
```json
|
|
148
|
+
{ "time": "...", "level": "info|warn|error", "msg": "...", "data": { ... } }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Violation entries use level `"error"` and include the invariant name, the offending event, and relevant recent history.
|
|
152
|
+
|
|
153
|
+
## Configuration
|
|
154
|
+
|
|
155
|
+
All tunable parameters live at the top of `fuzz.js` as named constants:
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
const NUM_PROCESSES = 4; // Child processes
|
|
159
|
+
const NUM_PERIODIC_JOBS = 5; // Fixed periodic job IDs
|
|
160
|
+
const PERIODIC_MIN_DELAY = 1000; // ms before re-queuing self
|
|
161
|
+
const PERIODIC_MAX_DELAY = 5000;
|
|
162
|
+
const CRASH_INTERVAL_MS = 30000; // Orchestrator kills a random child process this often
|
|
163
|
+
const CLOCK_TOLERANCE_MS = 100;
|
|
164
|
+
const STALL_THRESHOLD_MS = 30000;
|
|
165
|
+
const PRIORITY_LAG_MS = 200;
|
|
166
|
+
const LOG_FILE = 'fuzz-output.log';
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Running
|
|
170
|
+
|
|
171
|
+
The fuzz test is separate from the default test suite and is never run by `npm test`. It is started manually:
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
node fuzztest/fuzz.js
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
It runs indefinitely. Stop with `Ctrl+C`. A summary of violations and events processed will be printed on exit.
|
|
178
|
+
|
|
179
|
+
## Notes on Implementation
|
|
180
|
+
|
|
181
|
+
- Child processes use the `queasy` library's public API (`queue()`, `dispatch()`, `listen()`). They do not talk directly to Redis.
|
|
182
|
+
- The orchestrator does not import from `src/`; it only spawns child processes and learns about child process lifecycle only from the `spawn` and `exit` events.
|
|
183
|
+
- All handler modules in `fuzztest/handlers/` must be self-contained ESM modules that can be passed as `handlerPath` to `queue.listen()`.
|
|
184
|
+
- Handlers write events to the `fuzz:events` Redis stream using a dedicated Redis client created at handler module load time. The orchestrator reads from this stream via `XREAD BLOCK`. This is the only communication channel between handlers and the orchestrator — no IPC is used.
|
|
185
|
+
- The chaos crash behavior must be triggered from the child process's main thread, not from inside a handler's worker thread. To simulate a crash, the handler uses `postMessage` to send a `{ type: 'crash' }` message to the main thread, which listens for it and calls `process.exit()`.
|