@c9up/bay 0.1.12 → 0.1.13
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 +66 -1
- package/dist/BayProvider.d.ts +24 -30
- package/dist/BayProvider.d.ts.map +1 -1
- package/dist/BayProvider.js +58 -9
- package/dist/BayProvider.js.map +1 -1
- package/dist/QueueManager.d.ts +7 -1
- package/dist/QueueManager.d.ts.map +1 -1
- package/dist/QueueManager.js +49 -5
- package/dist/QueueManager.js.map +1 -1
- package/dist/configure.d.ts +18 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +31 -0
- package/dist/configure.js.map +1 -0
- package/dist/drivers/RedisDriver.d.ts +6 -0
- package/dist/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/drivers/RedisDriver.js +49 -17
- package/dist/drivers/RedisDriver.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/nodeEnv.d.ts +16 -0
- package/dist/nodeEnv.d.ts.map +1 -0
- package/dist/nodeEnv.js +32 -0
- package/dist/nodeEnv.js.map +1 -0
- package/dist/services/main.d.ts +5 -0
- package/dist/services/main.d.ts.map +1 -1
- package/dist/services/main.js +7 -0
- package/dist/services/main.js.map +1 -1
- package/dist/stores.d.ts +41 -0
- package/dist/stores.d.ts.map +1 -0
- package/dist/stores.js +46 -0
- package/dist/stores.js.map +1 -0
- package/package.json +5 -1
- package/src/BayProvider.ts +84 -19
- package/src/QueueManager.ts +48 -5
- package/src/configure.ts +45 -0
- package/src/drivers/RedisDriver.ts +69 -22
- package/src/index.ts +1 -0
- package/src/nodeEnv.ts +30 -0
- package/src/services/main.ts +8 -0
- package/src/stores.ts +59 -0
package/README.md
CHANGED
|
@@ -11,9 +11,14 @@ pnpm add @c9up/bay
|
|
|
11
11
|
ream configure @c9up/bay
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
`ream add @c9up/bay` installs it, registers the provider and writes
|
|
15
|
+
`config/queue.ts`. The rest of this page assumes that has run.
|
|
16
|
+
|
|
14
17
|
## Usage
|
|
15
18
|
|
|
16
|
-
Register the provider
|
|
19
|
+
Register the provider, then name the queue backend in `config/queue.ts` — the
|
|
20
|
+
provider reads `config.get('queue')`, so a `config/bay.ts` would be loaded under
|
|
21
|
+
the key `bay` and never seen:
|
|
17
22
|
|
|
18
23
|
```ts
|
|
19
24
|
// reamrc.ts
|
|
@@ -22,6 +27,66 @@ providers: [
|
|
|
22
27
|
]
|
|
23
28
|
```
|
|
24
29
|
|
|
30
|
+
```ts
|
|
31
|
+
// config/queue.ts
|
|
32
|
+
import { defineConfig, stores } from '@c9up/bay'
|
|
33
|
+
import env from '#start/env'
|
|
34
|
+
|
|
35
|
+
export default defineConfig({
|
|
36
|
+
default: env.get('QUEUE_STORE'),
|
|
37
|
+
stores: {
|
|
38
|
+
memory: stores.memory(),
|
|
39
|
+
redis: stores.redis({ connection: 'main' }),
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// start/queue.ts
|
|
46
|
+
import queue from '@c9up/bay/services/main'
|
|
47
|
+
|
|
48
|
+
queue.register('send-email', new SendEmailJob())
|
|
49
|
+
await queue.dispatch('send-email', { to: 'user@example.com' })
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| Store | Keeps jobs | Use it when |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| `stores.memory()` | until the process exits | tests, and local work |
|
|
55
|
+
| `stores.redis({ connection })` | in Redis | anything that must survive a restart |
|
|
56
|
+
|
|
57
|
+
Factories are lazy: only the store actually selected is built, so naming a Redis
|
|
58
|
+
queue in a config that runs in memory opens no connection. A `default` that
|
|
59
|
+
names nothing throws, listing what exists — falling back to memory would look
|
|
60
|
+
like it worked until a restart dropped every pending job.
|
|
61
|
+
|
|
62
|
+
`stores.redis` takes a `@c9up/quasar` connection name, resolved at first use so
|
|
63
|
+
bay never imports quasar, which stays an optional peer. Pass an ioredis-shaped
|
|
64
|
+
client (or a function answering one) to use any other.
|
|
65
|
+
|
|
66
|
+
## Delivery guarantee, and the one way to lose it
|
|
67
|
+
|
|
68
|
+
A job the queue accepted gets run: `pop()` moves it from pending to processing
|
|
69
|
+
in a single `LMOVE`, so a worker that dies mid-job leaves the job recoverable
|
|
70
|
+
rather than gone.
|
|
71
|
+
|
|
72
|
+
`LMOVE` needs **Redis 6.2 or later**. Without it the move is `lpop` then
|
|
73
|
+
`rpush`, and a crash between the two deletes the job from pending before it
|
|
74
|
+
reaches processing — nothing recovers it, because nothing knows it existed.
|
|
75
|
+
That turns at-least-once delivery into at-most-once.
|
|
76
|
+
|
|
77
|
+
So on an older Redis the driver **refuses to start in production**, and says
|
|
78
|
+
which two ways out there are:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// Either upgrade the server, or state that losing a job is acceptable here:
|
|
82
|
+
stores.redis({ connection: 'jobs', allowNonAtomicPop: true })
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The opt-in is honoured and still logs a warning on every process that starts
|
|
86
|
+
with it, naming production — agreeing once in a config file is not the same as
|
|
87
|
+
being reminded, in the logs of an incident, that this is how the process was
|
|
88
|
+
running. Outside production the fallback simply warns.
|
|
89
|
+
|
|
25
90
|
## Entry points
|
|
26
91
|
|
|
27
92
|
- `@c9up/bay` — main API
|
package/dist/BayProvider.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { QueueStoreFactory } from "./stores.js";
|
|
1
2
|
/**
|
|
2
3
|
* Slim, duck-typed host context — bay stays publishable without
|
|
3
4
|
* importing `@c9up/ream`. Any framework that exposes a Container with
|
|
@@ -17,44 +18,37 @@ export interface BayAppContext {
|
|
|
17
18
|
}
|
|
18
19
|
export interface BayProviderConfig {
|
|
19
20
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
*
|
|
21
|
+
* Which named store to use — a key of {@link stores}. Read from the
|
|
22
|
+
* environment in the generated config, so a deployment picks its queue
|
|
23
|
+
* backend without editing a file.
|
|
24
|
+
*/
|
|
25
|
+
default?: string;
|
|
26
|
+
/**
|
|
27
|
+
* The queue stores this application can use, by name. Each is a factory
|
|
28
|
+
* from `stores.*`, built only when it is the one selected.
|
|
29
|
+
*/
|
|
30
|
+
stores?: Record<string, QueueStoreFactory>;
|
|
31
|
+
/**
|
|
32
|
+
* The single-store form, kept for configs written against it: only
|
|
33
|
+
* `"memory"` was ever accepted. Prefer `default` + `stores`, which is how a
|
|
34
|
+
* pluggable backend is configured everywhere else and what lets the
|
|
35
|
+
* environment choose.
|
|
28
36
|
*/
|
|
29
37
|
driver?: "memory";
|
|
30
38
|
}
|
|
31
|
-
/**
|
|
32
|
-
* BayProvider — registers a default in-memory `QueueManager` in the
|
|
33
|
-
* host container so apps that don't need custom driver wiring can
|
|
34
|
-
* `import queue from '@c9up/bay/services/main'` and dispatch
|
|
35
|
-
* straight away. Job handlers are still registered manually via
|
|
36
|
-
* `queue.register(name, handler)` — that's intrinsic to the queue
|
|
37
|
-
* design (handlers are app-defined, not config-driven).
|
|
38
|
-
*
|
|
39
|
-
* Apps with non-trivial wiring (Redis driver, custom queue config)
|
|
40
|
-
* can ignore this provider and bind their own `QueueManager` instance
|
|
41
|
-
* in the container; the `services/main` proxy resolves whatever is
|
|
42
|
-
* registered.
|
|
43
|
-
*
|
|
44
|
-
* // reamrc.ts
|
|
45
|
-
* providers: [() => import('@c9up/bay/provider')]
|
|
46
|
-
*
|
|
47
|
-
* // start/queue.ts
|
|
48
|
-
* import queue from '@c9up/bay/services/main'
|
|
49
|
-
*
|
|
50
|
-
* queue.register('send-email', new SendEmailJob())
|
|
51
|
-
* await queue.dispatch('send-email', { to: 'user@example.com' })
|
|
52
|
-
*/
|
|
53
39
|
export default class BayProvider {
|
|
40
|
+
#private;
|
|
54
41
|
protected app: BayAppContext;
|
|
55
42
|
constructor(app: BayAppContext);
|
|
56
43
|
register(): void;
|
|
57
44
|
boot(): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Stop the worker the app started, and let the job in flight finish.
|
|
47
|
+
*
|
|
48
|
+
* A worker polls on a timer. Left running, it survives a dev reload, a test
|
|
49
|
+
* teardown and a SIGTERM — so the old process keeps pulling jobs the new
|
|
50
|
+
* one is also pulling, and the same job runs twice.
|
|
51
|
+
*/
|
|
58
52
|
shutdown(): Promise<void>;
|
|
59
53
|
}
|
|
60
54
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BayProvider.d.ts","sourceRoot":"","sources":["../src/BayProvider.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,UAAU,YAAY;IACrB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,GAAG,IAAI,CAAC;IACxD,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACjD;AACD,UAAU,cAAc;IACvB,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;CAC7C;AACD,MAAM,WAAW,aAAa;IAC7B,SAAS,EAAE,YAAY,CAAC;IACxB,MAAM,EAAE,cAAc,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IACjC
|
|
1
|
+
{"version":3,"file":"BayProvider.d.ts","sourceRoot":"","sources":["../src/BayProvider.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD;;;;;GAKG;AACH,UAAU,YAAY;IACrB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,GAAG,IAAI,CAAC;IACxD,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACjD;AACD,UAAU,cAAc;IACvB,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;CAC7C;AACD,MAAM,WAAW,aAAa;IAC7B,SAAS,EAAE,YAAY,CAAC;IACxB,MAAM,EAAE,cAAc,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IACjC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC3C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC;CAClB;AAoED,MAAM,CAAC,OAAO,OAAO,WAAW;;IACnB,SAAS,CAAC,GAAG,EAAE,aAAa;gBAAlB,GAAG,EAAE,aAAa;IAExC,QAAQ,IAAI,IAAI;IAaV,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B;;;;;;OAMG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAS/B"}
|
package/dist/BayProvider.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MemoryDriver } from "./drivers/MemoryDriver.js";
|
|
2
2
|
import { QueueManager } from "./QueueManager.js";
|
|
3
|
-
import { setQueue } from "./services/main.js";
|
|
3
|
+
import { clearQueue, getQueue, setQueue } from "./services/main.js";
|
|
4
4
|
/**
|
|
5
5
|
* BayProvider — registers a default in-memory `QueueManager` in the
|
|
6
6
|
* host container so apps that don't need custom driver wiring can
|
|
@@ -23,6 +23,40 @@ import { setQueue } from "./services/main.js";
|
|
|
23
23
|
* queue.register('send-email', new SendEmailJob())
|
|
24
24
|
* await queue.dispatch('send-email', { to: 'user@example.com' })
|
|
25
25
|
*/
|
|
26
|
+
/**
|
|
27
|
+
* The driver the config asks for.
|
|
28
|
+
*
|
|
29
|
+
* `default` + `stores` first — the form an environment variable can steer. The
|
|
30
|
+
* `driver` key is the single-store form kept for configs written against it.
|
|
31
|
+
* Naming a store that does not exist throws rather than falling back to memory:
|
|
32
|
+
* an application that meant to queue in Redis and silently got an in-process
|
|
33
|
+
* queue would only find out when a restart dropped every pending job.
|
|
34
|
+
*/
|
|
35
|
+
function buildDriver(config) {
|
|
36
|
+
const stores = config?.stores;
|
|
37
|
+
const name = config?.default;
|
|
38
|
+
if (stores && name !== undefined) {
|
|
39
|
+
const selected = stores[name];
|
|
40
|
+
if (!selected) {
|
|
41
|
+
const known = Object.keys(stores);
|
|
42
|
+
throw new Error(`[bay] config.queue names the store '${name}', which is not in \`stores\`. ` +
|
|
43
|
+
(known.length > 0
|
|
44
|
+
? `Declared: ${known.join(", ")}.`
|
|
45
|
+
: "`stores` is empty — declare one with stores.memory() or stores.redis()."));
|
|
46
|
+
}
|
|
47
|
+
return selected();
|
|
48
|
+
}
|
|
49
|
+
if (stores && name === undefined) {
|
|
50
|
+
throw new Error("[bay] config.queue declares `stores` but no `default` naming which one to use. " +
|
|
51
|
+
`Set default to one of: ${Object.keys(stores).join(", ")}.`);
|
|
52
|
+
}
|
|
53
|
+
const driverName = config?.driver ?? "memory";
|
|
54
|
+
if (driverName !== "memory") {
|
|
55
|
+
throw new Error(`[bay] Unsupported driver '${driverName}' — name it under \`stores\` instead: ` +
|
|
56
|
+
"stores: { redis: stores.redis({ connection: 'main' }) }.");
|
|
57
|
+
}
|
|
58
|
+
return new MemoryDriver();
|
|
59
|
+
}
|
|
26
60
|
export default class BayProvider {
|
|
27
61
|
app;
|
|
28
62
|
constructor(app) {
|
|
@@ -31,20 +65,35 @@ export default class BayProvider {
|
|
|
31
65
|
register() {
|
|
32
66
|
this.app.container.singleton(QueueManager, () => {
|
|
33
67
|
const config = this.app.config.get("queue");
|
|
34
|
-
|
|
35
|
-
if (driverName !== "memory") {
|
|
36
|
-
throw new Error(`[bay] Unsupported driver '${driverName}' for default provider — ` +
|
|
37
|
-
"wire QueueManager yourself in start/queue.ts for non-memory drivers.");
|
|
38
|
-
}
|
|
39
|
-
return new QueueManager(new MemoryDriver());
|
|
68
|
+
return new QueueManager(buildDriver(config));
|
|
40
69
|
});
|
|
41
70
|
this.app.container.singleton("queue", () => this.app.container.resolve(QueueManager));
|
|
42
71
|
}
|
|
72
|
+
/** The queue THIS provider booted — not whatever the module singleton holds. */
|
|
73
|
+
#queue;
|
|
43
74
|
async boot() {
|
|
44
75
|
// Populate the `@c9up/bay/services/main` singleton so apps can
|
|
45
76
|
// `import queue from '@c9up/bay/services/main'` from anywhere.
|
|
46
|
-
|
|
77
|
+
this.#queue = await this.app.container.resolve(QueueManager);
|
|
78
|
+
setQueue(this.#queue);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Stop the worker the app started, and let the job in flight finish.
|
|
82
|
+
*
|
|
83
|
+
* A worker polls on a timer. Left running, it survives a dev reload, a test
|
|
84
|
+
* teardown and a SIGTERM — so the old process keeps pulling jobs the new
|
|
85
|
+
* one is also pulling, and the same job runs twice.
|
|
86
|
+
*/
|
|
87
|
+
async shutdown() {
|
|
88
|
+
if (!this.#queue)
|
|
89
|
+
return;
|
|
90
|
+
await this.#queue.stop();
|
|
91
|
+
// Two applications can share a process — parallel tests, a hot reload.
|
|
92
|
+
// The module singleton holds whichever booted last, so it is only ours
|
|
93
|
+
// to clear while it still points at the queue this provider booted.
|
|
94
|
+
if (getQueue() === this.#queue)
|
|
95
|
+
clearQueue();
|
|
96
|
+
this.#queue = undefined;
|
|
47
97
|
}
|
|
48
|
-
async shutdown() { }
|
|
49
98
|
}
|
|
50
99
|
//# sourceMappingURL=BayProvider.js.map
|
package/dist/BayProvider.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BayProvider.js","sourceRoot":"","sources":["../src/BayProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"BayProvider.js","sourceRoot":"","sources":["../src/BayProvider.ts"],"names":[],"mappings":"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"}
|
package/dist/QueueManager.d.ts
CHANGED
|
@@ -62,7 +62,13 @@ export declare class QueueManager {
|
|
|
62
62
|
recoverStale(): Promise<number>;
|
|
63
63
|
/** Await the currently in-flight processOne, if any. */
|
|
64
64
|
drain(): Promise<void>;
|
|
65
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Stop the worker and wait for it to actually be gone.
|
|
67
|
+
*
|
|
68
|
+
* Awaits the LOOP, not just the job in flight: a stop that returns while
|
|
69
|
+
* the loop is still sleeping leaves a worker running past the teardown that
|
|
70
|
+
* asked it to stop.
|
|
71
|
+
*/
|
|
66
72
|
stop(): Promise<void>;
|
|
67
73
|
/** Get failed jobs. */
|
|
68
74
|
failedJobs(): Promise<Job[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QueueManager.d.ts","sourceRoot":"","sources":["../src/QueueManager.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,GAAG;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,CAAC;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAC1B,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,WAAW;IAC3B,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,GAAG,IAAI,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IAC3B,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACzB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACxB;;;;OAIG;IACH,YAAY,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACjC;AAED,qBAAa,YAAY;;
|
|
1
|
+
{"version":3,"file":"QueueManager.d.ts","sourceRoot":"","sources":["../src/QueueManager.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,GAAG;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,CAAC;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAC1B,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,WAAW;IAC3B,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,GAAG,IAAI,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IAC3B,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACzB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACxB;;;;OAIG;IACH,YAAY,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACjC;AAED,qBAAa,YAAY;;gBAUZ,MAAM,EAAE,WAAW;IAI/B,8BAA8B;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,UAAU,CAAC,GAAG,IAAI;IAI1E,mCAAmC;IAC7B,QAAQ,CACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAChC,OAAO,CAAC,MAAM,CAAC;IAkBlB,yCAAyC;IACnC,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IA2CpC;;;;;OAKG;IACG,IAAI,CAAC,cAAc,SAAO,EAAE,cAAc,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IA6EzE;;;;;OAKG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAIrC,wDAAwD;IAClD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5B;;;;;;OAMG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B,uBAAuB;IACjB,UAAU,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAIlC,sBAAsB;IAChB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CAG7B"}
|
package/dist/QueueManager.js
CHANGED
|
@@ -10,6 +10,10 @@ export class QueueManager {
|
|
|
10
10
|
#driver;
|
|
11
11
|
#handlers = new Map();
|
|
12
12
|
#running = false;
|
|
13
|
+
/** The running loop, so `stop()` can wait for it to finish. */
|
|
14
|
+
#loopPromise;
|
|
15
|
+
/** Cuts the sleep between polls short. */
|
|
16
|
+
#wake;
|
|
13
17
|
#inflightPromise = null;
|
|
14
18
|
constructor(driver) {
|
|
15
19
|
this.#driver = driver;
|
|
@@ -89,19 +93,36 @@ export class QueueManager {
|
|
|
89
93
|
throw new Error("QueueManager is already running");
|
|
90
94
|
}
|
|
91
95
|
this.#running = true;
|
|
96
|
+
const loop = this.#loop(pollIntervalMs, recoverStaleMs);
|
|
97
|
+
this.#loopPromise = loop;
|
|
98
|
+
try {
|
|
99
|
+
await loop;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
this.#loopPromise = undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The polling loop itself.
|
|
107
|
+
*
|
|
108
|
+
* Between jobs it sleeps, and that sleep is CANCELLABLE: `stop()` wakes it
|
|
109
|
+
* rather than waiting out the interval. Without that, stopping returned
|
|
110
|
+
* while the loop was still pending — up to a full poll interval of a worker
|
|
111
|
+
* that was supposed to be gone, and a timer holding the process open.
|
|
112
|
+
*/
|
|
113
|
+
async #loop(pollIntervalMs, recoverStaleMs) {
|
|
92
114
|
await this.#tryRecoverStale();
|
|
93
115
|
let lastRecover = Date.now();
|
|
94
116
|
while (this.#running) {
|
|
95
117
|
try {
|
|
96
118
|
this.#inflightPromise = this.processOne();
|
|
97
119
|
const processed = await this.#inflightPromise;
|
|
98
|
-
if (!processed)
|
|
99
|
-
await
|
|
100
|
-
}
|
|
120
|
+
if (!processed)
|
|
121
|
+
await this.#sleep(pollIntervalMs);
|
|
101
122
|
}
|
|
102
123
|
catch (err) {
|
|
103
124
|
process.stderr.write(`QueueManager processOne error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
104
|
-
await
|
|
125
|
+
await this.#sleep(pollIntervalMs);
|
|
105
126
|
}
|
|
106
127
|
finally {
|
|
107
128
|
this.#inflightPromise = null;
|
|
@@ -112,6 +133,20 @@ export class QueueManager {
|
|
|
112
133
|
}
|
|
113
134
|
}
|
|
114
135
|
}
|
|
136
|
+
/** Wait, unless `stop()` says otherwise first. */
|
|
137
|
+
#sleep(ms) {
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
const timer = setTimeout(() => {
|
|
140
|
+
this.#wake = undefined;
|
|
141
|
+
resolve();
|
|
142
|
+
}, ms);
|
|
143
|
+
this.#wake = () => {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
this.#wake = undefined;
|
|
146
|
+
resolve();
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
115
150
|
/** recoverStale() wrapper that swallows driver errors — used by the work loop. */
|
|
116
151
|
async #tryRecoverStale() {
|
|
117
152
|
try {
|
|
@@ -136,10 +171,19 @@ export class QueueManager {
|
|
|
136
171
|
await this.#inflightPromise.catch(() => { });
|
|
137
172
|
}
|
|
138
173
|
}
|
|
139
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* Stop the worker and wait for it to actually be gone.
|
|
176
|
+
*
|
|
177
|
+
* Awaits the LOOP, not just the job in flight: a stop that returns while
|
|
178
|
+
* the loop is still sleeping leaves a worker running past the teardown that
|
|
179
|
+
* asked it to stop.
|
|
180
|
+
*/
|
|
140
181
|
async stop() {
|
|
141
182
|
this.#running = false;
|
|
183
|
+
this.#wake?.();
|
|
142
184
|
await this.drain();
|
|
185
|
+
if (this.#loopPromise)
|
|
186
|
+
await this.#loopPromise.catch(() => { });
|
|
143
187
|
}
|
|
144
188
|
/** Get failed jobs. */
|
|
145
189
|
async failedJobs() {
|
package/dist/QueueManager.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QueueManager.js","sourceRoot":"","sources":["../src/QueueManager.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"QueueManager.js","sourceRoot":"","sources":["../src/QueueManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ream configure @c9up/bay` — wire the job queue in one command.
|
|
3
|
+
*
|
|
4
|
+
* The provider alone is not enough: it reads `config/queue.ts`, and a package
|
|
5
|
+
* registered without one falls back to a default that is rarely the one an
|
|
6
|
+
* application wants. Writing both together is what makes `ream add` mean
|
|
7
|
+
* installed AND working.
|
|
8
|
+
*/
|
|
9
|
+
interface Codemods {
|
|
10
|
+
addProvider(importPath: string): Promise<void>;
|
|
11
|
+
addEnvVars(vars: Record<string, string>): Promise<void>;
|
|
12
|
+
writeFile(filePath: string, content: string, options?: {
|
|
13
|
+
force?: boolean;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export declare function configure(codemods: Codemods): Promise<void>;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=configure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configure.d.ts","sourceRoot":"","sources":["../src/configure.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,UAAU,QAAQ;IACjB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,SAAS,CACR,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAC3B,OAAO,CAAC,IAAI,CAAC,CAAC;CACjB;AAED,wBAAsB,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBjE"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ream configure @c9up/bay` — wire the job queue in one command.
|
|
3
|
+
*
|
|
4
|
+
* The provider alone is not enough: it reads `config/queue.ts`, and a package
|
|
5
|
+
* registered without one falls back to a default that is rarely the one an
|
|
6
|
+
* application wants. Writing both together is what makes `ream add` mean
|
|
7
|
+
* installed AND working.
|
|
8
|
+
*/
|
|
9
|
+
export async function configure(codemods) {
|
|
10
|
+
// The config below reads these, so they are declared here. Writing the file
|
|
11
|
+
// without them leaves an application whose config asks the environment for
|
|
12
|
+
// something nothing ever put there.
|
|
13
|
+
await codemods.addEnvVars({
|
|
14
|
+
QUEUE_STORE: "memory",
|
|
15
|
+
});
|
|
16
|
+
await codemods.addProvider("@c9up/bay/provider");
|
|
17
|
+
await codemods.writeFile("config/queue.ts", `import { defineConfig, stores } from '@c9up/bay'
|
|
18
|
+
import env from '#start/env'
|
|
19
|
+
|
|
20
|
+
export default defineConfig({
|
|
21
|
+
// Which store to run on. Memory forgets everything on restart, which is
|
|
22
|
+
// what a single process in development wants and nothing else does.
|
|
23
|
+
default: env.get('QUEUE_STORE', 'memory'),
|
|
24
|
+
|
|
25
|
+
stores: {
|
|
26
|
+
memory: stores.memory(),
|
|
27
|
+
redis: stores.redis({ connection: 'main' }),
|
|
28
|
+
},
|
|
29
|
+
})`);
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=configure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configure.js","sourceRoot":"","sources":["../src/configure.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAYH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,QAAkB;IACjD,4EAA4E;IAC5E,2EAA2E;IAC3E,oCAAoC;IACpC,MAAM,QAAQ,CAAC,UAAU,CAAC;QACzB,WAAW,EAAE,QAAQ;KACrB,CAAC,CAAC;IAEH,MAAM,QAAQ,CAAC,WAAW,CAAC,oBAAoB,CAAC,CAAC;IACjD,MAAM,QAAQ,CAAC,SAAS,CACvB,iBAAiB,EACjB;;;;;;;;;;;;GAYC,CACD,CAAC;AACH,CAAC"}
|
|
@@ -41,6 +41,12 @@ export declare class RedisDriver implements QueueDriver {
|
|
|
41
41
|
constructor(source: RedisClientSource, options?: {
|
|
42
42
|
prefix?: string;
|
|
43
43
|
visibilityTimeoutMs?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Accept the non-atomic pop on a Redis older than 6.2, in
|
|
46
|
+
* production. Off by default: losing an accepted job is a choice a
|
|
47
|
+
* deployment makes, not one a version check makes for it.
|
|
48
|
+
*/
|
|
49
|
+
allowNonAtomicPop?: boolean;
|
|
44
50
|
});
|
|
45
51
|
push(job: Job): Promise<void>;
|
|
46
52
|
pop(): Promise<Job | null>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RedisDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/RedisDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;
|
|
1
|
+
{"version":3,"file":"RedisDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/RedisDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAE3D,MAAM,WAAW,WAAW;IAC3B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,KAAK,CAAC,CACL,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,GAAG,OAAO,EACtB,EAAE,EAAE,MAAM,GAAG,OAAO,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1B,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACpE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC3E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACzC;AAcD;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAC1B,WAAW,GACX,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;AAmD9C,qBAAa,WAAY,YAAW,WAAW;;gBAsC7C,MAAM,EAAE,iBAAiB,EACzB,OAAO,CAAC,EAAE;QACT,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B;;;;WAIG;QACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;KAC5B;IAkCI,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7B,GAAG,IAAI,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC;IAkD1B,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAMjC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS5C,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ9B,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IA8D/B,MAAM,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAexB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CAI7B"}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* { PX: ms }) does NOT satisfy this interface and would drop the lease TTL — it
|
|
18
18
|
* needs a thin adapter.
|
|
19
19
|
*/
|
|
20
|
+
import { inProduction } from "../nodeEnv.js";
|
|
20
21
|
function isValidJob(obj) {
|
|
21
22
|
if (typeof obj !== "object" || obj === null)
|
|
22
23
|
return false;
|
|
@@ -33,13 +34,32 @@ function isValidJob(obj) {
|
|
|
33
34
|
* has no client to inspect yet.
|
|
34
35
|
*/
|
|
35
36
|
const warned = new WeakSet();
|
|
36
|
-
function
|
|
37
|
-
if (typeof client.lmove === "function"
|
|
37
|
+
function checkLmove(client, allowNonAtomicPop) {
|
|
38
|
+
if (typeof client.lmove === "function")
|
|
39
|
+
return;
|
|
40
|
+
// A queue's whole promise is that a job it accepted gets run. Without LMOVE
|
|
41
|
+
// the pop is `lpop` then `rpush`, and a crash between the two deletes the
|
|
42
|
+
// job from pending before it reaches processing: nothing recovers it,
|
|
43
|
+
// because nothing knows it existed. That is a different product, and in
|
|
44
|
+
// production it must be asked for rather than fallen into.
|
|
45
|
+
if (inProduction() && !allowNonAtomicPop) {
|
|
46
|
+
throw new Error("[bay] this Redis client has no LMOVE (Redis < 6.2), so pop() would be a non-atomic lpop+rpush — " +
|
|
47
|
+
"a crash between the two loses the in-flight job, turning at-least-once delivery into at-most-once.\n" +
|
|
48
|
+
" Upgrade to Redis 6.2 or later, or pass `allowNonAtomicPop: true` to state that losing a job is acceptable here.");
|
|
49
|
+
}
|
|
50
|
+
if (warned.has(client))
|
|
38
51
|
return;
|
|
39
52
|
warned.add(client);
|
|
53
|
+
// Said even when the deployment opted in: agreeing to lose a job once, in a
|
|
54
|
+
// config file, is not the same as being reminded that this process is
|
|
55
|
+
// running that way. The line has to be in the logs of the incident.
|
|
56
|
+
const optedIn = inProduction() && allowNonAtomicPop;
|
|
40
57
|
console.warn("[bay] RedisDriver: client lacks LMOVE (Redis <6.2). pop() falls back to " +
|
|
41
58
|
"a non-atomic lpop+rpush, downgrading delivery from at-least-once to " +
|
|
42
|
-
"at-most-once — a crash between the two commands loses the in-flight job."
|
|
59
|
+
"at-most-once — a crash between the two commands loses the in-flight job." +
|
|
60
|
+
(optedIn
|
|
61
|
+
? "\n Running this way in PRODUCTION because allowNonAtomicPop was set."
|
|
62
|
+
: ""));
|
|
43
63
|
}
|
|
44
64
|
/**
|
|
45
65
|
* A key prefix that ends in a separator.
|
|
@@ -67,7 +87,7 @@ export class RedisDriver {
|
|
|
67
87
|
return this.#resolved;
|
|
68
88
|
if (typeof this.#source !== "function") {
|
|
69
89
|
this.#resolved = this.#source;
|
|
70
|
-
|
|
90
|
+
checkLmove(this.#resolved, this.#allowNonAtomicPop);
|
|
71
91
|
return this.#resolved;
|
|
72
92
|
}
|
|
73
93
|
if (!this.#pending) {
|
|
@@ -75,7 +95,7 @@ export class RedisDriver {
|
|
|
75
95
|
this.#pending = Promise.resolve(resolver())
|
|
76
96
|
.then((client) => {
|
|
77
97
|
this.#resolved = client;
|
|
78
|
-
|
|
98
|
+
checkLmove(client, this.#allowNonAtomicPop);
|
|
79
99
|
return client;
|
|
80
100
|
})
|
|
81
101
|
// Cleared on failure too. Clearing only on success left the
|
|
@@ -93,13 +113,15 @@ export class RedisDriver {
|
|
|
93
113
|
// A client handed in directly can be checked now, so the warning keeps
|
|
94
114
|
// landing at construction as it always did. A named connection has no
|
|
95
115
|
// client yet — it is checked when the connection resolves.
|
|
96
|
-
if (typeof source !== "function")
|
|
97
|
-
|
|
116
|
+
if (typeof source !== "function") {
|
|
117
|
+
checkLmove(source, options?.allowNonAtomicPop ?? false);
|
|
118
|
+
}
|
|
98
119
|
// Normalised rather than documented: every key is built by concatenation
|
|
99
120
|
// (`${prefix}pending`), so a prefix without a trailing separator yields
|
|
100
121
|
// "myapppending" — unreadable, and able to collide with a neighbouring
|
|
101
122
|
// prefix. Nothing warned, because nothing failed.
|
|
102
123
|
this.#prefix = withSeparator(options?.prefix ?? "queue:");
|
|
124
|
+
this.#allowNonAtomicPop = options?.allowNonAtomicPop ?? false;
|
|
103
125
|
const visibilityTimeout = options?.visibilityTimeoutMs ?? 30_000;
|
|
104
126
|
// A non-positive / non-integer timeout makes pop()'s `SET … PX <ms>` fail
|
|
105
127
|
// on a real Redis; the catch then removes the job from `processing` and
|
|
@@ -112,6 +134,7 @@ export class RedisDriver {
|
|
|
112
134
|
}
|
|
113
135
|
#pendingKey = () => `${this.#prefix}pending`;
|
|
114
136
|
#processingKey = () => `${this.#prefix}processing`;
|
|
137
|
+
#allowNonAtomicPop = false;
|
|
115
138
|
#failedKey = () => `${this.#prefix}failed`;
|
|
116
139
|
#leaseKey = (jobId) => `${this.#prefix}lease:${jobId}`;
|
|
117
140
|
async push(job) {
|
|
@@ -131,22 +154,31 @@ export class RedisDriver {
|
|
|
131
154
|
}
|
|
132
155
|
if (!raw)
|
|
133
156
|
return null;
|
|
157
|
+
// Only a payload that can never be run is purged. Everything past this
|
|
158
|
+
// point is a REAL job that already sits in `processing`, and deleting
|
|
159
|
+
// it there is the one thing that loses it for good: it is gone from
|
|
160
|
+
// pending too, and recoverStale() scans processing, so nothing would
|
|
161
|
+
// ever find it again.
|
|
162
|
+
let parsed;
|
|
134
163
|
try {
|
|
135
|
-
|
|
136
|
-
if (!isValidJob(parsed)) {
|
|
137
|
-
// Malformed payload — purge from `processing` so it can't sit
|
|
138
|
-
// there indefinitely as a poison pill. recoverStale() also
|
|
139
|
-
// catches survivors but pop()'s own move is the primary path.
|
|
140
|
-
await client.lrem(this.#processingKey(), 1, raw);
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
await client.set(this.#leaseKey(parsed.id), raw, "PX", String(this.#visibilityTimeout));
|
|
144
|
-
return parsed;
|
|
164
|
+
parsed = JSON.parse(raw);
|
|
145
165
|
}
|
|
146
166
|
catch {
|
|
167
|
+
// A poison pill: unparseable, and it would sit in processing
|
|
168
|
+
// forever blocking nothing but wasting every recovery pass.
|
|
169
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
if (!isValidJob(parsed)) {
|
|
147
173
|
await client.lrem(this.#processingKey(), 1, raw);
|
|
148
174
|
return null;
|
|
149
175
|
}
|
|
176
|
+
// A lease that cannot be written is a transient Redis failure, not a
|
|
177
|
+
// bad job. The error propagates and the job STAYS in processing with
|
|
178
|
+
// no lease, which is precisely the state recoverStale() puts back in
|
|
179
|
+
// pending — so the delivery guarantee survives the blip.
|
|
180
|
+
await client.set(this.#leaseKey(parsed.id), raw, "PX", String(this.#visibilityTimeout));
|
|
181
|
+
return parsed;
|
|
150
182
|
}
|
|
151
183
|
async complete(job) {
|
|
152
184
|
const client = await this.#client();
|