@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.
- package/README.md +128 -0
- package/esm/cross-process-smoke.spec.d.ts +2 -0
- package/esm/cross-process-smoke.spec.d.ts.map +1 -0
- package/esm/cross-process-smoke.spec.js +463 -0
- package/esm/cross-process-smoke.spec.js.map +1 -0
- package/esm/define-redis-task-runner.d.ts +38 -0
- package/esm/define-redis-task-runner.d.ts.map +1 -0
- package/esm/define-redis-task-runner.js +60 -0
- package/esm/define-redis-task-runner.js.map +1 -0
- package/esm/index.d.ts +7 -0
- package/esm/index.d.ts.map +1 -0
- package/esm/index.js +4 -0
- package/esm/index.js.map +1 -0
- package/esm/multi-service-smoke.spec.d.ts +2 -0
- package/esm/multi-service-smoke.spec.d.ts.map +1 -0
- package/esm/multi-service-smoke.spec.js +468 -0
- package/esm/multi-service-smoke.spec.js.map +1 -0
- package/esm/redis-queue-adapter.d.ts +125 -0
- package/esm/redis-queue-adapter.d.ts.map +1 -0
- package/esm/redis-queue-adapter.js +566 -0
- package/esm/redis-queue-adapter.js.map +1 -0
- package/esm/redis-queue-adapter.unit.spec.d.ts +2 -0
- package/esm/redis-queue-adapter.unit.spec.d.ts.map +1 -0
- package/esm/redis-queue-adapter.unit.spec.js +860 -0
- package/esm/redis-queue-adapter.unit.spec.js.map +1 -0
- package/esm/redis-task-runner.d.ts +33 -0
- package/esm/redis-task-runner.d.ts.map +1 -0
- package/esm/redis-task-runner.integration.spec.d.ts +2 -0
- package/esm/redis-task-runner.integration.spec.d.ts.map +1 -0
- package/esm/redis-task-runner.integration.spec.js +536 -0
- package/esm/redis-task-runner.integration.spec.js.map +1 -0
- package/esm/redis-task-runner.js +43 -0
- package/esm/redis-task-runner.js.map +1 -0
- package/esm/redis-task-runner.unit.spec.d.ts +2 -0
- package/esm/redis-task-runner.unit.spec.d.ts.map +1 -0
- package/esm/redis-task-runner.unit.spec.js +97 -0
- package/esm/redis-task-runner.unit.spec.js.map +1 -0
- package/package.json +63 -0
- package/src/cross-process-smoke.spec.ts +459 -0
- package/src/define-redis-task-runner.ts +91 -0
- package/src/index.ts +8 -0
- package/src/multi-service-smoke.spec.ts +511 -0
- package/src/redis-queue-adapter.ts +733 -0
- package/src/redis-queue-adapter.unit.spec.ts +613 -0
- package/src/redis-task-runner.integration.spec.ts +440 -0
- package/src/redis-task-runner.ts +67 -0
- 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 @@
|
|
|
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
|