@furystack/redis-task-runner 1.0.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.
Files changed (47) hide show
  1. package/README.md +128 -0
  2. package/esm/cross-process-smoke.spec.d.ts +2 -0
  3. package/esm/cross-process-smoke.spec.d.ts.map +1 -0
  4. package/esm/cross-process-smoke.spec.js +463 -0
  5. package/esm/cross-process-smoke.spec.js.map +1 -0
  6. package/esm/define-redis-task-runner.d.ts +38 -0
  7. package/esm/define-redis-task-runner.d.ts.map +1 -0
  8. package/esm/define-redis-task-runner.js +60 -0
  9. package/esm/define-redis-task-runner.js.map +1 -0
  10. package/esm/index.d.ts +7 -0
  11. package/esm/index.d.ts.map +1 -0
  12. package/esm/index.js +4 -0
  13. package/esm/index.js.map +1 -0
  14. package/esm/multi-service-smoke.spec.d.ts +2 -0
  15. package/esm/multi-service-smoke.spec.d.ts.map +1 -0
  16. package/esm/multi-service-smoke.spec.js +468 -0
  17. package/esm/multi-service-smoke.spec.js.map +1 -0
  18. package/esm/redis-queue-adapter.d.ts +125 -0
  19. package/esm/redis-queue-adapter.d.ts.map +1 -0
  20. package/esm/redis-queue-adapter.js +566 -0
  21. package/esm/redis-queue-adapter.js.map +1 -0
  22. package/esm/redis-queue-adapter.unit.spec.d.ts +2 -0
  23. package/esm/redis-queue-adapter.unit.spec.d.ts.map +1 -0
  24. package/esm/redis-queue-adapter.unit.spec.js +860 -0
  25. package/esm/redis-queue-adapter.unit.spec.js.map +1 -0
  26. package/esm/redis-task-runner.d.ts +33 -0
  27. package/esm/redis-task-runner.d.ts.map +1 -0
  28. package/esm/redis-task-runner.integration.spec.d.ts +2 -0
  29. package/esm/redis-task-runner.integration.spec.d.ts.map +1 -0
  30. package/esm/redis-task-runner.integration.spec.js +536 -0
  31. package/esm/redis-task-runner.integration.spec.js.map +1 -0
  32. package/esm/redis-task-runner.js +43 -0
  33. package/esm/redis-task-runner.js.map +1 -0
  34. package/esm/redis-task-runner.unit.spec.d.ts +2 -0
  35. package/esm/redis-task-runner.unit.spec.d.ts.map +1 -0
  36. package/esm/redis-task-runner.unit.spec.js +97 -0
  37. package/esm/redis-task-runner.unit.spec.js.map +1 -0
  38. package/package.json +63 -0
  39. package/src/cross-process-smoke.spec.ts +459 -0
  40. package/src/define-redis-task-runner.ts +91 -0
  41. package/src/index.ts +8 -0
  42. package/src/multi-service-smoke.spec.ts +511 -0
  43. package/src/redis-queue-adapter.ts +733 -0
  44. package/src/redis-queue-adapter.unit.spec.ts +613 -0
  45. package/src/redis-task-runner.integration.spec.ts +440 -0
  46. package/src/redis-task-runner.ts +67 -0
  47. package/src/redis-task-runner.unit.spec.ts +110 -0
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # @furystack/redis-task-runner
2
+
3
+ Redis Streams adapter for [`@furystack/task-runner`](../task-runner). Backs
4
+ multi-node task execution with persistent, broker-side-reclaimable consumer
5
+ groups. See `docs/internal/distributed-task-management.md` Milestone 3 for
6
+ the full design.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @furystack/redis-task-runner
12
+ # or
13
+ yarn add @furystack/redis-task-runner
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ The caller owns the `redis` client lifecycle (`connect` / `quit`) — same as
19
+ [`@furystack/redis-cross-node-bus`](../redis-cross-node-bus) and
20
+ [`@furystack/redis-store`](../redis-store).
21
+
22
+ ```ts
23
+ import { S3Client } from '@aws-sdk/client-s3'
24
+ import { createInjector } from '@furystack/inject'
25
+ import { BlobStore } from '@furystack/blob-store'
26
+ import { CrossNodeBus } from '@furystack/cross-node-bus'
27
+ import { defineRedisCrossNodeBusAdapter } from '@furystack/redis-cross-node-bus'
28
+ import { defineS3BlobStore } from '@furystack/s3-blob-store'
29
+ import { TaskRunner } from '@furystack/task-runner'
30
+ import { defineRedisTaskRunner } from '@furystack/redis-task-runner'
31
+ import { createClient } from 'redis'
32
+
33
+ const client = createClient({ url: process.env.REDIS_URL })
34
+ await client.connect()
35
+
36
+ const s3Client = new S3Client({ region: process.env.AWS_REGION })
37
+
38
+ await using injector = createInjector()
39
+ injector.bind(CrossNodeBus, defineRedisCrossNodeBusAdapter({ client, serviceName: 'svc-a' }))
40
+ injector.bind(BlobStore, defineS3BlobStore({ client: s3Client, bucket: 'svc-a-blobs' }))
41
+ injector.bind(
42
+ TaskRunner,
43
+ defineRedisTaskRunner({
44
+ client,
45
+ serviceName: 'svc-a',
46
+ topicPrefix: 'svc-a/',
47
+ visibilityTimeoutMs: 60_000,
48
+ }),
49
+ )
50
+
51
+ const runner = injector.get(TaskRunner)
52
+
53
+ // shutdown
54
+ await injector[Symbol.asyncDispose]()
55
+ await client.quit()
56
+ ```
57
+
58
+ ## Stream layout
59
+
60
+ | Key | Purpose |
61
+ | ------------------------------------------------- | ----------------------------------------------------------------------------- |
62
+ | `${prefix}tasks:queue:${type}:v${handlerVersion}` | One stream per `(type, version)` lane. XADD on enqueue. |
63
+ | `${prefix}tasks:idem:${type}:${idempotencyKey}` | `SET NX EX` idempotency-lease key. |
64
+ | `${prefix}tasks:scheduler` | Single ZSET parking delayed tasks until their `notBefore` (members are JSON). |
65
+
66
+ A single consumer group named `runner` (override via `consumerGroup`) joins
67
+ every lane. Each registered worker spawns `concurrency` slots; slot consumers
68
+ are named `${workerId}-slot-${index}`.
69
+
70
+ ## Capabilities
71
+
72
+ | Capability | Value | Notes |
73
+ | --------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
74
+ | `persistent` | `true` | Streams survive broker restarts (configure broker durability separately). |
75
+ | `distributed` | `true` | Consumer-group competing-consumer dispatch across nodes. |
76
+ | `delayedDispatch` | `true` | Scheduler ZSET + Lua atomic-pop dispatcher; `notBefore` honored at the broker. |
77
+ | `fleetCapEnforcement` | `true` | Per-type ZSET holder tokens (scored by expiry); a claim that finds its lane full is released back to the stream and retried, so the cap holds fleet-wide. |
78
+ | `brokerSideReclaim` | `true` | `XAUTOCLAIM` recovers stale PEL entries past the visibility timeout. |
79
+
80
+ ## Delayed dispatch
81
+
82
+ Tasks submitted with a future `notBefore` are parked in a single global
83
+ `${prefix}tasks:scheduler` ZSET (member = JSON `{taskId, type, handlerVersion}`,
84
+ score = epoch ms). Every adapter instance ticks a 250 ms scheduler that
85
+ evaluates a short Lua script atomically popping due entries and
86
+ `XADD`ing them onto the matching `(type, version)` stream. Lua atomicity
87
+ (the `ZREM` inside the script) prevents two adapter instances from
88
+ double-dispatching the same delayed task.
89
+
90
+ Tune the tick rate via `schedulerIntervalMs` — lower = lower delay
91
+ jitter at the cost of broker chatter; higher = looser floor on
92
+ `notBefore` precision.
93
+
94
+ ## Visibility & reclaim
95
+
96
+ Each slot iteration runs `XAUTOCLAIM` first (idle threshold = the
97
+ type-specific `visibilityTimeoutMs`); reclaimed entries are delivered through
98
+ the same `onClaim` path as fresh ones. The runner core marks the prior
99
+ in-progress attempt as `'timed-out'` before starting a fresh attempt — handlers
100
+ that observe `ctx.attempt > 1` see the same payload as attempt 1 (replay log
101
+ keyed by `(taskId, stepIndex)` dedups recorded steps).
102
+
103
+ `heartbeat()` resets the broker-side idle counter via `XCLAIM JUSTID`. Hot-lane
104
+ progress reports (`ctx.reportProgress`) implicitly heartbeat via the runner
105
+ core's coalesced dataset write — handlers reporting progress regularly stay
106
+ under the visibility threshold automatically.
107
+
108
+ ## Constructor options
109
+
110
+ | Option | Type | Default | Notes |
111
+ | ------------------------- | -------- | ---------- | ----------------------------------------------------- |
112
+ | `client` | required | — | Caller-owned `redis` client. Must be connected. |
113
+ | `serviceName` | required | — | Telemetry attribution. |
114
+ | `topicPrefix` | optional | `''` | Wire prefix for every stream / lease / scheduler key. |
115
+ | `consumerGroup` | optional | `'runner'` | Group name. Override only when sharing a broker. |
116
+ | `visibilityTimeoutMs` | optional | `60_000` | Default reclaim threshold per task lane. |
117
+ | `visibilityTimeoutByType` | optional | `{}` | Per-type override (`{ 'video-encode': 600_000 }`). |
118
+ | `blockTimeoutMs` | optional | `200` | `XREADGROUP BLOCK` timeout. |
119
+ | `retryBackoffMs` | optional | `250` | Backoff between reads after broker errors. |
120
+ | `idempotencyTtlSec` | optional | `86_400` | `SET NX EX` lease TTL. |
121
+ | `schedulerIntervalMs` | optional | `250` | Delayed-dispatch scheduler tick interval. |
122
+
123
+ ## Integration tests
124
+
125
+ `src/redis-task-runner.integration.spec.ts` exercises submit/claim/complete,
126
+ two-worker no-double-execute, broker-side reclaim, draft/start two-phase,
127
+ and cancel-broadcast against a real broker reachable at `REDIS_URL`
128
+ (default `redis://localhost:6379`). CI brings it up via `docker compose up -d`.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cross-process-smoke.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cross-process-smoke.spec.d.ts","sourceRoot":"","sources":["../src/cross-process-smoke.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,463 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ /**
54
+ * Cross-process multi-worker smoke (PRD F2 / Milestone 6 precursor).
55
+ *
56
+ * Forks real OS worker processes (`child_process.fork`) that each host their
57
+ * own injector, Redis client and `RedisTaskRunner`, sharing one broker. The
58
+ * Task + TaskReplayLog datasets are bound to `@furystack/redis-store` so task
59
+ * state is coherent across processes (this is what F1 + F1b unblocked — the
60
+ * runner cannot use an in-memory store cross-process). The cross-node bus is
61
+ * in-process per child: the scenarios below (claim concurrency, fleet cap,
62
+ * visibility reclaim, drain) are all broker- or worker-local and need no
63
+ * cross-process bus delivery.
64
+ *
65
+ * Scenarios:
66
+ *
67
+ * 1. **Claim concurrency / no double-execute** — two worker processes drain a
68
+ * batch of tasks; every task runs exactly once across the fleet.
69
+ * 2. **Fleet cap** — `concurrencyLimits` holds the max-concurrent count for a
70
+ * type across both processes (measured via a shared Redis counter).
71
+ * 3. **Visibility reclaim** — a worker is `SIGKILL`ed mid-handler; a second
72
+ * worker reclaims the stalled claim (`XAUTOCLAIM`) and completes it.
73
+ * 4. **Graceful drain** — a worker drains mid-flight: the in-flight task
74
+ * finishes and no further task is claimed.
75
+ *
76
+ * **Build prereq.** Children execute the compiled `esm/*.js`, so the package
77
+ * must be built before this runs. CI runs `yarn build` before `yarn test`;
78
+ * a local isolated run needs `yarn workspace @furystack/redis-task-runner build`
79
+ * first.
80
+ *
81
+ * **Redis prereq.** Broker reachable at `REDIS_URL` (default
82
+ * `redis://localhost:6379`); CI brings it up via `docker compose up -d`.
83
+ */
84
+ import { fork } from 'node:child_process';
85
+ import { randomUUID } from 'node:crypto';
86
+ import { dirname, resolve } from 'node:path';
87
+ import { fileURLToPath } from 'node:url';
88
+ import { BlobStore, InMemoryBlobStore } from '@furystack/blob-store';
89
+ import { CrossNodeBus, defineInProcessCrossNodeBus } from '@furystack/cross-node-bus';
90
+ import { createInjector } from '@furystack/inject';
91
+ import { RedisStore } from '@furystack/redis-store';
92
+ import { defineTaskHandler, isTerminalStatus, Task, TaskReplayLogEntry, TaskReplayLogStore, TaskRunner, TaskStore, } from '@furystack/task-runner';
93
+ import { defineRedisTaskRunner } from './define-redis-task-runner.js';
94
+ import { createClient } from 'redis';
95
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
96
+ const REDIS_URL = process.env.REDIS_URL ?? 'redis://localhost:6379';
97
+ const CHILD_CONFIG_ENV = 'CHILD_CONFIG';
98
+ /** Tracks the max concurrent value seen in `KEYS[1]` against the live count. */
99
+ const MAX_SCRIPT = `
100
+ local m = tonumber(redis.call('GET', KEYS[1]) or '0')
101
+ local v = tonumber(ARGV[1])
102
+ if v > m then redis.call('SET', KEYS[1], v) end
103
+ return v
104
+ `;
105
+ const sleep = (ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
106
+ const evalScript = (client, script, keys, args) => {
107
+ const evalFn = client.eval;
108
+ return evalFn.call(client, script, { keys, arguments: args });
109
+ };
110
+ /**
111
+ * Handlers shared by every worker. Each records progress in shared Redis keys
112
+ * (under the `${storePrefix}c:` namespace) so the parent can assert behavior
113
+ * across processes.
114
+ */
115
+ const buildHandlers = (client, ns) => {
116
+ const echo = defineTaskHandler({
117
+ type: 'echo',
118
+ version: 1,
119
+ handler: async (ctx) => {
120
+ await client.incr(`${ns}exec:${ctx.taskId}`);
121
+ return { ok: true };
122
+ },
123
+ });
124
+ const capped = defineTaskHandler({
125
+ type: 'capped',
126
+ version: 1,
127
+ handler: async () => {
128
+ const live = await client.incr(`${ns}live`);
129
+ await evalScript(client, MAX_SCRIPT, [`${ns}max`], [String(live)]);
130
+ await sleep(300);
131
+ await client.decr(`${ns}live`);
132
+ },
133
+ });
134
+ const reclaim = defineTaskHandler({
135
+ type: 'reclaim',
136
+ version: 1,
137
+ retryPolicy: { maxAttempts: 1, backoff: 'none', baseDelayMs: 0, jitter: 0 },
138
+ handler: async () => {
139
+ await client.incr(`${ns}reclaim:starts`);
140
+ await sleep(5000);
141
+ await client.incr(`${ns}reclaim:finishes`);
142
+ },
143
+ });
144
+ const drainable = defineTaskHandler({
145
+ type: 'drain',
146
+ version: 1,
147
+ handler: async () => {
148
+ await client.incr(`${ns}drain:started`);
149
+ await sleep(700);
150
+ await client.incr(`${ns}drain:done`);
151
+ },
152
+ });
153
+ return [echo, capped, reclaim, drainable];
154
+ };
155
+ const bindRuntime = (injector, client, config) => {
156
+ injector.bind(BlobStore, ({ onDispose }) => {
157
+ const store = new InMemoryBlobStore({ name: `${config.id}-blobs` });
158
+ // eslint-disable-next-line furystack/prefer-using-wrapper -- delegated to onDispose
159
+ onDispose(() => store[Symbol.dispose]());
160
+ return store;
161
+ });
162
+ injector.bind(CrossNodeBus, defineInProcessCrossNodeBus());
163
+ // `as PhysicalStore` bridges the method-param variance gap between
164
+ // `RedisStore`'s `TWriteableData` (id required) and the StoreToken's
165
+ // `WithOptionalId` — the same shape `defineStore` accepts via bivariance.
166
+ injector.bind(TaskStore, () => new RedisStore({ model: Task, primaryKey: 'id', client, keyPrefix: `${config.storePrefix}t` }));
167
+ injector.bind(TaskReplayLogStore, () => new RedisStore({
168
+ model: TaskReplayLogEntry,
169
+ primaryKey: 'id',
170
+ client,
171
+ keyPrefix: `${config.storePrefix}r`,
172
+ }));
173
+ injector.bind(TaskRunner, defineRedisTaskRunner({
174
+ client,
175
+ serviceName: config.id,
176
+ topicPrefix: config.topicPrefix,
177
+ visibilityTimeoutMs: config.visibilityMs,
178
+ blockTimeoutMs: 50,
179
+ retryBackoffMs: 50,
180
+ schedulerIntervalMs: 50,
181
+ reconcilerIntervalMs: 1_000_000,
182
+ concurrencyLimits: config.capLimit !== undefined ? { capped: config.capLimit } : undefined,
183
+ }));
184
+ };
185
+ // ─────────────────────────────────────────────────────────────────────────────
186
+ // Child worker process.
187
+ // ─────────────────────────────────────────────────────────────────────────────
188
+ const runChild = async (config) => {
189
+ const send = (message) => {
190
+ process.send?.(message);
191
+ };
192
+ const client = createClient({ url: REDIS_URL });
193
+ await client.connect();
194
+ const injector = createInjector();
195
+ bindRuntime(injector, client, config);
196
+ const runner = injector.get(TaskRunner);
197
+ const worker = runner.registerWorker({
198
+ name: config.id,
199
+ handlers: buildHandlers(client, `${config.storePrefix}c:`),
200
+ concurrency: config.concurrency,
201
+ tags: [],
202
+ compatibleVersions: {},
203
+ });
204
+ process.on('message', (message) => {
205
+ void (async () => {
206
+ if (message.kind === 'drain') {
207
+ await worker.drain({ timeoutMs: 5000 });
208
+ send({ kind: 'drained', id: config.id });
209
+ }
210
+ else {
211
+ worker[Symbol.dispose]();
212
+ await injector[Symbol.asyncDispose]();
213
+ if (client.isOpen)
214
+ await client.quit();
215
+ process.exit(0);
216
+ }
217
+ })().catch((error) => send({ kind: 'log', line: `${config.id} crashed: ${String(error)}` }));
218
+ });
219
+ send({ kind: 'ready', id: config.id });
220
+ };
221
+ const isChildMode = process.argv[2] === 'child';
222
+ if (isChildMode) {
223
+ const raw = process.env[CHILD_CONFIG_ENV];
224
+ if (!raw) {
225
+ process.stderr.write('cross-process-smoke child requires CHILD_CONFIG\n');
226
+ process.exit(1);
227
+ }
228
+ else {
229
+ runChild(JSON.parse(raw)).catch((error) => {
230
+ process.stderr.write(`child crashed: ${error.stack ?? String(error)}\n`);
231
+ process.exit(1);
232
+ });
233
+ }
234
+ }
235
+ // ─────────────────────────────────────────────────────────────────────────────
236
+ // Parent / vitest side.
237
+ // ─────────────────────────────────────────────────────────────────────────────
238
+ const COMPILED_SPEC_PATH = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'esm', 'cross-process-smoke.spec.js');
239
+ const forkWorker = (config) => {
240
+ const inbox = [];
241
+ const listeners = new Set();
242
+ const child = fork(COMPILED_SPEC_PATH, ['child', config.id], {
243
+ env: { ...process.env, [CHILD_CONFIG_ENV]: JSON.stringify(config) },
244
+ });
245
+ child.on('message', (message) => {
246
+ const typed = message;
247
+ inbox.push(typed);
248
+ for (const listener of listeners)
249
+ listener(typed);
250
+ });
251
+ const receive = (predicate, timeoutMs = 8000) => {
252
+ const matched = inbox.find(predicate);
253
+ if (matched)
254
+ return Promise.resolve(matched);
255
+ return new Promise((resolveReceive, reject) => {
256
+ const timer = setTimeout(() => {
257
+ listeners.delete(handler);
258
+ reject(new Error(`Timed out after ${timeoutMs}ms waiting on ${config.id}`));
259
+ }, timeoutMs);
260
+ const handler = (message) => {
261
+ if (predicate(message)) {
262
+ clearTimeout(timer);
263
+ listeners.delete(handler);
264
+ resolveReceive(message);
265
+ }
266
+ };
267
+ listeners.add(handler);
268
+ });
269
+ };
270
+ return { id: config.id, child, receive };
271
+ };
272
+ const shutdownWorker = async (worker) => {
273
+ if (worker.child.exitCode !== null || worker.child.signalCode !== null)
274
+ return;
275
+ worker.child.send({ kind: 'shutdown' });
276
+ await new Promise((resolveExit) => {
277
+ if (worker.child.exitCode !== null)
278
+ resolveExit();
279
+ else
280
+ worker.child.once('exit', () => resolveExit());
281
+ });
282
+ };
283
+ const buildParent = async (topicPrefix, storePrefix) => {
284
+ const client = createClient({ url: REDIS_URL });
285
+ await client.connect();
286
+ const injector = createInjector();
287
+ bindRuntime(injector, client, { id: 'parent', topicPrefix, storePrefix, visibilityMs: 60_000 });
288
+ const runner = injector.get(TaskRunner);
289
+ return {
290
+ runner,
291
+ client,
292
+ [Symbol.asyncDispose]: async () => {
293
+ await injector[Symbol.asyncDispose]();
294
+ if (client.isOpen)
295
+ await client.quit();
296
+ },
297
+ };
298
+ };
299
+ const waitFor = async (predicate, timeoutMs) => {
300
+ const deadline = Date.now() + timeoutMs;
301
+ while (Date.now() < deadline) {
302
+ if (await predicate())
303
+ return;
304
+ await sleep(75);
305
+ }
306
+ throw new Error(`waitFor: timed out after ${timeoutMs}ms`);
307
+ };
308
+ const pollStatus = async (parent, taskId, timeoutMs) => {
309
+ const deadline = Date.now() + timeoutMs;
310
+ while (Date.now() < deadline) {
311
+ const task = await parent.runner.get(taskId);
312
+ if (task && isTerminalStatus(task.status))
313
+ return task.status;
314
+ await sleep(100);
315
+ }
316
+ return (await parent.runner.get(taskId))?.status;
317
+ };
318
+ const num = (value) => (value ? Number(value) : 0);
319
+ if (!isChildMode) {
320
+ describe('cross-process smoke (PRD F2)', () => {
321
+ let topicPrefix;
322
+ let storePrefix;
323
+ let active = [];
324
+ const fork_ = (config) => {
325
+ const worker = forkWorker({ ...config, topicPrefix, storePrefix });
326
+ active.push(worker);
327
+ return worker;
328
+ };
329
+ const awaitReady = async (...workers) => {
330
+ await Promise.all(workers.map((worker) => worker.receive((message) => message.kind === 'ready')));
331
+ };
332
+ beforeEach(() => {
333
+ topicPrefix = `xp-${randomUUID().slice(0, 8)}/`;
334
+ storePrefix = `xps-${randomUUID().slice(0, 8)}/`;
335
+ active = [];
336
+ });
337
+ afterEach(async () => {
338
+ await Promise.all(active.map(shutdownWorker));
339
+ active = [];
340
+ const client = createClient({ url: REDIS_URL });
341
+ try {
342
+ await client.connect();
343
+ for (const prefix of [topicPrefix, storePrefix]) {
344
+ const keys = await client.keys(`${prefix}*`);
345
+ if (keys.length > 0)
346
+ await client.del(keys);
347
+ }
348
+ }
349
+ catch {
350
+ // Redis unreachable — the test bodies already failed loudly.
351
+ }
352
+ finally {
353
+ if (client.isOpen)
354
+ await client.quit();
355
+ }
356
+ }, 15_000);
357
+ it('runs each task exactly once across two worker processes', async () => {
358
+ const env_1 = { stack: [], error: void 0, hasError: false };
359
+ try {
360
+ const parent = __addDisposableResource(env_1, await buildParent(topicPrefix, storePrefix), true);
361
+ const w1 = fork_({ id: 'w1', concurrency: 2, visibilityMs: 60_000 });
362
+ const w2 = fork_({ id: 'w2', concurrency: 2, visibilityMs: 60_000 });
363
+ await awaitReady(w1, w2);
364
+ const tasks = await Promise.all(Array.from({ length: 8 }, () => parent.runner.submit({ type: 'echo', payload: {}, handlerVersion: 1 })));
365
+ for (const task of tasks) {
366
+ expect(await pollStatus(parent, task.id, 15_000)).toBe('succeeded');
367
+ }
368
+ for (const task of tasks) {
369
+ expect(num(await parent.client.get(`${storePrefix}c:exec:${task.id}`))).toBe(1);
370
+ }
371
+ }
372
+ catch (e_1) {
373
+ env_1.error = e_1;
374
+ env_1.hasError = true;
375
+ }
376
+ finally {
377
+ const result_1 = __disposeResources(env_1);
378
+ if (result_1)
379
+ await result_1;
380
+ }
381
+ }, 40_000);
382
+ it('enforces a fleet-wide concurrency cap across two worker processes', async () => {
383
+ const env_2 = { stack: [], error: void 0, hasError: false };
384
+ try {
385
+ const parent = __addDisposableResource(env_2, await buildParent(topicPrefix, storePrefix), true);
386
+ const w1 = fork_({ id: 'w1', concurrency: 2, visibilityMs: 60_000, capLimit: 2 });
387
+ const w2 = fork_({ id: 'w2', concurrency: 2, visibilityMs: 60_000, capLimit: 2 });
388
+ await awaitReady(w1, w2);
389
+ const tasks = await Promise.all(Array.from({ length: 6 }, () => parent.runner.submit({ type: 'capped', payload: {}, handlerVersion: 1 })));
390
+ for (const task of tasks) {
391
+ expect(await pollStatus(parent, task.id, 20_000)).toBe('succeeded');
392
+ }
393
+ const maxConcurrent = num(await parent.client.get(`${storePrefix}c:max`));
394
+ expect(maxConcurrent).toBeLessThanOrEqual(2);
395
+ expect(maxConcurrent).toBe(2);
396
+ }
397
+ catch (e_2) {
398
+ env_2.error = e_2;
399
+ env_2.hasError = true;
400
+ }
401
+ finally {
402
+ const result_2 = __disposeResources(env_2);
403
+ if (result_2)
404
+ await result_2;
405
+ }
406
+ }, 40_000);
407
+ it('reclaims a task after the owning worker process is killed', async () => {
408
+ const env_3 = { stack: [], error: void 0, hasError: false };
409
+ try {
410
+ const parent = __addDisposableResource(env_3, await buildParent(topicPrefix, storePrefix), true);
411
+ const victim = fork_({ id: 'victim', concurrency: 1, visibilityMs: 1000 });
412
+ await awaitReady(victim);
413
+ const task = await parent.runner.submit({ type: 'reclaim', payload: {}, handlerVersion: 1 });
414
+ await waitFor(async () => num(await parent.client.get(`${storePrefix}c:reclaim:starts`)) >= 1, 10_000);
415
+ // Hard-kill the worker mid-handler; its claim goes stale in the PEL.
416
+ victim.child.kill('SIGKILL');
417
+ const survivor = fork_({ id: 'survivor', concurrency: 1, visibilityMs: 1000 });
418
+ await awaitReady(survivor);
419
+ expect(await pollStatus(parent, task.id, 25_000)).toBe('succeeded');
420
+ expect(num(await parent.client.get(`${storePrefix}c:reclaim:finishes`))).toBeGreaterThanOrEqual(1);
421
+ }
422
+ catch (e_3) {
423
+ env_3.error = e_3;
424
+ env_3.hasError = true;
425
+ }
426
+ finally {
427
+ const result_3 = __disposeResources(env_3);
428
+ if (result_3)
429
+ await result_3;
430
+ }
431
+ }, 45_000);
432
+ it('drains a worker process: in-flight task finishes, no new task is claimed', async () => {
433
+ const env_4 = { stack: [], error: void 0, hasError: false };
434
+ try {
435
+ const parent = __addDisposableResource(env_4, await buildParent(topicPrefix, storePrefix), true);
436
+ const worker = fork_({ id: 'w1', concurrency: 1, visibilityMs: 60_000 });
437
+ await awaitReady(worker);
438
+ const inFlight = await parent.runner.submit({ type: 'drain', payload: {}, handlerVersion: 1 });
439
+ await waitFor(async () => num(await parent.client.get(`${storePrefix}c:drain:started`)) >= 1, 10_000);
440
+ worker.child.send({ kind: 'drain' });
441
+ const queued = await parent.runner.submit({ type: 'drain', payload: {}, handlerVersion: 1 });
442
+ await worker.receive((message) => message.kind === 'drained', 10_000);
443
+ // The in-flight task completed despite the drain.
444
+ expect((await parent.runner.get(inFlight.id))?.status).toBe('succeeded');
445
+ expect(num(await parent.client.get(`${storePrefix}c:drain:done`))).toBe(1);
446
+ // The second task was never claimed by the drained worker.
447
+ await sleep(500);
448
+ expect((await parent.runner.get(queued.id))?.status).toBe('pending');
449
+ expect(num(await parent.client.get(`${storePrefix}c:drain:started`))).toBe(1);
450
+ }
451
+ catch (e_4) {
452
+ env_4.error = e_4;
453
+ env_4.hasError = true;
454
+ }
455
+ finally {
456
+ const result_4 = __disposeResources(env_4);
457
+ if (result_4)
458
+ await result_4;
459
+ }
460
+ }, 40_000);
461
+ });
462
+ }
463
+ //# sourceMappingURL=cross-process-smoke.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cross-process-smoke.spec.js","sourceRoot":"","sources":["../src/cross-process-smoke.spec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { ServiceFactory } from '@furystack/inject';
2
+ import { type TaskRunner } from '@furystack/task-runner';
3
+ import type { RedisQueueAdapterOptions } from './redis-queue-adapter.js';
4
+ import { type RedisTaskRunnerOptions } from './redis-task-runner.js';
5
+ /**
6
+ * Options accepted by {@link defineRedisTaskRunner}. `client` and
7
+ * `serviceName` are required; the remaining knobs forward to
8
+ * {@link RedisQueueAdapterOptions}.
9
+ */
10
+ export type DefineRedisTaskRunnerOptions = Omit<RedisQueueAdapterOptions, 'client' | 'serviceName'> & {
11
+ client: RedisQueueAdapterOptions['client'];
12
+ serviceName: string;
13
+ } & RedisTaskRunnerOptions;
14
+ /**
15
+ * Returns a {@link ServiceFactory} for {@link TaskRunner}, backed by
16
+ * Redis Streams. Override the (intentionally throwing) default
17
+ * binding at boot:
18
+ *
19
+ * ```ts
20
+ * const client = createClient({ url: process.env.REDIS_URL })
21
+ * await client.connect()
22
+ *
23
+ * injector.bind(
24
+ * TaskRunner,
25
+ * defineRedisTaskRunner({
26
+ * client,
27
+ * serviceName: 'svc-a',
28
+ * topicPrefix: 'svc-a/',
29
+ * visibilityTimeoutMs: 60_000,
30
+ * }),
31
+ * )
32
+ * ```
33
+ *
34
+ * Caller still owns `client.connect()` / `client.quit()` —
35
+ * the adapter never closes it.
36
+ */
37
+ export declare const defineRedisTaskRunner: (options: DefineRedisTaskRunnerOptions) => ServiceFactory<TaskRunner>;
38
+ //# sourceMappingURL=define-redis-task-runner.d.ts.map