@rvoh/psychic-workers 2.6.0 → 2.7.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/dist/cjs/src/background/BaseBackgroundedModel.js +49 -25
- package/dist/cjs/src/background/BaseBackgroundedService.js +24 -12
- package/dist/cjs/src/background/BaseScheduledService.js +1 -1
- package/dist/cjs/src/background/index.js +383 -98
- package/dist/cjs/src/error/background/DebouncedJobOutpacesRateLimit.js +64 -0
- package/dist/cjs/src/error/background/DebouncedJobRequiresMinimumDelay.js +52 -0
- package/dist/cjs/src/error/background/NamedWorkstreamRateLimitMissingMaxOrDuration.js +31 -0
- package/dist/cjs/src/error/background/RateLimitedPsychicJob.js +82 -0
- package/dist/cjs/src/error/background/RateLimitedPsychicJobThrownFromWorkerWithoutLimiter.js +51 -0
- package/dist/cjs/src/package-exports/errors.js +7 -0
- package/dist/cjs/src/test-utils/WorkerTestUtils.js +78 -13
- package/dist/esm/src/background/BaseBackgroundedModel.js +49 -25
- package/dist/esm/src/background/BaseBackgroundedService.js +24 -12
- package/dist/esm/src/background/BaseScheduledService.js +1 -1
- package/dist/esm/src/background/index.js +383 -98
- package/dist/esm/src/error/background/DebouncedJobOutpacesRateLimit.js +64 -0
- package/dist/esm/src/error/background/DebouncedJobRequiresMinimumDelay.js +52 -0
- package/dist/esm/src/error/background/NamedWorkstreamRateLimitMissingMaxOrDuration.js +31 -0
- package/dist/esm/src/error/background/RateLimitedPsychicJob.js +82 -0
- package/dist/esm/src/error/background/RateLimitedPsychicJobThrownFromWorkerWithoutLimiter.js +51 -0
- package/dist/esm/src/package-exports/errors.js +7 -0
- package/dist/esm/src/test-utils/WorkerTestUtils.js +78 -13
- package/dist/types/src/background/BaseBackgroundedModel.d.ts +49 -25
- package/dist/types/src/background/BaseBackgroundedService.d.ts +24 -12
- package/dist/types/src/background/BaseScheduledService.d.ts +1 -1
- package/dist/types/src/background/index.d.ts +109 -5
- package/dist/types/src/error/background/DebouncedJobOutpacesRateLimit.d.ts +31 -0
- package/dist/types/src/error/background/DebouncedJobRequiresMinimumDelay.d.ts +23 -0
- package/dist/types/src/error/background/NamedWorkstreamRateLimitMissingMaxOrDuration.d.ts +16 -0
- package/dist/types/src/error/background/RateLimitedPsychicJob.d.ts +73 -0
- package/dist/types/src/error/background/RateLimitedPsychicJobThrownFromWorkerWithoutLimiter.d.ts +16 -0
- package/dist/types/src/package-exports/errors.d.ts +1 -0
- package/dist/types/src/psychic-app-workers/index.d.ts +102 -53
- package/dist/types/src/test-utils/WorkerTestUtils.d.ts +32 -1
- package/dist/types/src/types/background.d.ts +88 -6
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@ export default class BaseBackgroundedService {
|
|
|
11
11
|
* @returns {object} config - the background job config
|
|
12
12
|
* @returns {string} config.priority - 'default' | 'urgent' | 'not_urgent' | 'last'
|
|
13
13
|
* @returns {string} config.workstream - a workstream name. This would be the name of a workstream, as defined in conf/workers.ts
|
|
14
|
-
* @returns {string} config.
|
|
14
|
+
* @returns {string} config.queue - the name of the BullMQ queue you wish to connect to. This can only be provided if workstream is not provided.
|
|
15
15
|
* @returns {string} config.groupId - the groupId of the BullMQ queue you wish to connect to. This can only be provided if workstream is not provided.
|
|
16
16
|
*/
|
|
17
17
|
static get backgroundJobConfig(): BackgroundJobConfig<BaseBackgroundedService>;
|
|
@@ -53,16 +53,22 @@ export default class BaseBackgroundedService {
|
|
|
53
53
|
static background<T, MethodName extends PsychicBackgroundedServiceStaticMethods<T & typeof BaseBackgroundedService>, MethodFunc extends T[MethodName & keyof T], MethodArgs extends BackgroundableMethodArgs<MethodFunc>>(this: T, methodName: MethodName, ...args: MethodArgs): Promise<void>;
|
|
54
54
|
/**
|
|
55
55
|
* runs the specified method in a background queue, driven by BullMQ,
|
|
56
|
-
* sending in the provided args,
|
|
57
|
-
*
|
|
58
|
-
* it is entered into the queue.
|
|
56
|
+
* sending in the provided args, along with a delay object — one or more of
|
|
57
|
+
* `{ seconds, minutes, hours, days }` — which holds the job off for that much
|
|
58
|
+
* time after it is entered into the queue.
|
|
59
|
+
*
|
|
60
|
+
* The delay object also accepts an optional `jobId`, which turns the delay
|
|
61
|
+
* into a **debounce**: repeated calls carrying the same `jobId` collapse into
|
|
62
|
+
* a single execution, which runs once the delay has elapsed without another
|
|
63
|
+
* call arriving. `jobId` is a deduplication key rather than a BullMQ job id.
|
|
64
|
+
*
|
|
65
|
+
* See {@link DelayedJobOpts.jobId}.
|
|
59
66
|
*
|
|
60
67
|
* ```ts
|
|
61
|
-
* await MyBackgroundableClass.backgroundWithDelay('myMethod', 'abc', 123)
|
|
68
|
+
* await MyBackgroundableClass.backgroundWithDelay({ minutes: 5 }, 'myMethod', 'abc', 123)
|
|
62
69
|
* ```
|
|
63
|
-
* though calling
|
|
64
|
-
* is an indication that
|
|
65
|
-
* completed.
|
|
70
|
+
* though calling backgroundWithDelay must be awaited, the resolution of the promise
|
|
71
|
+
* is an indication that a run is pending, not that it has completed.
|
|
66
72
|
*
|
|
67
73
|
* NOTE: in test environments, psychic will immediately invoke the underlying
|
|
68
74
|
* method, preventing you from needing to explicitly wait for queues to flush
|
|
@@ -70,7 +76,7 @@ export default class BaseBackgroundedService {
|
|
|
70
76
|
*
|
|
71
77
|
* @deprecated use `backgroundWith({ delay }, methodName, ...args)` instead. This method will be removed in a future major version.
|
|
72
78
|
*
|
|
73
|
-
* @param
|
|
79
|
+
* @param delay - how long you want to hold off before allowing the job to run, given as at least one of `seconds`, `minutes`, `hours` or `days`, plus an optional `jobId` (a deduplication key) which debounces repeated calls into a single run
|
|
74
80
|
* @param methodName - the name of the static method you wish to run in the background
|
|
75
81
|
* @param args - a variadic list of arguments to be sent to your method
|
|
76
82
|
*/
|
|
@@ -81,6 +87,13 @@ export default class BaseBackgroundedService {
|
|
|
81
87
|
* be used to delay the job and/or override the priority provided by
|
|
82
88
|
* the `backgroundJobConfig` getter on this service.
|
|
83
89
|
*
|
|
90
|
+
* Adding a `jobId` to the delay turns it into a **debounce**: repeated calls
|
|
91
|
+
* carrying the same `jobId` collapse into a single execution, which runs once
|
|
92
|
+
* the delay has elapsed without another call arriving. `jobId` is a
|
|
93
|
+
* deduplication key rather than a BullMQ job id.
|
|
94
|
+
*
|
|
95
|
+
* See {@link DelayedJobOpts.jobId}.
|
|
96
|
+
*
|
|
84
97
|
* ```ts
|
|
85
98
|
* await MyBackgroundableClass.backgroundWith(
|
|
86
99
|
* { delay: { seconds: 30, jobId: 'my-unique-job-id' }, priority: 'urgent' },
|
|
@@ -90,15 +103,14 @@ export default class BaseBackgroundedService {
|
|
|
90
103
|
* )
|
|
91
104
|
* ```
|
|
92
105
|
* though calling backgroundWith must be awaited, the resolution of the promise
|
|
93
|
-
* is an indication that
|
|
94
|
-
* completed.
|
|
106
|
+
* is an indication that a run is pending, not that it has completed.
|
|
95
107
|
*
|
|
96
108
|
* NOTE: in test environments, psychic will immediately invoke the underlying
|
|
97
109
|
* method, preventing you from needing to explicitly wait for queues to flush
|
|
98
110
|
* before making assertions.
|
|
99
111
|
*
|
|
100
112
|
* @param opts - options for backgrounding this job
|
|
101
|
-
* @param opts.delay - (optional)
|
|
113
|
+
* @param opts.delay - (optional) how long you want to hold off before allowing the job to run, given as at least one of `seconds`, `minutes`, `hours` or `days`, plus an optional `jobId` (a deduplication key) which debounces repeated calls into a single run
|
|
102
114
|
* @param opts.priority - (optional) a priority which, when provided, overrides the priority provided by `backgroundJobConfig`
|
|
103
115
|
* @param methodName - the name of the static method you wish to run in the background
|
|
104
116
|
* @param args - a variadic list of arguments to be sent to your method
|
|
@@ -10,7 +10,7 @@ export default class BaseScheduledService {
|
|
|
10
10
|
* @returns {object} config - the background job config
|
|
11
11
|
* @returns {string} config.priority - 'default' | 'urgent' | 'not_urgent' | 'last'
|
|
12
12
|
* @returns {string} config.workstream - a workstream name. This would be the name of a workstream, as defined in conf/workers.ts
|
|
13
|
-
* @returns {string} config.
|
|
13
|
+
* @returns {string} config.queue - the name of the BullMQ queue you wish to connect to. This can only be provided if workstream is not provided.
|
|
14
14
|
* @returns {string} config.groupId - the groupId of the BullMQ queue you wish to connect to. This can only be provided if workstream is not provided.
|
|
15
15
|
*/
|
|
16
16
|
static get backgroundJobConfig(): BackgroundJobConfig<BaseScheduledService>;
|
|
@@ -7,7 +7,13 @@ import { BackgroundJobConfig, BackgroundJobData, BackgroundQueuePriority, JobTyp
|
|
|
7
7
|
*/
|
|
8
8
|
export declare class Background {
|
|
9
9
|
/**
|
|
10
|
-
* returns the default queue
|
|
10
|
+
* returns the **logical** name of your app's default queue, built from your
|
|
11
|
+
* app name (`MyAppBackgroundJobQueue`).
|
|
12
|
+
*
|
|
13
|
+
* This is never the name the queue has in Redis: `connect` rewrites it per
|
|
14
|
+
* connection (brace-stripped, cluster hash-tagged, test-suffixed), and every
|
|
15
|
+
* key BullMQ writes is namespaced by the rewritten name. Do not compose Redis
|
|
16
|
+
* keys from this — read `queue.name` off `background.queues` instead.
|
|
11
17
|
*/
|
|
12
18
|
static get defaultQueueName(): string;
|
|
13
19
|
/**
|
|
@@ -75,17 +81,61 @@ export declare class Background {
|
|
|
75
81
|
*/
|
|
76
82
|
private redisConnections;
|
|
77
83
|
/**
|
|
78
|
-
*
|
|
84
|
+
* @internal
|
|
85
|
+
*
|
|
86
|
+
* For every queue built by `connect`: how it was configured, whether its
|
|
87
|
+
* workers carry a BullMQ `limiter`, and what the workers are built from.
|
|
88
|
+
* Written whether or not workers are activated here, so activation can happen
|
|
89
|
+
* later without re-walking the configuration. Keyed by Queue identity rather
|
|
90
|
+
* than name: a transitional workstream's queue has the same formatted name as
|
|
91
|
+
* its current twin but its own configuration.
|
|
92
|
+
*/
|
|
93
|
+
private queueWorkerRecords;
|
|
94
|
+
/**
|
|
95
|
+
* @internal
|
|
96
|
+
*
|
|
97
|
+
* set immediately before {@link buildRecordedWorkers} runs its build loop, so
|
|
98
|
+
* that a second activation builds nothing
|
|
99
|
+
*/
|
|
100
|
+
private workersActivated;
|
|
101
|
+
/**
|
|
102
|
+
* Establishes connection to BullMQ via redis: builds the `Queue` objects for
|
|
103
|
+
* the default and named workstreams and, only when `activateWorkers` is true,
|
|
104
|
+
* the `Worker` objects that run jobs off them. Synchronous: it returns
|
|
105
|
+
* nothing, so it is called without `await` even from async code.
|
|
106
|
+
*
|
|
107
|
+
* Connecting and activating are separate. `activateWorkers` defaults to
|
|
108
|
+
* `false`, so connecting gets the producer side only. Queues are built once
|
|
109
|
+
* per instance, but a later activation still builds workers, so a process that
|
|
110
|
+
* connects first and calls `work()` afterwards gets its workers.
|
|
111
|
+
*
|
|
112
|
+
* You rarely call this yourself — Psychic connects on the
|
|
113
|
+
* `server:init:after-routes` hook, on every enqueue path, in CLI codegen and
|
|
114
|
+
* in the `WorkerTestUtils` helpers.
|
|
79
115
|
*/
|
|
80
116
|
connect({ activateWorkers, }?: {
|
|
81
117
|
activateWorkers?: boolean;
|
|
82
118
|
}): void;
|
|
83
119
|
/**
|
|
84
|
-
* Returns all the queues in your application
|
|
120
|
+
* Returns all the queues in your application: the default queue, every named
|
|
121
|
+
* queue, and their transitional twins when transitional workstreams are
|
|
122
|
+
* configured.
|
|
123
|
+
*
|
|
124
|
+
* `connect` is what populates this, and before it has run the getter returns
|
|
125
|
+
* an **empty array**, with no error and no warning — indistinguishable from
|
|
126
|
+
* an application with nothing queued. Each `Queue` here carries the rewritten
|
|
127
|
+
* Redis name rather than the name you configured.
|
|
85
128
|
*/
|
|
86
129
|
get queues(): Queue[];
|
|
87
130
|
/**
|
|
88
|
-
* Returns all the workers in your application
|
|
131
|
+
* Returns all the workers in your application: every BullMQ `Worker` this
|
|
132
|
+
* process has built.
|
|
133
|
+
*
|
|
134
|
+
* Only a process that activated workers has any — `work()`, or a direct
|
|
135
|
+
* `connect({ activateWorkers: true })`. Anywhere else the getter returns an
|
|
136
|
+
* **empty array**, with no error and no warning, so a worker entrypoint that
|
|
137
|
+
* attaches failure listeners by looping over it before `work()` runs attaches
|
|
138
|
+
* nothing and logs nothing to say so.
|
|
89
139
|
*/
|
|
90
140
|
get workers(): Worker<any, any, string>[];
|
|
91
141
|
/**
|
|
@@ -141,7 +191,61 @@ export declare class Background {
|
|
|
141
191
|
*/
|
|
142
192
|
private nativeBullMQConnect;
|
|
143
193
|
/**
|
|
144
|
-
*
|
|
194
|
+
* @internal
|
|
195
|
+
*
|
|
196
|
+
* the sole writer of {@link queueWorkerRecords}. `workerOptions` is the very
|
|
197
|
+
* object the workers are built from (minus the connection), so the record and
|
|
198
|
+
* the workers cannot disagree.
|
|
199
|
+
*/
|
|
200
|
+
private recordQueueWorkers;
|
|
201
|
+
/**
|
|
202
|
+
* @internal
|
|
203
|
+
*
|
|
204
|
+
* builds every worker `connect` recorded a queue for, in the order the queues
|
|
205
|
+
* were built, so the default missing-connection throw still pre-empts the
|
|
206
|
+
* named one. The activation flag is set before the loop, not after it, because
|
|
207
|
+
* `_workers` is append-only and a build that throws partway must not be
|
|
208
|
+
* retryable into a second copy of the workers it already constructed.
|
|
209
|
+
*/
|
|
210
|
+
private buildRecordedWorkers;
|
|
211
|
+
/**
|
|
212
|
+
* @internal
|
|
213
|
+
*
|
|
214
|
+
* throws {@link NamedWorkstreamRateLimitMissingMaxOrDuration} for the first
|
|
215
|
+
* workstream whose `rateLimit` lacks a positive integer `max` or `duration`.
|
|
216
|
+
* The type requires both but admits any number, and open-source BullMQ does
|
|
217
|
+
* not validate a `limiter`: a fractional `duration` floors to 0ms and rate
|
|
218
|
+
* limits nothing, and one past Redis's integer range fails every job fetch.
|
|
219
|
+
*/
|
|
220
|
+
private assertNamedWorkstreamRateLimits;
|
|
221
|
+
/**
|
|
222
|
+
* @internal
|
|
223
|
+
*
|
|
224
|
+
* when `err` is a {@link RateLimitedPsychicJob} thrown on a queue whose workers
|
|
225
|
+
* carry no BullMQ `limiter`, returns the misconfiguration error to fail the job
|
|
226
|
+
* with instead; otherwise undefined. Declared as `Error` so the concrete,
|
|
227
|
+
* unexported class stays out of the emitted declarations. Four callers, one
|
|
228
|
+
* across a module boundary in `WorkerTestUtils`.
|
|
229
|
+
*/
|
|
230
|
+
misconfiguredRateLimitSignal(err: unknown, queue: Queue): Error | undefined;
|
|
231
|
+
/**
|
|
232
|
+
* @internal
|
|
233
|
+
*
|
|
234
|
+
* the processor every worker on `queue` runs: {@link doWork}, with a
|
|
235
|
+
* {@link RateLimitedPsychicJob} translated into BullMQ's own rate-limit
|
|
236
|
+
* signal — the queue's limiter key is set, pausing every worker on the queue,
|
|
237
|
+
* then `RateLimitError` moves the job back without counting an attempt or
|
|
238
|
+
* emitting `failed`. Thrown where the workers carry no `limiter`, it is
|
|
239
|
+
* instead a misconfiguration: see {@link misconfiguredRateLimitSignal}.
|
|
240
|
+
*/
|
|
241
|
+
private processorFor;
|
|
242
|
+
/**
|
|
243
|
+
* starts background workers: connects if this process has not connected yet,
|
|
244
|
+
* then builds the `Worker` objects for every queue `connect` recorded, and
|
|
245
|
+
* installs the process-level fatal-error and signal handlers that shut them
|
|
246
|
+
* down. An earlier producer-only `connect()` no longer leaves `work()` with
|
|
247
|
+
* nothing to do. A process with no `defaultWorkerConnection` configured throws
|
|
248
|
+
* here rather than returning quietly.
|
|
145
249
|
*/
|
|
146
250
|
work(): void;
|
|
147
251
|
/**
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { WorkerQueueDescription } from '../../types/background.js';
|
|
2
|
+
/**
|
|
3
|
+
* Not exported from the package: a delayed background job carried a `jobId`
|
|
4
|
+
* behind a delay short enough that it can enqueue work faster than its own
|
|
5
|
+
* queue's rate limit allows that work to start. Thrown from `_addToQueue`,
|
|
6
|
+
* above the test-mode short circuit, so a consumer's test environment raises it
|
|
7
|
+
* exactly as production does.
|
|
8
|
+
*/
|
|
9
|
+
export default class DebouncedJobOutpacesRateLimit extends Error {
|
|
10
|
+
private jobId;
|
|
11
|
+
private delayMs;
|
|
12
|
+
private keyLifetimeMs;
|
|
13
|
+
private limiter;
|
|
14
|
+
private queue;
|
|
15
|
+
constructor(jobId: string, delayMs: number, keyLifetimeMs: number, limiter: {
|
|
16
|
+
max: number;
|
|
17
|
+
duration: number;
|
|
18
|
+
}, queue: WorkerQueueDescription);
|
|
19
|
+
get message(): string;
|
|
20
|
+
private get msPerJob();
|
|
21
|
+
/**
|
|
22
|
+
* the shortest delay that would pass: enough that the key outlives the
|
|
23
|
+
* limiter's spacing. Rounded up to a whole second, since the delay is given
|
|
24
|
+
* in whole-ish duration fields and a value that still failed by a fraction
|
|
25
|
+
* of a second would be a hostile suggestion.
|
|
26
|
+
*/
|
|
27
|
+
private get minimumDelayMs();
|
|
28
|
+
private seconds;
|
|
29
|
+
private get kind();
|
|
30
|
+
private get where();
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Not exported from the package, and deliberately so: every branch of the guard
|
|
3
|
+
* that throws it is a call-site mistake — a `jobId` behind a delay under the
|
|
4
|
+
* floor, an empty `jobId`, or a delay that is zero, negative, `Infinity`, `NaN`
|
|
5
|
+
* or past `Number.MAX_SAFE_INTEGER` — so an application can never usefully
|
|
6
|
+
* catch it. The fix is always an edit to the call.
|
|
7
|
+
*
|
|
8
|
+
* `durationToSeconds` catches none of these: it validates nothing, and its
|
|
9
|
+
* falsy-field sum silently drops a `NaN` field rather than propagating it
|
|
10
|
+
* (`{ hours: 1, seconds: NaN }` is `3600`), so `{ seconds: NaN }` reaches this
|
|
11
|
+
* check as `0`.
|
|
12
|
+
*
|
|
13
|
+
* The check runs before the test-mode short circuit in `_addToQueue`, so a
|
|
14
|
+
* consumer's default test environment raises this exactly as production does.
|
|
15
|
+
*/
|
|
16
|
+
export default class DebouncedJobRequiresMinimumDelay extends Error {
|
|
17
|
+
private jobId;
|
|
18
|
+
private delaySeconds;
|
|
19
|
+
private minimumDelayMs;
|
|
20
|
+
constructor(jobId: string, delaySeconds: number | undefined, minimumDelayMs: number);
|
|
21
|
+
get message(): string;
|
|
22
|
+
private get emptyJobIdMessage();
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Not exported from the package: a named workstream's `rateLimit` reached
|
|
3
|
+
* `connect()` without a positive integer `max` or `duration`. Open-source
|
|
4
|
+
* BullMQ forwards the worker `limiter` it becomes unvalidated: a missing or
|
|
5
|
+
* non-numeric field fails every job fetch on that workstream with a Lua error
|
|
6
|
+
* that never names `rateLimit`, a fractional `duration` floors to 0ms and rate
|
|
7
|
+
* limits nothing, and one past Redis's integer range fails every fetch.
|
|
8
|
+
*/
|
|
9
|
+
export default class NamedWorkstreamRateLimitMissingMaxOrDuration extends Error {
|
|
10
|
+
private workstreamName;
|
|
11
|
+
private transitional;
|
|
12
|
+
private field;
|
|
13
|
+
private received;
|
|
14
|
+
constructor(workstreamName: string, transitional: boolean, field: 'max' | 'duration', received: unknown);
|
|
15
|
+
get message(): string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throw from inside a backgrounded (or scheduled) method to signal that the
|
|
3
|
+
* outside service it talks to rate-limited the request (an HTTP 429, say) and
|
|
4
|
+
* asked it to stop sending for a while. `pauseQueueForSeconds` is how long that
|
|
5
|
+
* while is, in whole seconds. Importable from `@rvoh/psychic-workers/errors`.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* // conf/workers.ts — one named workstream per rate-limited service
|
|
9
|
+
* namedWorkstreams: [{ name: 'slack', rateLimit: { max: 1, duration: 1000 } }]
|
|
10
|
+
*
|
|
11
|
+
* // app/services/SlackNotifier.ts
|
|
12
|
+
* import { RateLimitedPsychicJob } from '@rvoh/psychic-workers/errors'
|
|
13
|
+
*
|
|
14
|
+
* export default class SlackNotifier extends ApplicationBackgroundedService {
|
|
15
|
+
* public static override get backgroundJobConfig() {
|
|
16
|
+
* return { workstream: 'slack' }
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* public static async post(channel: string, text: string) {
|
|
20
|
+
* const response = await fetch('https://slack.com/api/chat.postMessage', { ... })
|
|
21
|
+
*
|
|
22
|
+
* if (response.status === 429) {
|
|
23
|
+
* // Slack's Retry-After header is in seconds, and so is this field
|
|
24
|
+
* const seconds = Number(response.headers.get('retry-after') ?? 1)
|
|
25
|
+
* throw new RateLimitedPsychicJob({ pauseQueueForSeconds: seconds })
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* The worker running the job must carry a BullMQ `limiter` — a named
|
|
32
|
+
* workstream with `rateLimit`, a native `namedQueueWorkers` entry with
|
|
33
|
+
* `limiter`, `nativeBullMQ.defaultWorkerOptions.limiter`, or a global
|
|
34
|
+
* `defaultBullMQWorkerOptions.limiter`. Given one, psychic-workers translates
|
|
35
|
+
* the signal into BullMQ's own rate-limit mechanism: the queue is paused for
|
|
36
|
+
* that many seconds and the job goes back onto the queue with `attemptsMade`
|
|
37
|
+
* unchanged and no backoff, so the burst is not multiplied by the retry
|
|
38
|
+
* schedule. Note that this package writes a BullMQ priority on every job it
|
|
39
|
+
* enqueues, so a re-queued job reports `prioritized` rather than `waiting` —
|
|
40
|
+
* assertions on `getWaitingCount()` do not hold.
|
|
41
|
+
*
|
|
42
|
+
* The pause replaces the queue's current limiter window and counter — the
|
|
43
|
+
* configured `rateLimit` window and any earlier pause alike — so once
|
|
44
|
+
* `pauseQueueForSeconds` elapses up to `max` jobs may start at once, and a
|
|
45
|
+
* `pauseQueueForSeconds` shorter than the window's remaining time shortens the
|
|
46
|
+
* configured limit. Concurrent throws overwrite each other: last writer wins,
|
|
47
|
+
* not longest.
|
|
48
|
+
*
|
|
49
|
+
* `attemptsMade` never grows, but each re-fetch counts in BullMQ's
|
|
50
|
+
* `attemptsStarted`, and the worker option `maxStartedAttempts`
|
|
51
|
+
* (`defaultBullMQWorkerOptions: { maxStartedAttempts: 10 }`, say) is the only
|
|
52
|
+
* bound on that cycle: without it it is unbounded. Set it on any workstream
|
|
53
|
+
* whose jobs throw this signal.
|
|
54
|
+
*
|
|
55
|
+
* Thrown from a job whose worker carries no `limiter`, the signal is a
|
|
56
|
+
* misconfiguration: the job fails with an ordinary error whose message names
|
|
57
|
+
* the fix. The same check runs under test invocation and `WorkerTestUtils`, so
|
|
58
|
+
* a misplaced job goes red in specs.
|
|
59
|
+
*
|
|
60
|
+
* `pauseQueueForSeconds` must be a positive, finite number of safe magnitude;
|
|
61
|
+
* the constructor throws a `RangeError` otherwise, at the throw site, so a
|
|
62
|
+
* nonsense value fails there rather than at the pause. A fractional number of
|
|
63
|
+
* seconds is legal and is rounded **up** — the field is a lower bound on the
|
|
64
|
+
* pause, so overshooting it by under a second cannot break the promise, while
|
|
65
|
+
* rounding down could.
|
|
66
|
+
*/
|
|
67
|
+
export default class RateLimitedPsychicJob extends Error {
|
|
68
|
+
readonly pauseQueueForSeconds: number;
|
|
69
|
+
constructor({ pauseQueueForSeconds }: {
|
|
70
|
+
pauseQueueForSeconds: number;
|
|
71
|
+
});
|
|
72
|
+
get message(): string;
|
|
73
|
+
}
|
package/dist/types/src/error/background/RateLimitedPsychicJobThrownFromWorkerWithoutLimiter.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { WorkerQueueDescription } from '../../types/background.js';
|
|
2
|
+
import RateLimitedPsychicJob from './RateLimitedPsychicJob.js';
|
|
3
|
+
/**
|
|
4
|
+
* Not exported from the package: a `RateLimitedPsychicJob` was thrown from a
|
|
5
|
+
* job whose worker carries no BullMQ `limiter`, so the queue cannot be paused.
|
|
6
|
+
* The job runner fails the job with this in the signal's place, so the failure
|
|
7
|
+
* reaches the application's failed-job monitoring.
|
|
8
|
+
*/
|
|
9
|
+
export default class RateLimitedPsychicJobThrownFromWorkerWithoutLimiter extends Error {
|
|
10
|
+
private signal;
|
|
11
|
+
private queue;
|
|
12
|
+
constructor(signal: RateLimitedPsychicJob, queue: WorkerQueueDescription);
|
|
13
|
+
get message(): string;
|
|
14
|
+
private get where();
|
|
15
|
+
private get fix();
|
|
16
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { default as NoClassForSpecifiedGlobalName } from '../error/background/NoClassForSpecifiedGlobalName.js';
|
|
2
2
|
export { default as NoQueueForSpecifiedQueueName } from '../error/background/NoQueueForSpecifiedQueueName.js';
|
|
3
3
|
export { default as NoQueueForSpecifiedWorkstream } from '../error/background/NoQueueForSpecifiedWorkstream.js';
|
|
4
|
+
export { default as RateLimitedPsychicJob } from '../error/background/RateLimitedPsychicJob.js';
|
|
@@ -64,7 +64,25 @@ export interface BullMQNativeWorkerOptions extends WorkerOptions {
|
|
|
64
64
|
concurrency?: number;
|
|
65
65
|
priority?: number;
|
|
66
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* How many jobs each worker built from this configuration runs at once
|
|
69
|
+
* (https://docs.bullmq.io/guide/workers/concurrency).
|
|
70
|
+
*
|
|
71
|
+
* Native BullMQ mode writes none, so BullMQ's default of **1** applies unless
|
|
72
|
+
* this or `defaultBullMQWorkerOptions.concurrency` sets it. See
|
|
73
|
+
* `PsychicBackgroundWorkstreamOptions.concurrency`.
|
|
74
|
+
*/
|
|
67
75
|
concurrency?: number;
|
|
76
|
+
/**
|
|
77
|
+
* the number of workers to create with this configuration, in this process.
|
|
78
|
+
*
|
|
79
|
+
* Read **only** for `nativeBullMQ.namedQueueWorkers`, where it defaults to 1
|
|
80
|
+
* (a named queue with no entry in `namedQueueWorkers` gets no workers at
|
|
81
|
+
* all). It is *not* read for `nativeBullMQ.defaultWorkerOptions`: the default
|
|
82
|
+
* queue's worker count comes from `nativeBullMQ.defaultWorkerCount`. See
|
|
83
|
+
* `PsychicBackgroundWorkstreamOptions.workerCount` for the
|
|
84
|
+
* `workerCount × concurrency` product.
|
|
85
|
+
*/
|
|
68
86
|
workerCount?: number;
|
|
69
87
|
}
|
|
70
88
|
export interface PsychicBackgroundNativeBullMQOptions extends PsychicBackgroundSharedOptions {
|
|
@@ -82,26 +100,24 @@ export interface PsychicBackgroundNativeBullMQOptions extends PsychicBackgroundS
|
|
|
82
100
|
*/
|
|
83
101
|
namedQueueOptions?: Record<string, QueueOptionsWithConnectionInstance>;
|
|
84
102
|
/**
|
|
85
|
-
* Native BullMQ options
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* only used to supply the number of default workers that you want.
|
|
103
|
+
* Native BullMQ options for the default workers, spread over
|
|
104
|
+
* `defaultBullMQWorkerOptions` — where `concurrency`, `limiter` and every
|
|
105
|
+
* other BullMQ worker option for them comes from. A `workerCount` set here
|
|
106
|
+
* is never read; the count comes from `defaultWorkerCount` below.
|
|
90
107
|
*/
|
|
91
108
|
defaultWorkerOptions?: BullMQNativeWorkerOptions;
|
|
92
109
|
/**
|
|
93
|
-
* The number of default workers to run
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* many workers running off a single queue. This number determines
|
|
98
|
-
* the number of those default workers to provide.
|
|
110
|
+
* The number of default workers to run in this process, and the only source
|
|
111
|
+
* of that count in native mode. Defaults to 1, and takes effect only in a
|
|
112
|
+
* process that activates workers. See
|
|
113
|
+
* `PsychicBackgroundWorkstreamOptions.workerCount`.
|
|
99
114
|
*/
|
|
100
115
|
defaultWorkerCount?: number;
|
|
101
116
|
/**
|
|
102
|
-
* namedQueueWorkers are necessary to work off namedQueues
|
|
103
|
-
*
|
|
104
|
-
*
|
|
117
|
+
* namedQueueWorkers are necessary to work off namedQueues.
|
|
118
|
+
* A named queue's workers can be rate limited with BullMQ's `limiter`
|
|
119
|
+
* option (https://docs.bullmq.io/guide/rate-limiting; useful for
|
|
120
|
+
* interacting with external APIs) on open-source BullMQ and BullMQ Pro alike
|
|
105
121
|
*/
|
|
106
122
|
namedQueueWorkers?: Record<string, BullMQNativeWorkerOptions>;
|
|
107
123
|
};
|
|
@@ -138,24 +154,34 @@ export interface PsychicBackgroundSimpleOptions extends PsychicBackgroundSharedO
|
|
|
138
154
|
*/
|
|
139
155
|
defaultWorkerConnection: RedisOrRedisClusterConnection | undefined;
|
|
140
156
|
/**
|
|
141
|
-
*
|
|
142
|
-
* workstream.
|
|
143
|
-
*
|
|
157
|
+
* The default workstream, whose queue carries every job not backgrounded to a
|
|
158
|
+
* named workstream. Both fields mean exactly what they mean on a named
|
|
159
|
+
* workstream — see `PsychicBackgroundWorkstreamOptions.workerCount` and
|
|
160
|
+
* `.concurrency`.
|
|
144
161
|
*/
|
|
145
162
|
defaultWorkstream?: {
|
|
163
|
+
/** The number of workers working through the default queue, in this process. Defaults to 1. */
|
|
146
164
|
workerCount?: number;
|
|
165
|
+
/**
|
|
166
|
+
* How many jobs each default worker runs at once
|
|
167
|
+
* (https://docs.bullmq.io/guide/workers/concurrency). Left unset, psychic
|
|
168
|
+
* falls back to 10, as everywhere in simple mode. See
|
|
169
|
+
* `PsychicBackgroundWorkstreamOptions.concurrency`.
|
|
170
|
+
*/
|
|
147
171
|
concurrency?: number;
|
|
148
172
|
};
|
|
149
173
|
/**
|
|
150
174
|
* When running background jobs on BullMQ, each named workstream corresponds
|
|
151
|
-
* to a specific queue and
|
|
152
|
-
* a
|
|
153
|
-
*
|
|
154
|
-
* named workstreams are useful for dispersing queues among nodes in a Redis cluster
|
|
155
|
-
* and for running queues on different Redis instances
|
|
175
|
+
* to a specific queue, and jobs backgrounded to a named workstream are added
|
|
176
|
+
* with a group id equal to the workstream name (a BullMQ Pro concept, ignored
|
|
177
|
+
* by open-source BullMQ)
|
|
156
178
|
*
|
|
157
|
-
*
|
|
158
|
-
* for
|
|
179
|
+
* named workstreams are useful for dispersing queues among nodes in a Redis cluster,
|
|
180
|
+
* for running queues on different Redis instances, and for rate limiting, on
|
|
181
|
+
* open-source BullMQ and BullMQ Pro alike (see
|
|
182
|
+
* `PsychicBackgroundWorkstreamOptions.rateLimit`). A rate limit targets one external
|
|
183
|
+
* limit, so give each rate-limited service its own named workstream — and where a
|
|
184
|
+
* service meters its endpoints separately, one workstream per limit
|
|
159
185
|
*/
|
|
160
186
|
namedWorkstreams?: PsychicBackgroundWorkstreamOptions[];
|
|
161
187
|
/**
|
|
@@ -171,43 +197,66 @@ export interface PsychicBackgroundWorkstreamOptions {
|
|
|
171
197
|
*/
|
|
172
198
|
name: string;
|
|
173
199
|
/**
|
|
174
|
-
* The number of workers you want to run on this configuration
|
|
200
|
+
* The number of workers you want to run on this configuration, in **this
|
|
201
|
+
* process**. Defaults to 1. These are BullMQ `Worker` objects sharing the
|
|
202
|
+
* activating process's event loop, not forked processes, so raising this buys
|
|
203
|
+
* concurrent waiting rather than concurrent CPU.
|
|
204
|
+
*
|
|
205
|
+
* `workerCount × concurrency` is the ceiling on this workstream's jobs in
|
|
206
|
+
* flight in one process, and that product is what to size against a
|
|
207
|
+
* downstream limit such as a connection pool or an external API's quota.
|
|
175
208
|
*/
|
|
176
209
|
workerCount?: number;
|
|
177
|
-
concurrency?: number;
|
|
178
|
-
/**
|
|
179
|
-
* See https://docs.bullmq.io/bullmq-pro/groups/rate-limiting for documentation
|
|
180
|
-
* on rate limiting in BullMQ Pro (requires paid BullMQ Pro license)
|
|
181
|
-
*/
|
|
182
|
-
rateLimit?: {
|
|
183
|
-
max?: number;
|
|
184
|
-
duration?: number;
|
|
185
|
-
};
|
|
186
|
-
/**
|
|
187
|
-
* Optional redis connection. If not provided, the default background redis connection will be used.
|
|
188
|
-
* See https://docs.bullmq.io/guide/going-to-production for the different settings to use between
|
|
189
|
-
* queue and worker connections.
|
|
190
|
-
*/
|
|
191
|
-
queueConnection?: RedisOrRedisClusterConnection;
|
|
192
|
-
workerConnection?: RedisOrRedisClusterConnection;
|
|
193
|
-
}
|
|
194
|
-
export interface PsychicBackgroundWorkstreamOptions {
|
|
195
|
-
/**
|
|
196
|
-
* This will be the name of the queue (and the group if using BullMQ Pro)
|
|
197
|
-
*/
|
|
198
|
-
name: string;
|
|
199
210
|
/**
|
|
200
|
-
*
|
|
211
|
+
* How many jobs each of this workstream's workers runs at once
|
|
212
|
+
* (https://docs.bullmq.io/guide/workers/concurrency).
|
|
213
|
+
*
|
|
214
|
+
* **In simple mode this defaults to 10.** Psychic always writes a
|
|
215
|
+
* `concurrency` onto a simple-mode worker, falling back to 10 when the
|
|
216
|
+
* workstream does not set one — which also means it overrides any
|
|
217
|
+
* `concurrency` placed in `defaultBullMQWorkerOptions`. Native BullMQ mode
|
|
218
|
+
* writes none, so BullMQ's own default of 1 applies there. Moving a queue
|
|
219
|
+
* between the two modes therefore changes how many jobs it runs at once by a
|
|
220
|
+
* factor of ten, with nothing in either configuration saying so.
|
|
221
|
+
*
|
|
222
|
+
* See `workerCount` above for the `workerCount × concurrency` product.
|
|
201
223
|
*/
|
|
202
|
-
workerCount?: number;
|
|
203
224
|
concurrency?: number;
|
|
204
225
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
226
|
+
* Rate-limit this workstream: at most `max` jobs start in any `duration`
|
|
227
|
+
* milliseconds. Works on open-source BullMQ and on BullMQ Pro.
|
|
228
|
+
*
|
|
229
|
+
* The limit is per queue, not per worker: every one of this workstream's
|
|
230
|
+
* workers, in every process running it, shares one counter kept in Redis, so
|
|
231
|
+
* `{ max: 1, duration: 1000 }` means one job per second for the workstream as
|
|
232
|
+
* a whole. `concurrency` caps how many jobs each worker runs at once;
|
|
233
|
+
* `rateLimit` caps how many may start per window across the whole workstream.
|
|
234
|
+
* For this workstream's workers it takes precedence over a global
|
|
235
|
+
* `defaultBullMQWorkerOptions.limiter`.
|
|
236
|
+
*
|
|
237
|
+
* A job on this workstream that is told to slow down (an HTTP 429, say) can
|
|
238
|
+
* throw `RateLimitedPsychicJob` from `@rvoh/psychic-workers/errors` to pause
|
|
239
|
+
* the whole workstream without burning a retry attempt; see that class for
|
|
240
|
+
* the pause semantics and for the `maxStartedAttempts` bound.
|
|
241
|
+
*
|
|
242
|
+
* A debounced job on this workstream must carry a delay long enough that it
|
|
243
|
+
* cannot enqueue faster than this limit starts jobs, or it is refused at
|
|
244
|
+
* enqueue: see `DelayedJobOpts.jobId`.
|
|
245
|
+
*
|
|
246
|
+
* `max` and `duration` are both required positive integers (`duration` in
|
|
247
|
+
* milliseconds): open-source BullMQ floors a fractional `duration` to 0ms
|
|
248
|
+
* (rate limiting nothing) and fails every job fetch on one past Redis's
|
|
249
|
+
* integer range. A `rateLimit` that reaches `connect()` without both fails
|
|
250
|
+
* `connect()` naming the workstream and the field, before any queue or worker
|
|
251
|
+
* is built.
|
|
252
|
+
*
|
|
253
|
+
* Implemented as BullMQ's worker `limiter` option, and on BullMQ Pro also as
|
|
254
|
+
* the workstream group's rate limit (unverified against an installed Pro
|
|
255
|
+
* build).
|
|
207
256
|
*/
|
|
208
257
|
rateLimit?: {
|
|
209
|
-
max
|
|
210
|
-
duration
|
|
258
|
+
max: number;
|
|
259
|
+
duration: number;
|
|
211
260
|
};
|
|
212
261
|
/**
|
|
213
262
|
* Optional redis connection. If not provided, the default background redis connection will be used.
|
|
@@ -1,10 +1,41 @@
|
|
|
1
|
+
import { Queue } from 'bullmq';
|
|
2
|
+
/**
|
|
3
|
+
* @internal
|
|
4
|
+
*
|
|
5
|
+
* resolves the queue's expected Redis name through `nameToRedisQueueName`,
|
|
6
|
+
* rather than hand-reconstructing a cluster hash tag or parallel-test suffix.
|
|
7
|
+
* `queue.opts.connection` is the same `Redis | Cluster` instance the queue was
|
|
8
|
+
* built with, which is why the cast is safe for every queue this package builds.
|
|
9
|
+
*/
|
|
10
|
+
export declare function queueNamesMatch(queue: Queue, compareQueueName: string): boolean;
|
|
1
11
|
export default class WorkerTestUtils {
|
|
2
12
|
static parallelTestSafeQueueName(queueName: string): string;
|
|
3
13
|
static work(opts?: TestWorkerWorkOffOpts): Promise<void>;
|
|
4
14
|
static workScheduled(opts?: TestWorkerScheduledWorkOffOpts): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* runs a delayed job in place. A `RateLimitedPsychicJob` thrown from a job on
|
|
17
|
+
* a queue whose workers carry no BullMQ `limiter` surfaces as the same
|
|
18
|
+
* misconfiguration error production would fail the job with; on a queue whose
|
|
19
|
+
* workers carry one, it propagates untranslated so the spec can assert on it.
|
|
20
|
+
*/
|
|
21
|
+
private static doScheduledWork;
|
|
5
22
|
static clean(): Promise<void>;
|
|
6
23
|
private static workOne;
|
|
7
|
-
|
|
24
|
+
/**
|
|
25
|
+
* runs one fetched job to completion or failure, mirroring what a real worker
|
|
26
|
+
* would do with a `RateLimitedPsychicJob`, then surfacing it so the spec goes
|
|
27
|
+
* red rather than looping:
|
|
28
|
+
*
|
|
29
|
+
* - on a queue whose workers carry no BullMQ `limiter`, the job is failed with
|
|
30
|
+
* the misconfiguration error production would fail it with (so, with the
|
|
31
|
+
* queue's `attempts` > 1, it lands in `delayed` for its retry, out of
|
|
32
|
+
* `active`, where `clean()` can reach it), and that error is rethrown
|
|
33
|
+
* - on a queue whose workers carry one, the job is moved back to the queue
|
|
34
|
+
* with no attempt counted (BullMQ's own rate-limit path), and the
|
|
35
|
+
* `RateLimitedPsychicJob` is rethrown
|
|
36
|
+
*
|
|
37
|
+
* Every other error only fails the job, as before.
|
|
38
|
+
*/
|
|
8
39
|
private static processJob;
|
|
9
40
|
}
|
|
10
41
|
interface TestWorkerWorkOffOpts {
|