@usehenri/jobs 0.0.0 → 1.2.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/CHANGELOG.md +311 -0
- package/LICENSE +21 -0
- package/README.md +8 -1
- package/index.js +45 -0
- package/module.js +8 -0
- package/package.json +50 -10
- package/src/batch.js +379 -0
- package/src/config.js +186 -0
- package/src/cron.js +237 -0
- package/src/definitions.js +236 -0
- package/src/duration.js +112 -0
- package/src/errors.js +115 -0
- package/src/jobs.js +1839 -0
- package/src/keys.js +65 -0
- package/src/module.js +442 -0
- package/src/runner.js +918 -0
- package/src/serialize.js +177 -0
- package/src/store/index.js +37 -0
- package/src/store/mongo.js +1334 -0
- package/src/store/schema.js +499 -0
- package/src/store/sql.js +1744 -0
package/src/batch.js
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
const { JobError } = require('./errors');
|
|
2
|
+
const { iso } = require('./duration');
|
|
3
|
+
const { toNumber } = require('./store/sql');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A batch: a set of jobs, and one job that runs when they are all done.
|
|
7
|
+
*
|
|
8
|
+
* ## A batch finishes, it does not succeed
|
|
9
|
+
*
|
|
10
|
+
* The callback runs once every job of the batch has reached a **terminal**
|
|
11
|
+
* state -- `dead` included -- and it is handed the counts. A batch whose
|
|
12
|
+
* last job failed is a finished batch with a failure in it, and what that
|
|
13
|
+
* means is the application's to decide: pretending otherwise would mean a
|
|
14
|
+
* callback that never runs and nobody noticing.
|
|
15
|
+
*
|
|
16
|
+
* ## The counter, and why it is exactly once
|
|
17
|
+
*
|
|
18
|
+
* `total` is written **once**, when the batch is sealed, and never moves
|
|
19
|
+
* again; `done` is advanced by one statement per terminal outcome, guarded
|
|
20
|
+
* by the claim token of the attempt that wrote it (`SqlStore#advanceBatch`,
|
|
21
|
+
* `MongoStore#advanceBatch`). So `done` reaches `total` exactly once, after
|
|
22
|
+
* the last job of the batch is terminal, whatever the interleaving of the
|
|
23
|
+
* runners -- and a job performed twice because its first runner went quiet
|
|
24
|
+
* counts once, for the attempt whose outcome actually landed.
|
|
25
|
+
*
|
|
26
|
+
* The callback is then enqueued under a unique key of the batch's own,
|
|
27
|
+
* which is what makes settling idempotent: the sweep settles an unfinished
|
|
28
|
+
* batch again after a runner is killed between writing an outcome and
|
|
29
|
+
* counting it, and the second settle answers the callback that is already
|
|
30
|
+
* in the queue rather than enqueuing a second one.
|
|
31
|
+
*
|
|
32
|
+
* ## What a batch is not
|
|
33
|
+
*
|
|
34
|
+
* It is not a transaction, and it is not atomic with the application's
|
|
35
|
+
* database: the queue reaches its own tables through the store adapter's
|
|
36
|
+
* raw `query()`, which does not join an open model transaction, so a batch
|
|
37
|
+
* enqueued inside one that rolls back is a batch that runs. That is true of
|
|
38
|
+
* every enqueue and the guide says so twice.
|
|
39
|
+
*
|
|
40
|
+
* A batch is also built where it is created: `add()` is a call on the
|
|
41
|
+
* handle, and once the handle has sealed the batch it refuses -- there is
|
|
42
|
+
* no adding a job from another process, which is the race that would let a
|
|
43
|
+
* callback run with work still on its way in.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/** The widest a batch name may be: the column that holds it */
|
|
47
|
+
const NAME_LENGTH = 190;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The options of the callback that are passed to `perform()`.
|
|
51
|
+
*
|
|
52
|
+
* `tenant` is one of them and is the only one henri fills in by itself
|
|
53
|
+
* (`Jobs#batch`): the callback is enqueued by a runner long after the
|
|
54
|
+
* request that made the batch is gone, so the tenant has to travel with
|
|
55
|
+
* the batch or the callback would be performed outside every tenant.
|
|
56
|
+
*/
|
|
57
|
+
const CALLBACK_OPTIONS = [
|
|
58
|
+
'at',
|
|
59
|
+
'maxAttempts',
|
|
60
|
+
'priority',
|
|
61
|
+
'queue',
|
|
62
|
+
'tenant',
|
|
63
|
+
'timeout',
|
|
64
|
+
'wait',
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A stored batch, as the API hands it out
|
|
69
|
+
*
|
|
70
|
+
* @param {?object} row A row of the batches table
|
|
71
|
+
* @returns {?object} The batch
|
|
72
|
+
*/
|
|
73
|
+
const toBatch = (row) => {
|
|
74
|
+
if (!row) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const total = toNumber(row.total) || 0;
|
|
79
|
+
const done = toNumber(row.done) || 0;
|
|
80
|
+
const failed = toNumber(row.failed) || 0;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
callback: row.callback || null,
|
|
84
|
+
callbackId: row.callback_id || null,
|
|
85
|
+
createdAt: iso(row.created_at),
|
|
86
|
+
done,
|
|
87
|
+
failed,
|
|
88
|
+
finished: Boolean(row.finished_at),
|
|
89
|
+
finishedAt: iso(row.finished_at),
|
|
90
|
+
id: row.id,
|
|
91
|
+
name: row.name || null,
|
|
92
|
+
sealed: Boolean(row.sealed_at),
|
|
93
|
+
sealedAt: iso(row.sealed_at),
|
|
94
|
+
succeeded: Math.max(0, done - failed),
|
|
95
|
+
total,
|
|
96
|
+
updatedAt: iso(row.updated_at),
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Refuses a batch declaration henri cannot read
|
|
102
|
+
*
|
|
103
|
+
* @param {string} why What is wrong with it
|
|
104
|
+
* @returns {void}
|
|
105
|
+
* @throws {JobError} HENRI_JOB_INVALID_BATCH, always
|
|
106
|
+
*/
|
|
107
|
+
const refuse = (why) => {
|
|
108
|
+
throw new JobError(
|
|
109
|
+
'HENRI_JOB_INVALID_BATCH',
|
|
110
|
+
`The batch cannot be read: ${why}`,
|
|
111
|
+
{
|
|
112
|
+
hint: "henri.jobs.batch({ callback: 'report/compile', jobs: [['resize', { id }]] }), or a function that adds them",
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* One job of a batch, however it was written
|
|
119
|
+
*
|
|
120
|
+
* `'resize'`, `['resize', args]`, `['resize', args, options]` and
|
|
121
|
+
* `{ name, args, options }` are the same thing.
|
|
122
|
+
*
|
|
123
|
+
* @param {*} entry What the application wrote
|
|
124
|
+
* @returns {object} `{ name, args, options }`
|
|
125
|
+
* @throws {JobError} HENRI_JOB_INVALID_BATCH on anything else
|
|
126
|
+
*/
|
|
127
|
+
const entry = (value) => {
|
|
128
|
+
if (typeof value === 'string') {
|
|
129
|
+
return { args: null, name: value, options: {} };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
const [name, args = null, options = {}] = value;
|
|
134
|
+
|
|
135
|
+
if (typeof name !== 'string' || name === '') {
|
|
136
|
+
refuse('a job of the list does not begin with a name');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { args, name, options: options || {} };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!value || typeof value !== 'object' || typeof value.name !== 'string') {
|
|
143
|
+
refuse(`${JSON.stringify(value)} is not a job name, a list or an object`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
args: typeof value.args === 'undefined' ? null : value.args,
|
|
148
|
+
name: value.name,
|
|
149
|
+
options: value.options || {},
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Reads what an application asked a batch for
|
|
155
|
+
*
|
|
156
|
+
* @param {object} [options={}] What `henri.jobs.batch()` was given
|
|
157
|
+
* @returns {object} `{ name, callback, args, options, jobs }`
|
|
158
|
+
* @throws {JobError} HENRI_JOB_INVALID_BATCH when it cannot be read
|
|
159
|
+
*/
|
|
160
|
+
const declaration = (options = {}) => {
|
|
161
|
+
const value = options || {};
|
|
162
|
+
|
|
163
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
164
|
+
refuse('it is not an object');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const name = typeof value.name === 'undefined' ? null : value.name;
|
|
168
|
+
|
|
169
|
+
if (
|
|
170
|
+
name !== null &&
|
|
171
|
+
(typeof name !== 'string' || name.length > NAME_LENGTH)
|
|
172
|
+
) {
|
|
173
|
+
refuse(`its name is not a string of at most ${NAME_LENGTH} characters`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const callback =
|
|
177
|
+
typeof value.callback === 'undefined' || value.callback === null
|
|
178
|
+
? null
|
|
179
|
+
: value.callback;
|
|
180
|
+
|
|
181
|
+
if (callback !== null && (typeof callback !== 'string' || callback === '')) {
|
|
182
|
+
refuse('its callback is not the name of a job');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const args = typeof value.args === 'undefined' ? null : value.args;
|
|
186
|
+
|
|
187
|
+
if (
|
|
188
|
+
args !== null &&
|
|
189
|
+
(typeof args !== 'object' || Array.isArray(args) || args instanceof Date)
|
|
190
|
+
) {
|
|
191
|
+
// The counts are handed over under `batch`, so there has to be somewhere
|
|
192
|
+
// to put them
|
|
193
|
+
refuse('the arguments of its callback are not a plain object');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// `null` is "no list at all", which leaves the batch open for the caller
|
|
197
|
+
// to add to and seal; `[]` is a batch of nothing, which is finished the
|
|
198
|
+
// moment it is made
|
|
199
|
+
const jobs = typeof value.jobs === 'undefined' ? null : value.jobs;
|
|
200
|
+
|
|
201
|
+
if (jobs !== null && !Array.isArray(jobs)) {
|
|
202
|
+
refuse('its jobs are not a list');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const passed = {};
|
|
206
|
+
|
|
207
|
+
for (const key of CALLBACK_OPTIONS) {
|
|
208
|
+
if (typeof value[key] !== 'undefined') {
|
|
209
|
+
passed[key] = value[key];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
args,
|
|
215
|
+
callback,
|
|
216
|
+
jobs: jobs && jobs.map(entry),
|
|
217
|
+
name,
|
|
218
|
+
options: passed,
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* A batch in hand: what adds jobs to it and closes it.
|
|
224
|
+
*
|
|
225
|
+
* `henri.jobs.batch()` answers one of these. Its counters are what the
|
|
226
|
+
* database said when it was last read, so `reload()` is how they are
|
|
227
|
+
* refreshed; everything else about a batch is read back with
|
|
228
|
+
* `henri.jobs.batches.get(id)`.
|
|
229
|
+
*
|
|
230
|
+
* @class Batch
|
|
231
|
+
*/
|
|
232
|
+
class Batch {
|
|
233
|
+
/**
|
|
234
|
+
* Creates an instance of Batch.
|
|
235
|
+
*
|
|
236
|
+
* @param {object} queue The queue that owns it
|
|
237
|
+
* @param {object} row The stored row
|
|
238
|
+
* @memberof Batch
|
|
239
|
+
*/
|
|
240
|
+
constructor(queue, row) {
|
|
241
|
+
this.queue = queue;
|
|
242
|
+
/** The ids of the jobs this handle enqueued */
|
|
243
|
+
this.jobs = [];
|
|
244
|
+
this.sync(row);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Reads a row onto this handle
|
|
249
|
+
*
|
|
250
|
+
* @param {object} row The stored row
|
|
251
|
+
* @returns {Batch} This batch
|
|
252
|
+
* @memberof Batch
|
|
253
|
+
*/
|
|
254
|
+
sync(row) {
|
|
255
|
+
Object.assign(this, toBatch(row));
|
|
256
|
+
|
|
257
|
+
return this;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Adds one job to the batch
|
|
262
|
+
*
|
|
263
|
+
* @param {string} name The job name
|
|
264
|
+
* @param {*} [args=null] What perform() receives
|
|
265
|
+
* @param {object} [options={}] The options of perform()
|
|
266
|
+
* @returns {Promise<object>} The enqueued job
|
|
267
|
+
* @throws {JobError} HENRI_JOB_BATCH_CLOSED once the batch is sealed
|
|
268
|
+
* @memberof Batch
|
|
269
|
+
*/
|
|
270
|
+
async add(name, args = null, options = {}) {
|
|
271
|
+
if (this.sealed) {
|
|
272
|
+
throw new JobError(
|
|
273
|
+
'HENRI_JOB_BATCH_CLOSED',
|
|
274
|
+
`The batch ${this.id} is closed: it holds ${this.total} job(s) and was sealed at ${this.sealedAt}`,
|
|
275
|
+
{
|
|
276
|
+
batch: this.id,
|
|
277
|
+
hint: 'A batch is built where it is created: add every job before it is sealed, or make another batch',
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const job = await this.queue.perform(name, args, {
|
|
283
|
+
...options,
|
|
284
|
+
batch: this.id,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
this.jobs.push(job.id);
|
|
288
|
+
|
|
289
|
+
return job;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Adds several jobs to the batch
|
|
294
|
+
*
|
|
295
|
+
* @param {Array} list The jobs, in any of the shapes `batch({ jobs })`
|
|
296
|
+
* takes
|
|
297
|
+
* @returns {Promise<Array<object>>} The enqueued jobs
|
|
298
|
+
* @memberof Batch
|
|
299
|
+
*/
|
|
300
|
+
async addAll(list) {
|
|
301
|
+
const enqueued = [];
|
|
302
|
+
|
|
303
|
+
for (const one of list) {
|
|
304
|
+
const { args, name, options } = entry(one);
|
|
305
|
+
|
|
306
|
+
enqueued.push(await this.add(name, args, options));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return enqueued;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Closes the batch, and settles it when there was nothing left to wait for
|
|
314
|
+
*
|
|
315
|
+
* @returns {Promise<Batch>} This batch
|
|
316
|
+
* @memberof Batch
|
|
317
|
+
*/
|
|
318
|
+
async seal() {
|
|
319
|
+
if (this.sealed) {
|
|
320
|
+
return this;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const row = await this.queue
|
|
324
|
+
.storeOrDie()
|
|
325
|
+
.sealBatch({ id: this.id, now: Date.now(), total: this.jobs.length });
|
|
326
|
+
|
|
327
|
+
this.sync(row || (await this.queue.storeOrDie().findBatch(this.id)));
|
|
328
|
+
|
|
329
|
+
// Every job of the batch may already be terminal -- an empty batch
|
|
330
|
+
// certainly is -- and nothing else will look at it until the sweep does
|
|
331
|
+
await this.queue.settle(await this.queue.storeOrDie().findBatch(this.id));
|
|
332
|
+
|
|
333
|
+
return this.reload();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Reads the batch back
|
|
338
|
+
*
|
|
339
|
+
* @returns {Promise<Batch>} This batch
|
|
340
|
+
* @memberof Batch
|
|
341
|
+
*/
|
|
342
|
+
async reload() {
|
|
343
|
+
return this.sync(await this.queue.storeOrDie().findBatch(this.id));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* The batch as a plain object
|
|
348
|
+
*
|
|
349
|
+
* @returns {object} What `henri.jobs.batches.get()` answers
|
|
350
|
+
* @memberof Batch
|
|
351
|
+
*/
|
|
352
|
+
toJSON() {
|
|
353
|
+
return {
|
|
354
|
+
callback: this.callback,
|
|
355
|
+
callbackId: this.callbackId,
|
|
356
|
+
createdAt: this.createdAt,
|
|
357
|
+
done: this.done,
|
|
358
|
+
failed: this.failed,
|
|
359
|
+
finished: this.finished,
|
|
360
|
+
finishedAt: this.finishedAt,
|
|
361
|
+
id: this.id,
|
|
362
|
+
name: this.name,
|
|
363
|
+
sealed: this.sealed,
|
|
364
|
+
sealedAt: this.sealedAt,
|
|
365
|
+
succeeded: this.succeeded,
|
|
366
|
+
total: this.total,
|
|
367
|
+
updatedAt: this.updatedAt,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
module.exports = {
|
|
373
|
+
Batch,
|
|
374
|
+
CALLBACK_OPTIONS,
|
|
375
|
+
NAME_LENGTH,
|
|
376
|
+
declaration,
|
|
377
|
+
entry,
|
|
378
|
+
toBatch,
|
|
379
|
+
};
|
package/src/config.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
const { next, parse } = require('./cron');
|
|
2
|
+
const { coded } = require('./errors');
|
|
3
|
+
const { duration } = require('./duration');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The `jobs` block of `config/<env>.json`, with every default filled in.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DEFAULTS = {
|
|
10
|
+
backoff: { base: '5s', factor: 4, jitter: 0.15, max: '1h' },
|
|
11
|
+
concurrency: 5,
|
|
12
|
+
install: true,
|
|
13
|
+
keepCompleted: '1d',
|
|
14
|
+
mailQueue: 'mailers',
|
|
15
|
+
maxArgsBytes: 512 * 1024,
|
|
16
|
+
maxAttempts: 5,
|
|
17
|
+
pollInterval: '1s',
|
|
18
|
+
priority: 0,
|
|
19
|
+
queue: 'default',
|
|
20
|
+
queues: [],
|
|
21
|
+
recurring: {},
|
|
22
|
+
store: 'default',
|
|
23
|
+
stuckAfter: '5m',
|
|
24
|
+
table: 'henri_jobs',
|
|
25
|
+
timeout: null,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A list of queue names from a string or an array
|
|
30
|
+
*
|
|
31
|
+
* @param {(string|Array<string>|null)} value `'a,b'`, `['a', 'b']` or nothing
|
|
32
|
+
* @returns {Array<string>} The queue names
|
|
33
|
+
*/
|
|
34
|
+
const queues = (value) => {
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
return value.map((entry) => String(entry).trim()).filter(Boolean);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof value === 'string') {
|
|
40
|
+
return value
|
|
41
|
+
.split(',')
|
|
42
|
+
.map((entry) => entry.trim())
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return [];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reads a recurring schedule
|
|
51
|
+
*
|
|
52
|
+
* @param {string} name The schedule name
|
|
53
|
+
* @param {object} entry Its configuration
|
|
54
|
+
* @returns {object} The normalized schedule
|
|
55
|
+
* @throws {Error} When it has neither `cron` nor `every`, or names no job
|
|
56
|
+
*/
|
|
57
|
+
const recurring = (name, entry) => {
|
|
58
|
+
const value = entry || {};
|
|
59
|
+
const job = value.job || value.name || name;
|
|
60
|
+
|
|
61
|
+
if (!value.cron && !value.every) {
|
|
62
|
+
throw coded(
|
|
63
|
+
'HENRI_JOB_INVALID_SCHEDULE',
|
|
64
|
+
`@usehenri/jobs: the recurring schedule "${name}" needs a "cron" or an "every"`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (value.cron && value.every) {
|
|
69
|
+
throw coded(
|
|
70
|
+
'HENRI_JOB_INVALID_SCHEDULE',
|
|
71
|
+
`@usehenri/jobs: the recurring schedule "${name}" has both a "cron" and an "every": pick one`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Parsed here, not on the first tick of a runner: an expression a runner
|
|
76
|
+
// cannot read would otherwise throw inside its loop, every second, with
|
|
77
|
+
// nothing being claimed while it does
|
|
78
|
+
if (value.cron) {
|
|
79
|
+
try {
|
|
80
|
+
if (next(parse(value.cron)) === null) {
|
|
81
|
+
throw new Error(`"${value.cron}" can never come round`);
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw Object.assign(
|
|
85
|
+
new Error(
|
|
86
|
+
`@usehenri/jobs: the recurring schedule "${name}" is invalid: ${error.message}`,
|
|
87
|
+
{ cause: error }
|
|
88
|
+
),
|
|
89
|
+
{ code: 'HENRI_JOB_INVALID_SCHEDULE' }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (value.every && duration(value.every) < 1000) {
|
|
95
|
+
throw coded(
|
|
96
|
+
'HENRI_JOB_INVALID_SCHEDULE',
|
|
97
|
+
`@usehenri/jobs: the recurring schedule "${name}" runs every ${value.every}, which is under a second`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
args: typeof value.args === 'undefined' ? null : value.args,
|
|
103
|
+
cron: value.cron || null,
|
|
104
|
+
every: value.every ? duration(value.every) : null,
|
|
105
|
+
job,
|
|
106
|
+
name,
|
|
107
|
+
priority: typeof value.priority === 'number' ? value.priority : null,
|
|
108
|
+
queue: value.queue || null,
|
|
109
|
+
spec: value.cron ? `cron:${value.cron}` : `every:${duration(value.every)}`,
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Checks a table name before it is written into every statement
|
|
115
|
+
*
|
|
116
|
+
* The queue interpolates its table names: they are configuration, not
|
|
117
|
+
* request input, but an application may now set any key from the
|
|
118
|
+
* environment, and a name that is not a plain identifier gives a syntax
|
|
119
|
+
* error deep in a query instead of a sentence here.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} value The name
|
|
122
|
+
* @returns {string} The name
|
|
123
|
+
* @throws {Error} When it is not a plain identifier
|
|
124
|
+
*/
|
|
125
|
+
const table = (value) => {
|
|
126
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
|
127
|
+
throw coded(
|
|
128
|
+
'HENRI_CONFIG_INVALID',
|
|
129
|
+
`@usehenri/jobs: invalid table name "${value}": letters, digits and underscores only`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return value;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The jobs configuration of an application
|
|
138
|
+
*
|
|
139
|
+
* @param {object} [config={}] The `jobs` block of the configuration
|
|
140
|
+
* @returns {object} The configuration, with the defaults filled in
|
|
141
|
+
* @throws {Error} When a duration or a schedule is invalid
|
|
142
|
+
*/
|
|
143
|
+
const normalize = (config = {}) => {
|
|
144
|
+
const value = config || {};
|
|
145
|
+
const backoff = { ...DEFAULTS.backoff, ...(value.backoff || {}) };
|
|
146
|
+
const schedules = value.recurring || DEFAULTS.recurring;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
backoff: {
|
|
150
|
+
base: duration(backoff.base, 5000),
|
|
151
|
+
factor: Number(backoff.factor) || 4,
|
|
152
|
+
jitter: Math.min(Math.max(Number(backoff.jitter) || 0, 0), 1),
|
|
153
|
+
max: duration(backoff.max, 3600000),
|
|
154
|
+
},
|
|
155
|
+
concurrency: Math.max(1, Number(value.concurrency) || DEFAULTS.concurrency),
|
|
156
|
+
install: value.install !== false,
|
|
157
|
+
keepCompleted: duration(
|
|
158
|
+
value.keepCompleted,
|
|
159
|
+
duration(DEFAULTS.keepCompleted)
|
|
160
|
+
),
|
|
161
|
+
mailQueue: value.mailQueue || DEFAULTS.mailQueue,
|
|
162
|
+
maxArgsBytes: Number(value.maxArgsBytes) || DEFAULTS.maxArgsBytes,
|
|
163
|
+
maxAttempts: Math.max(1, Number(value.maxAttempts) || DEFAULTS.maxAttempts),
|
|
164
|
+
pollInterval: Math.max(
|
|
165
|
+
50,
|
|
166
|
+
duration(value.pollInterval, duration(DEFAULTS.pollInterval))
|
|
167
|
+
),
|
|
168
|
+
priority: Number(value.priority) || DEFAULTS.priority,
|
|
169
|
+
queue: value.queue || DEFAULTS.queue,
|
|
170
|
+
queues: queues(value.queues),
|
|
171
|
+
recurring: Object.keys(schedules)
|
|
172
|
+
.sort()
|
|
173
|
+
.map((name) => recurring(name, schedules[name])),
|
|
174
|
+
store: value.store || DEFAULTS.store,
|
|
175
|
+
stuckAfter: duration(value.stuckAfter, duration(DEFAULTS.stuckAfter)),
|
|
176
|
+
tables: {
|
|
177
|
+
batches: `${table(value.table || DEFAULTS.table)}_batches`,
|
|
178
|
+
jobs: table(value.table || DEFAULTS.table),
|
|
179
|
+
limits: `${table(value.table || DEFAULTS.table)}_limits`,
|
|
180
|
+
schedules: `${table(value.table || DEFAULTS.table)}_schedules`,
|
|
181
|
+
},
|
|
182
|
+
timeout: duration(value.timeout, DEFAULTS.timeout),
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
module.exports = { DEFAULTS, normalize, queues, recurring, table };
|