@tryghost/job-manager 3.0.3 → 3.1.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/LICENSE +21 -0
- package/README.md +27 -21
- package/lib/JobManager.js +61 -60
- package/lib/JobsRepository.js +5 -5
- package/lib/assemble-bree-job.js +6 -6
- package/lib/is-cron-expression.js +2 -2
- package/package.json +24 -26
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2013-2026 Ghost Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ A manager for background jobs in Ghost, supporting one-off tasks and recurring j
|
|
|
7
7
|
Background job orchestration for Ghost, supporting one-off and recurring jobs in-process or offloaded workers.
|
|
8
8
|
|
|
9
9
|
## Table of Contents
|
|
10
|
+
|
|
10
11
|
- [Quick Start](#quick-start)
|
|
11
12
|
- [Job Types](#job-types)
|
|
12
13
|
- [Background Job Requirements](#background-job-requirements)
|
|
@@ -17,20 +18,20 @@ Background job orchestration for Ghost, supporting one-off and recurring jobs in
|
|
|
17
18
|
|
|
18
19
|
```js
|
|
19
20
|
const JobManager = require('@tryghost/job-manager');
|
|
20
|
-
const jobManager = new JobManager({JobModel: models.Job});
|
|
21
|
+
const jobManager = new JobManager({ JobModel: models.Job });
|
|
21
22
|
|
|
22
23
|
// Simple one-off job
|
|
23
24
|
jobManager.addJob({
|
|
24
25
|
name: 'hello-world',
|
|
25
26
|
job: () => console.log('Hello World'),
|
|
26
|
-
offloaded: false
|
|
27
|
+
offloaded: false,
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
// Recurring job every 5 minutes
|
|
30
31
|
jobManager.addJob({
|
|
31
32
|
name: 'check-emails',
|
|
32
33
|
at: 'every 5 minutes',
|
|
33
|
-
job: './jobs/check-emails.js'
|
|
34
|
+
job: './jobs/check-emails.js',
|
|
34
35
|
});
|
|
35
36
|
```
|
|
36
37
|
|
|
@@ -39,14 +40,14 @@ jobManager.addJob({
|
|
|
39
40
|
Ghost supports two types of jobs:
|
|
40
41
|
|
|
41
42
|
1. **Inline Jobs**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
- Run in the main Ghost process
|
|
44
|
+
- Best for quick, simple tasks
|
|
45
|
+
- Cannot be scheduled or recurring
|
|
45
46
|
|
|
46
47
|
2. **Offloaded Jobs**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
- Run in a separate process
|
|
49
|
+
- Good for CPU-intensive tasks
|
|
50
|
+
- Can be scheduled or recurring
|
|
50
51
|
|
|
51
52
|
## Background Job Requirements
|
|
52
53
|
|
|
@@ -61,30 +62,31 @@ Offloaded jobs must:
|
|
|
61
62
|
## Advanced Usage
|
|
62
63
|
|
|
63
64
|
Below is a sample code to wire up job manger and initialize jobs. This is the simplest way to interact with the job manager - these jobs do not persist after reboot:
|
|
65
|
+
|
|
64
66
|
```js
|
|
65
67
|
const JobManager = require('@tryghost/job-manager');
|
|
66
68
|
|
|
67
|
-
const jobManager = new JobManager({JobModel: models.Job});
|
|
69
|
+
const jobManager = new JobManager({ JobModel: models.Job });
|
|
68
70
|
|
|
69
71
|
// register a job "function" with queued execution in current event loop
|
|
70
72
|
jobManager.addJob({
|
|
71
73
|
job: (word) => console.log(word),
|
|
72
74
|
name: 'hello',
|
|
73
|
-
offloaded: false
|
|
75
|
+
offloaded: false,
|
|
74
76
|
});
|
|
75
77
|
|
|
76
78
|
// register a job "module" with queued execution in current even loop
|
|
77
79
|
jobManager.addJob({
|
|
78
|
-
job:'./path/to/email-module.js',
|
|
79
|
-
data: {email: 'send@here.com'},
|
|
80
|
-
offloaded: false
|
|
80
|
+
job: './path/to/email-module.js',
|
|
81
|
+
data: { email: 'send@here.com' },
|
|
82
|
+
offloaded: false,
|
|
81
83
|
});
|
|
82
84
|
|
|
83
85
|
// register recurring job which needs execution outside of current event loop
|
|
84
86
|
jobManager.addJob({
|
|
85
87
|
at: 'every 5 minutes',
|
|
86
88
|
job: './path/to/jobs/check-emails.js',
|
|
87
|
-
name: 'email-checker'
|
|
89
|
+
name: 'email-checker',
|
|
88
90
|
});
|
|
89
91
|
|
|
90
92
|
// register recurring job with cron syntax running every 5 minutes
|
|
@@ -93,20 +95,20 @@ jobManager.addJob({
|
|
|
93
95
|
jobManager.addJob({
|
|
94
96
|
at: '0 1/5 * * * *',
|
|
95
97
|
job: './path/to/jobs/check-emails.js',
|
|
96
|
-
name: 'email-checker-cron'
|
|
98
|
+
name: 'email-checker-cron',
|
|
97
99
|
});
|
|
98
100
|
|
|
99
101
|
// register a job to run immediately running outside of current even loop
|
|
100
102
|
jobManager.addJob({
|
|
101
103
|
job: './path/to/jobs/check-emails.js',
|
|
102
|
-
name: 'email-checker-now'
|
|
104
|
+
name: 'email-checker-now',
|
|
103
105
|
});
|
|
104
106
|
|
|
105
107
|
// register a one-off job to be executed immediately within current event loop
|
|
106
108
|
jobManager.addOneOffJob({
|
|
107
109
|
name: 'members-migrations',
|
|
108
110
|
offloaded: false,
|
|
109
|
-
job: stripeService.migrations.execute.bind(stripeService.migrations)
|
|
111
|
+
job: stripeService.migrations.execute.bind(stripeService.migrations),
|
|
110
112
|
});
|
|
111
113
|
|
|
112
114
|
// register a one-off job to be executed immediately outside of current event loop
|
|
@@ -115,12 +117,12 @@ jobManager.addOneOffJob({
|
|
|
115
117
|
job: './path/to/jobs/backup.js',
|
|
116
118
|
});
|
|
117
119
|
|
|
118
|
-
// optionally await completion of the one-off job in case
|
|
120
|
+
// optionally await completion of the one-off job in case
|
|
119
121
|
// there are state changes expected to execute the rest of the process
|
|
120
122
|
// NOTE: if multiple jobs are submitted using the same name, the first completion will resolve
|
|
121
123
|
await jobManager.awaitOneOffCompletion('members-migrations');
|
|
122
124
|
|
|
123
|
-
// check if previously registered one-off job has been executed
|
|
125
|
+
// check if previously registered one-off job has been executed
|
|
124
126
|
// successfully - it exists and doesn't have a "failed" state.
|
|
125
127
|
// NOTE: this is stored in memory and cleared on reboot
|
|
126
128
|
const backupSuccessful = await jobManager.hasExecutedSuccessfully('generate-backup-2022-09-15');
|
|
@@ -139,16 +141,20 @@ For more examples of JobManager initialization check [test/examples](https://git
|
|
|
139
141
|
## Technical Details
|
|
140
142
|
|
|
141
143
|
### Process Isolation
|
|
144
|
+
|
|
142
145
|
Background jobs run in separate processes, meaning:
|
|
146
|
+
|
|
143
147
|
- No shared memory with Ghost
|
|
144
148
|
- Must import own dependencies
|
|
145
149
|
- Should avoid heavy dependencies
|
|
146
150
|
- Should handle graceful shutdown
|
|
147
151
|
|
|
148
152
|
### Job Lifecycle
|
|
153
|
+
|
|
149
154
|
Offloaded jobs are running on dedicated worker threads which makes their lifecycle a bit different from inline jobs:
|
|
155
|
+
|
|
150
156
|
1. When **starting** a job it's only sharing ENV variables with its parent process. The job itself is run on an independent JavaScript execution thread. The script has to re-initialize any modules it will use. For example, it should take care of: model layer initialization, cache initialization, etc.
|
|
151
|
-
2. When **finishing** work in a job prefer to signal successful termination by sending 'done' message to the parent thread: `parentPort.postMessage('done')` ([example use](https://github.com/TryGhost/Utils/blob/0e423f6c5c69b08d81d470f49de95654d8cc90e3/packages/job-manager/test/jobs/graceful.js#L33-L37)). Finishing work this way terminates the thread through [worker.terminate()]((https://nodejs.org/dist/latest-v14.x/docs/api/worker_threads.html#worker_threads_worker_terminate)), which logs termination in parent process and flushes any pipes opened in thread.
|
|
157
|
+
2. When **finishing** work in a job prefer to signal successful termination by sending 'done' message to the parent thread: `parentPort.postMessage('done')` ([example use](https://github.com/TryGhost/Utils/blob/0e423f6c5c69b08d81d470f49de95654d8cc90e3/packages/job-manager/test/jobs/graceful.js#L33-L37)). Finishing work this way terminates the thread through [worker.terminate()](<(https://nodejs.org/dist/latest-v14.x/docs/api/worker_threads.html#worker_threads_worker_terminate)>), which logs termination in parent process and flushes any pipes opened in thread.
|
|
152
158
|
3. Jobs that have iterative nature, or need cleanup before interrupting work should allow for **graceful shutdown** by listening on `'cancel'` message coming from parent thread ([example use](https://github.com/TryGhost/Utils/blob/0e423f6c5c69b08d81d470f49de95654d8cc90e3/packages/job-manager/test/jobs/graceful.js#L12-L16)).
|
|
153
159
|
4. When **exceptions** happen and expected outcome is to terminate current job, leave the exception unhandled allowing it to bubble up to the job manager. Unhandled exceptions [terminate current thread](https://nodejs.org/dist/latest-v14.x/docs/api/worker_threads.html#worker_threads_event_error) and allow for next scheduled job execution to happen.
|
|
154
160
|
|
package/lib/JobManager.js
CHANGED
|
@@ -4,7 +4,7 @@ const setTimeoutPromise = util.promisify(setTimeout);
|
|
|
4
4
|
const fastq = require('fastq');
|
|
5
5
|
const later = require('@breejs/later');
|
|
6
6
|
const Bree = require('bree');
|
|
7
|
-
const {UnhandledJobError, IncorrectUsageError} = require('@tryghost/errors');
|
|
7
|
+
const { UnhandledJobError, IncorrectUsageError } = require('@tryghost/errors');
|
|
8
8
|
const logging = require('@tryghost/logging');
|
|
9
9
|
const isCronExpression = require('./is-cron-expression');
|
|
10
10
|
const assembleBreeJob = require('./assemble-bree-job');
|
|
@@ -23,7 +23,7 @@ const ALL_STATUSES = {
|
|
|
23
23
|
started: 'started',
|
|
24
24
|
finished: 'finished',
|
|
25
25
|
failed: 'failed',
|
|
26
|
-
queued: 'queued'
|
|
26
|
+
queued: 'queued',
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
/**
|
|
@@ -37,9 +37,6 @@ const ALL_STATUSES = {
|
|
|
37
37
|
class JobManager {
|
|
38
38
|
#domainEvents;
|
|
39
39
|
#completionPromises = new Map();
|
|
40
|
-
#config;
|
|
41
|
-
#JobModel;
|
|
42
|
-
#events;
|
|
43
40
|
|
|
44
41
|
/**
|
|
45
42
|
* @param {Object} options
|
|
@@ -47,30 +44,25 @@ class JobManager {
|
|
|
47
44
|
* @param {Function} [options.workerMessageHandler] - custom message handler coming from workers
|
|
48
45
|
* @param {Object} [options.JobModel] - a model which can persist job data in the storage
|
|
49
46
|
* @param {Object} [options.domainEvents] - domain events emitter
|
|
50
|
-
* @param {Object} [options.config] - config
|
|
51
|
-
* @param {Object} [options.events] - events instance (for testing)
|
|
52
47
|
*/
|
|
53
|
-
constructor({errorHandler, workerMessageHandler, JobModel, domainEvents
|
|
48
|
+
constructor({ errorHandler, workerMessageHandler, JobModel, domainEvents }) {
|
|
54
49
|
this.inlineQueue = fastq(this, worker, 3);
|
|
55
50
|
this._jobMessageHandler = this._jobMessageHandler.bind(this);
|
|
56
51
|
this._jobErrorHandler = this._jobErrorHandler.bind(this);
|
|
57
52
|
this.#domainEvents = domainEvents;
|
|
58
|
-
this.#config = config;
|
|
59
|
-
this.#JobModel = JobModel;
|
|
60
|
-
this.#events = events;
|
|
61
53
|
|
|
62
54
|
const combinedMessageHandler = workerMessageHandler
|
|
63
|
-
? ({name, message}) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
? ({ name, message }) => {
|
|
56
|
+
workerMessageHandler({ name, message });
|
|
57
|
+
this._jobMessageHandler({ name, message });
|
|
58
|
+
}
|
|
67
59
|
: this._jobMessageHandler;
|
|
68
60
|
|
|
69
61
|
const combinedErrorHandler = errorHandler
|
|
70
62
|
? (error, workerMeta) => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
63
|
+
errorHandler(error, workerMeta);
|
|
64
|
+
this._jobErrorHandler(error, workerMeta);
|
|
65
|
+
}
|
|
74
66
|
: this._jobErrorHandler;
|
|
75
67
|
|
|
76
68
|
this.bree = new Bree({
|
|
@@ -79,15 +71,15 @@ class JobManager {
|
|
|
79
71
|
outputWorkerMetadata: true,
|
|
80
72
|
logger: logging,
|
|
81
73
|
errorHandler: combinedErrorHandler,
|
|
82
|
-
workerMessageHandler: combinedMessageHandler
|
|
74
|
+
workerMessageHandler: combinedMessageHandler,
|
|
83
75
|
});
|
|
84
76
|
|
|
85
77
|
this.bree.on('worker created', (name) => {
|
|
86
|
-
this._jobMessageHandler({name, message: ALL_STATUSES.started});
|
|
78
|
+
this._jobMessageHandler({ name, message: ALL_STATUSES.started });
|
|
87
79
|
});
|
|
88
80
|
|
|
89
81
|
if (JobModel) {
|
|
90
|
-
this._jobsRepository = new JobsRepository({JobModel});
|
|
82
|
+
this._jobsRepository = new JobsRepository({ JobModel });
|
|
91
83
|
}
|
|
92
84
|
}
|
|
93
85
|
|
|
@@ -95,12 +87,12 @@ class JobManager {
|
|
|
95
87
|
return async (error, result) => {
|
|
96
88
|
if (error) {
|
|
97
89
|
await this._jobErrorHandler(error, {
|
|
98
|
-
name: jobName
|
|
90
|
+
name: jobName,
|
|
99
91
|
});
|
|
100
92
|
} else {
|
|
101
93
|
await this._jobMessageHandler({
|
|
102
94
|
name: jobName,
|
|
103
|
-
message: 'done'
|
|
95
|
+
message: 'done',
|
|
104
96
|
});
|
|
105
97
|
}
|
|
106
98
|
|
|
@@ -109,7 +101,7 @@ class JobManager {
|
|
|
109
101
|
};
|
|
110
102
|
}
|
|
111
103
|
|
|
112
|
-
async _jobMessageHandler({name, message}) {
|
|
104
|
+
async _jobMessageHandler({ name, message }) {
|
|
113
105
|
if (name) {
|
|
114
106
|
if (message === ALL_STATUSES.started) {
|
|
115
107
|
if (this._jobsRepository) {
|
|
@@ -118,7 +110,7 @@ class JobManager {
|
|
|
118
110
|
if (job) {
|
|
119
111
|
await this._jobsRepository.update(job.id, {
|
|
120
112
|
status: ALL_STATUSES.started,
|
|
121
|
-
started_at: new Date()
|
|
113
|
+
started_at: new Date(),
|
|
122
114
|
});
|
|
123
115
|
}
|
|
124
116
|
}
|
|
@@ -129,7 +121,7 @@ class JobManager {
|
|
|
129
121
|
if (job) {
|
|
130
122
|
await this._jobsRepository.update(job.id, {
|
|
131
123
|
status: ALL_STATUSES.finished,
|
|
132
|
-
finished_at: new Date()
|
|
124
|
+
finished_at: new Date(),
|
|
133
125
|
});
|
|
134
126
|
}
|
|
135
127
|
}
|
|
@@ -169,7 +161,7 @@ class JobManager {
|
|
|
169
161
|
|
|
170
162
|
if (job) {
|
|
171
163
|
await this._jobsRepository.update(job.id, {
|
|
172
|
-
status: ALL_STATUSES.failed
|
|
164
|
+
status: ALL_STATUSES.failed,
|
|
173
165
|
});
|
|
174
166
|
}
|
|
175
167
|
}
|
|
@@ -205,7 +197,7 @@ class JobManager {
|
|
|
205
197
|
* @prop {Object} [GhostJob.data] - data to be passed into the job
|
|
206
198
|
* @prop {Boolean} [GhostJob.offloaded] - creates an "offloaded" job running in a worker thread by default. If set to "false" runs an "inline" job on the same event loop
|
|
207
199
|
*/
|
|
208
|
-
async addJob({name, at, job, data, offloaded = true}) {
|
|
200
|
+
async addJob({ name, at, job, data, offloaded = true }) {
|
|
209
201
|
if (offloaded) {
|
|
210
202
|
logging.info('Adding offloaded job to the inline job queue');
|
|
211
203
|
let schedule;
|
|
@@ -215,7 +207,7 @@ class JobManager {
|
|
|
215
207
|
name = path.parse(job).name;
|
|
216
208
|
} else {
|
|
217
209
|
throw new IncorrectUsageError({
|
|
218
|
-
message: 'Name parameter should be present if job is a function'
|
|
210
|
+
message: 'Name parameter should be present if job is a function',
|
|
219
211
|
});
|
|
220
212
|
}
|
|
221
213
|
}
|
|
@@ -229,11 +221,13 @@ class JobManager {
|
|
|
229
221
|
|
|
230
222
|
if ((schedule.error && schedule.error !== -1) || schedule.schedules.length === 0) {
|
|
231
223
|
throw new IncorrectUsageError({
|
|
232
|
-
message: 'Invalid schedule format'
|
|
224
|
+
message: 'Invalid schedule format',
|
|
233
225
|
});
|
|
234
226
|
}
|
|
235
227
|
|
|
236
|
-
logging.info(
|
|
228
|
+
logging.info(
|
|
229
|
+
`Scheduling job ${name} at ${at}. Next run on: ${later.schedule(schedule).next()}`,
|
|
230
|
+
);
|
|
237
231
|
} else if (at !== undefined) {
|
|
238
232
|
logging.info(`Scheduling job ${name} at ${at}`);
|
|
239
233
|
} else {
|
|
@@ -244,7 +238,9 @@ class JobManager {
|
|
|
244
238
|
await this.bree.add(breeJob);
|
|
245
239
|
return this.bree.start(name);
|
|
246
240
|
} else {
|
|
247
|
-
logging.info(
|
|
241
|
+
logging.info(
|
|
242
|
+
`Adding one-off job to inlineQueue with current length = ${this.inlineQueue.length()} called '${name || 'anonymous'}'`,
|
|
243
|
+
);
|
|
248
244
|
|
|
249
245
|
this.inlineQueue.push(async () => {
|
|
250
246
|
try {
|
|
@@ -252,7 +248,7 @@ class JobManager {
|
|
|
252
248
|
// distinguish between states when the job fails immediately
|
|
253
249
|
await this._jobMessageHandler({
|
|
254
250
|
name: name,
|
|
255
|
-
message: ALL_STATUSES.started
|
|
251
|
+
message: ALL_STATUSES.started,
|
|
256
252
|
});
|
|
257
253
|
|
|
258
254
|
if (typeof job === 'function') {
|
|
@@ -263,10 +259,12 @@ class JobManager {
|
|
|
263
259
|
} catch (err) {
|
|
264
260
|
// NOTE: each job should be written in a safe way and handle all errors internally
|
|
265
261
|
// if the error is caught here jobs implementation should be changed
|
|
266
|
-
logging.error(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
262
|
+
logging.error(
|
|
263
|
+
new UnhandledJobError({
|
|
264
|
+
context: typeof job === 'function' ? 'function' : job,
|
|
265
|
+
err,
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
270
268
|
|
|
271
269
|
throw err;
|
|
272
270
|
}
|
|
@@ -275,39 +273,39 @@ class JobManager {
|
|
|
275
273
|
}
|
|
276
274
|
|
|
277
275
|
/**
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
async addOneOffJob({name, job, data, offloaded = true}) {
|
|
276
|
+
* Adds a job that could ever be executed once. In case the job fails
|
|
277
|
+
* can be "added" again, effectively restarting the failed job.
|
|
278
|
+
*
|
|
279
|
+
* @param {Object} GhostJob - job options
|
|
280
|
+
* @prop {Function | String} GhostJob.job - function or path to a module defining a job
|
|
281
|
+
* @prop {String} GhostJob.name - unique job name, if not provided takes function name or job script filename
|
|
282
|
+
* @prop {String | Date} [GhostJob.at] - Date, cron or human readable schedule format. Manage will do immediate execution if not specified. Not supported for "inline" jobs
|
|
283
|
+
* @prop {Object} [GhostJob.data] - data to be passed into the job
|
|
284
|
+
* @prop {Boolean} [GhostJob.offloaded] - creates an "offloaded" job running in a worker thread by default. If set to "false" runs an "inline" job on the same event loop
|
|
285
|
+
*/
|
|
286
|
+
async addOneOffJob({ name, job, data, offloaded = true }) {
|
|
289
287
|
if (!name) {
|
|
290
288
|
throw new IncorrectUsageError({
|
|
291
|
-
message: `The name parameter is required for a one off job
|
|
289
|
+
message: `The name parameter is required for a one off job.`,
|
|
292
290
|
});
|
|
293
291
|
}
|
|
294
292
|
|
|
295
293
|
const persistedJob = await this._jobsRepository.read(name);
|
|
296
294
|
|
|
297
|
-
if (persistedJob &&
|
|
295
|
+
if (persistedJob && persistedJob.get('status') !== ALL_STATUSES.failed) {
|
|
298
296
|
throw new IncorrectUsageError({
|
|
299
|
-
message: `A "${name}" one off job has already been executed
|
|
297
|
+
message: `A "${name}" one off job has already been executed.`,
|
|
300
298
|
});
|
|
301
299
|
}
|
|
302
300
|
|
|
303
|
-
if (persistedJob &&
|
|
301
|
+
if (persistedJob && persistedJob.get('status') === ALL_STATUSES.failed) {
|
|
304
302
|
await this._jobsRepository.update(persistedJob.id, {
|
|
305
|
-
status: ALL_STATUSES.queued
|
|
303
|
+
status: ALL_STATUSES.queued,
|
|
306
304
|
});
|
|
307
305
|
} else {
|
|
308
306
|
await this._jobsRepository.add({
|
|
309
307
|
name,
|
|
310
|
-
status: ALL_STATUSES.queued
|
|
308
|
+
status: ALL_STATUSES.queued,
|
|
311
309
|
});
|
|
312
310
|
}
|
|
313
311
|
|
|
@@ -316,7 +314,7 @@ class JobManager {
|
|
|
316
314
|
// For example, it failed and the process was restarted.
|
|
317
315
|
// If we want to be able to restart within the same instance,
|
|
318
316
|
// we'd need to handle job restart/removal in Bree first
|
|
319
|
-
this.addJob({name, job, data, offloaded});
|
|
317
|
+
this.addJob({ name, job, data, offloaded });
|
|
320
318
|
}
|
|
321
319
|
|
|
322
320
|
/**
|
|
@@ -330,7 +328,7 @@ class JobManager {
|
|
|
330
328
|
if (!persistedJob) {
|
|
331
329
|
return false;
|
|
332
330
|
} else {
|
|
333
|
-
return
|
|
331
|
+
return persistedJob.get('status') !== ALL_STATUSES.failed;
|
|
334
332
|
}
|
|
335
333
|
} else {
|
|
336
334
|
return false;
|
|
@@ -346,7 +344,10 @@ class JobManager {
|
|
|
346
344
|
async awaitOneOffCompletion(name) {
|
|
347
345
|
const persistedJob = await this._jobsRepository.read(name);
|
|
348
346
|
|
|
349
|
-
if (
|
|
347
|
+
if (
|
|
348
|
+
!persistedJob ||
|
|
349
|
+
![ALL_STATUSES.finished, ALL_STATUSES.failed].includes(persistedJob.get('status'))
|
|
350
|
+
) {
|
|
350
351
|
// NOTE: can implement exponential backoff here if that's ever needed
|
|
351
352
|
await setTimeoutPromise(500);
|
|
352
353
|
|
|
@@ -365,7 +366,7 @@ class JobManager {
|
|
|
365
366
|
const promise = new Promise((resolve, reject) => {
|
|
366
367
|
this.#completionPromises.set(name, [
|
|
367
368
|
...(this.#completionPromises.get(name) ?? []),
|
|
368
|
-
{resolve, reject}
|
|
369
|
+
{ resolve, reject },
|
|
369
370
|
]);
|
|
370
371
|
});
|
|
371
372
|
|
|
@@ -386,7 +387,7 @@ class JobManager {
|
|
|
386
387
|
|
|
387
388
|
this.#completionPromises.set(name, [
|
|
388
389
|
...(this.#completionPromises.get(name) ?? []),
|
|
389
|
-
{resolve, reject}
|
|
390
|
+
{ resolve, reject },
|
|
390
391
|
]);
|
|
391
392
|
});
|
|
392
393
|
}
|
|
@@ -417,7 +418,7 @@ class JobManager {
|
|
|
417
418
|
|
|
418
419
|
logging.warn('Waiting for busy job in inline job queue');
|
|
419
420
|
|
|
420
|
-
const {default: pWaitFor} = await import('p-wait-for');
|
|
421
|
+
const { default: pWaitFor } = await import('p-wait-for');
|
|
421
422
|
await pWaitFor(() => this.inlineQueue.idle() === true, options);
|
|
422
423
|
|
|
423
424
|
logging.warn('Inline job queue finished');
|
package/lib/JobsRepository.js
CHANGED
|
@@ -10,7 +10,7 @@ class JobsRepository {
|
|
|
10
10
|
* @param {Object} options - The options object.
|
|
11
11
|
* @param {Object} options.JobModel - The Job model for database operations.
|
|
12
12
|
*/
|
|
13
|
-
constructor({JobModel}) {
|
|
13
|
+
constructor({ JobModel }) {
|
|
14
14
|
// NOTE: We ought to clean this up. We want to use bookshelf models for all db operations,
|
|
15
15
|
// but we use knex directly in a few places still largely for performance reasons.
|
|
16
16
|
this._JobModel = JobModel;
|
|
@@ -36,7 +36,7 @@ class JobsRepository {
|
|
|
36
36
|
* @returns {Promise<Object|null>} The job object if found, null otherwise.
|
|
37
37
|
*/
|
|
38
38
|
async read(name) {
|
|
39
|
-
const job = await this._JobModel.findOne({name});
|
|
39
|
+
const job = await this._JobModel.findOne({ name });
|
|
40
40
|
return job;
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -49,7 +49,7 @@ class JobsRepository {
|
|
|
49
49
|
* @returns {Promise<void>}
|
|
50
50
|
*/
|
|
51
51
|
async update(id, data) {
|
|
52
|
-
await this._JobModel.edit(data, {id});
|
|
52
|
+
await this._JobModel.edit(data, { id });
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
@@ -61,11 +61,11 @@ class JobsRepository {
|
|
|
61
61
|
*/
|
|
62
62
|
async delete(id) {
|
|
63
63
|
try {
|
|
64
|
-
await this._JobModel.destroy({id});
|
|
64
|
+
await this._JobModel.destroy({ id });
|
|
65
65
|
} catch (error) {
|
|
66
66
|
logging.error(`Error deleting job ${id}:`, error);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
module.exports = JobsRepository;
|
|
71
|
+
module.exports = JobsRepository;
|
package/lib/assemble-bree-job.js
CHANGED
|
@@ -12,28 +12,28 @@ const assemble = (at, job, data, name) => {
|
|
|
12
12
|
const breeJob = {
|
|
13
13
|
name: name,
|
|
14
14
|
// NOTE: both function and path syntaxes work with 'path' parameter
|
|
15
|
-
path: job
|
|
15
|
+
path: job,
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
if (data) {
|
|
19
19
|
Object.assign(breeJob, {
|
|
20
20
|
worker: {
|
|
21
|
-
workerData: data
|
|
22
|
-
}
|
|
21
|
+
workerData: data,
|
|
22
|
+
},
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
if (at instanceof Date) {
|
|
27
27
|
Object.assign(breeJob, {
|
|
28
|
-
date: at
|
|
28
|
+
date: at,
|
|
29
29
|
});
|
|
30
30
|
} else if (at && isCronExpression(at)) {
|
|
31
31
|
Object.assign(breeJob, {
|
|
32
|
-
cron: at
|
|
32
|
+
cron: at,
|
|
33
33
|
});
|
|
34
34
|
} else if (at !== undefined) {
|
|
35
35
|
Object.assign(breeJob, {
|
|
36
|
-
interval: at
|
|
36
|
+
interval: at,
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -28,8 +28,8 @@ const isCronExpression = (expression) => {
|
|
|
28
28
|
let cronResult = cronValidate(expression, {
|
|
29
29
|
preset: 'default', // second field not supported in default preset
|
|
30
30
|
override: {
|
|
31
|
-
useSeconds: true // override preset option
|
|
32
|
-
}
|
|
31
|
+
useSeconds: true, // override preset option
|
|
32
|
+
},
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
return cronResult.isValid();
|
package/package.json
CHANGED
|
@@ -1,43 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tryghost/job-manager",
|
|
3
|
-
"version": "3.0
|
|
4
|
-
"author": "Ghost Foundation",
|
|
3
|
+
"version": "3.1.0",
|
|
5
4
|
"license": "MIT",
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"dev": "echo \"Implement me!\"",
|
|
12
|
-
"test:unit": "NODE_ENV=testing vitest run --coverage",
|
|
13
|
-
"test": "yarn test:unit",
|
|
14
|
-
"lint:code": "eslint *.js lib/ --ext .js --cache",
|
|
15
|
-
"lint": "yarn lint:code && yarn lint:test",
|
|
16
|
-
"lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache"
|
|
5
|
+
"author": "Ghost Foundation",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/TryGhost/framework.git",
|
|
9
|
+
"directory": "packages/job-manager"
|
|
17
10
|
},
|
|
18
11
|
"files": [
|
|
19
12
|
"index.js",
|
|
20
13
|
"lib"
|
|
21
14
|
],
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
"rewire": "9.0.1",
|
|
26
|
-
"sinon": "21.0.2"
|
|
15
|
+
"main": "index.js",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
27
18
|
},
|
|
28
19
|
"dependencies": {
|
|
29
20
|
"@breejs/later": "4.2.0",
|
|
30
|
-
"@tryghost/errors": "^3.0.3",
|
|
31
|
-
"@tryghost/logging": "^4.0.3",
|
|
32
21
|
"bree": "9.2.9",
|
|
33
22
|
"cron-validate": "1.5.3",
|
|
34
23
|
"fastq": "1.20.1",
|
|
35
24
|
"p-wait-for": "6.0.0",
|
|
36
|
-
"workerpool": "10.0.
|
|
25
|
+
"workerpool": "10.0.2",
|
|
26
|
+
"@tryghost/errors": "3.1.0",
|
|
27
|
+
"@tryghost/logging": "4.1.0"
|
|
37
28
|
},
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@sinonjs/fake-timers": "15.3.2",
|
|
31
|
+
"date-fns": "4.1.0",
|
|
32
|
+
"rewire": "9.0.1",
|
|
33
|
+
"sinon": "21.1.2"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"dev": "echo \"Implement me!\"",
|
|
37
|
+
"test:unit": "NODE_ENV=testing vitest run --coverage",
|
|
38
|
+
"test": "pnpm run test:unit",
|
|
39
|
+
"lint": "oxlint -c ../../.oxlintrc.json ."
|
|
42
40
|
}
|
|
43
|
-
}
|
|
41
|
+
}
|