@tryghost/job-manager 0.8.25 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +93 -78
- package/index.js +1 -1
- package/lib/JobManager.js +427 -0
- package/lib/JobsRepository.js +71 -0
- package/package.json +24 -20
- package/lib/job-manager.js +0 -153
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,33 +1,70 @@
|
|
|
1
1
|
# Job Manager
|
|
2
2
|
|
|
3
|
-
A manager for jobs
|
|
3
|
+
A manager for background jobs in Ghost, supporting one-off tasks and recurring jobs.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Quick Start](#quick-start)
|
|
7
|
+
- [Job Types](#job-types)
|
|
8
|
+
- [Background Job Requirements](#background-job-requirements)
|
|
9
|
+
- [Advanced Usage](#advanced-usage)
|
|
10
|
+
- [Technical Details](#technical-details)
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
## Quick Start
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
```js
|
|
15
|
+
const JobManager = require('@tryghost/job-manager');
|
|
16
|
+
const jobManager = new JobManager({JobModel: models.Job});
|
|
17
|
+
|
|
18
|
+
// Simple one-off job
|
|
19
|
+
jobManager.addJob({
|
|
20
|
+
name: 'hello-world',
|
|
21
|
+
job: () => console.log('Hello World'),
|
|
22
|
+
offloaded: false
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Recurring job every 5 minutes
|
|
26
|
+
jobManager.addJob({
|
|
27
|
+
name: 'check-emails',
|
|
28
|
+
at: 'every 5 minutes',
|
|
29
|
+
job: './jobs/check-emails.js'
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Job Types
|
|
10
34
|
|
|
11
|
-
|
|
35
|
+
Ghost supports two types of jobs:
|
|
12
36
|
|
|
37
|
+
1. **Inline Jobs**
|
|
38
|
+
- Run in the main Ghost process
|
|
39
|
+
- Best for quick, simple tasks
|
|
40
|
+
- Cannot be scheduled or recurring
|
|
13
41
|
|
|
14
|
-
|
|
42
|
+
2. **Offloaded Jobs**
|
|
43
|
+
- Run in a separate process
|
|
44
|
+
- Good for CPU-intensive tasks
|
|
45
|
+
- Can be scheduled or recurring
|
|
15
46
|
|
|
16
|
-
|
|
47
|
+
## Background Job Requirements
|
|
48
|
+
|
|
49
|
+
Offloaded jobs must:
|
|
50
|
+
|
|
51
|
+
- Have a unique name
|
|
52
|
+
- Be idempotent (safe to run multiple times)
|
|
53
|
+
- Use minimal parameters (prefer using IDs rather than full objects)
|
|
54
|
+
- Import their own dependencies
|
|
55
|
+
- Primarily use DB or API calls
|
|
56
|
+
|
|
57
|
+
## Advanced Usage
|
|
58
|
+
|
|
59
|
+
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:
|
|
17
60
|
```js
|
|
18
61
|
const JobManager = require('@tryghost/job-manager');
|
|
19
62
|
|
|
20
|
-
const
|
|
21
|
-
info: console.log,
|
|
22
|
-
warn: console.log,
|
|
23
|
-
error: console.error
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const jobManager = new JobManager(logging);
|
|
63
|
+
const jobManager = new JobManager({JobModel: models.Job});
|
|
27
64
|
|
|
28
65
|
// register a job "function" with queued execution in current event loop
|
|
29
66
|
jobManager.addJob({
|
|
30
|
-
job:
|
|
67
|
+
job: (word) => console.log(word),
|
|
31
68
|
name: 'hello',
|
|
32
69
|
offloaded: false
|
|
33
70
|
});
|
|
@@ -55,82 +92,60 @@ jobManager.addJob({
|
|
|
55
92
|
name: 'email-checker-cron'
|
|
56
93
|
});
|
|
57
94
|
|
|
58
|
-
// register a job to
|
|
95
|
+
// register a job to run immediately running outside of current even loop
|
|
59
96
|
jobManager.addJob({
|
|
60
97
|
job: './path/to/jobs/check-emails.js',
|
|
61
98
|
name: 'email-checker-now'
|
|
62
99
|
});
|
|
63
|
-
```
|
|
64
100
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
- **"offloaded"** - job which is executed in separate to caller's event loop. For Node >v12 clients it spawns a [Worker thread](https://nodejs.org/dist/latest-v12.x/docs/api/worker_threads.html#worker_threads_new_worker_filename_options), for older Node runtimes it is executed in separate process through [child_process](https://nodejs.org/docs/latest-v10.x/api/child_process.html). Comparing to **inline** jobs, **offloaded** jobs are safer to execute as they are run on a dedicated thread (or process) acting like a sandbox. These jobs also give better utilization of multi-core CPUs. This type of jobs is useful when there are heavy computations needed to be done blocking the event loop or need a sandboxed environment to run in safely. Example jobs would be: statistical information processing, memory intensive computations (e.g. recursive algorithms), processing that requires blocking I/O operations etc.
|
|
72
|
-
|
|
73
|
-
Job manager's instance registers jobs through `addJob` method. The `offloaded` parameter controls if the job is **inline** (executed in the same event loop) or is **offloaded** (executed in worker thread/separate process). By default `offloaded` is set to `true` - creates an "offloaded" job.
|
|
101
|
+
// register a one-off job to be executed immediately within current event loop
|
|
102
|
+
jobManager.addOneOffJob({
|
|
103
|
+
name: 'members-migrations',
|
|
104
|
+
offloaded: false,
|
|
105
|
+
job: stripeService.migrations.execute.bind(stripeService.migrations)
|
|
106
|
+
});
|
|
74
107
|
|
|
75
|
-
|
|
108
|
+
// register a one-off job to be executed immediately outside of current event loop
|
|
109
|
+
jobManager.addOneOffJob({
|
|
110
|
+
name: 'generate-backup-2022-09-15',
|
|
111
|
+
job: './path/to/jobs/backup.js',
|
|
112
|
+
});
|
|
76
113
|
|
|
77
|
-
|
|
114
|
+
// optionally await completion of the one-off job in case
|
|
115
|
+
// there are state changes expected to execute the rest of the process
|
|
116
|
+
// NOTE: if multiple jobs are submitted using the same name, the first completion will resolve
|
|
117
|
+
await jobManager.awaitOneOffCompletion('members-migrations');
|
|
118
|
+
|
|
119
|
+
// check if previously registered one-off job has been executed
|
|
120
|
+
// successfully - it exists and doesn't have a "failed" state.
|
|
121
|
+
// NOTE: this is stored in memory and cleared on reboot
|
|
122
|
+
const backupSuccessful = await jobManager.hasExecutedSuccessfully('generate-backup-2022-09-15');
|
|
123
|
+
|
|
124
|
+
if (!backupSuccessful) {
|
|
125
|
+
// One-off jobs with "failed" state can be rescheduled
|
|
126
|
+
jobManager.addOneOffJob({
|
|
127
|
+
name: 'generate-backup-2022-09-15',
|
|
128
|
+
job: './path/to/jobs/backup.js',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
```
|
|
78
132
|
|
|
133
|
+
For more examples of JobManager initialization check [test/examples](https://github.com/TryGhost/Utils/tree/master/packages/job-manager/test/examples) directory.
|
|
79
134
|
|
|
80
|
-
|
|
81
|
-
To prevent complications around failed job retries and and handling of specific job states here are some rules that should be followed for all scheduled jobs:
|
|
82
|
-
1. Jobs are **self contained** - meaning job manager should be able to run the job with the state information included within the job's parameters. Job script should look up for the rest of needed information from somewhere else, like a database, API, or file.
|
|
83
|
-
2. Jobs should be [idempotent](https://en.wikipedia.org/wiki/Idempotence) - consequent job executions should be safe.
|
|
84
|
-
3. Job **parameters** should be **kept to the minimum**. When passing large amounts of data around performance can suffer from slow JSON serialization. Also, storage size restrictions that can arise if there is a need to store parameters in the future.Job parameters should be kept to only information that is needed to retrieve the rest of information from somewhere else. For example, it's recommended to pass in only an *id* of the resource that could be fetched from the data storage during job execution or pass in a file path which could be read during execution.
|
|
85
|
-
4. Scheduled **job execution time should not overlap**. It's up to the registering service to assure job execution time does not ecceed time between subsequent scheduled jobs. For example, if job is scheduled to run every 5 minutes it should always run under 5 minutes, otherwise next scheduled job would fail to start.
|
|
135
|
+
## Technical Details
|
|
86
136
|
|
|
87
|
-
###
|
|
137
|
+
### Process Isolation
|
|
138
|
+
Background jobs run in separate processes, meaning:
|
|
139
|
+
- No shared memory with Ghost
|
|
140
|
+
- Must import own dependencies
|
|
141
|
+
- Should avoid heavy dependencies
|
|
142
|
+
- Should handle graceful shutdown
|
|
88
143
|
|
|
89
|
-
|
|
90
|
-
|
|
144
|
+
### Job Lifecycle
|
|
145
|
+
Offloaded jobs are running on dedicated worker threads which makes their lifecycle a bit different from inline jobs:
|
|
146
|
+
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.
|
|
91
147
|
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.
|
|
92
148
|
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)).
|
|
93
149
|
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.
|
|
94
150
|
|
|
95
151
|
For more nuances on job structure best practices check [bree documentation](https://github.com/breejs/bree#writing-jobs-with-promises-and-async-await).
|
|
96
|
-
|
|
97
|
-
### Offloaded job script quirks
|
|
98
|
-
|
|
99
|
-
⚠️ to ensure worker thread back compatibility and correct inter-thread communication use [btrheads](https://github.com/chjj/bthreads) polyfill instead of native [worker_threads](https://nodejs.org/api/worker_threads.html#worker_threads) module in job scripts.
|
|
100
|
-
|
|
101
|
-
Instead of:
|
|
102
|
-
```js
|
|
103
|
-
const {isMainThread, parentPort} = require('worker_threads');
|
|
104
|
-
```
|
|
105
|
-
use
|
|
106
|
-
```js
|
|
107
|
-
const {isMainThread, parentPort} = require('bthreads');
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
It should be possible to use native `worker_threads` module once Node v10 [hits EOL](https://nodejs.org/en/about/releases/) (2021-04-30).
|
|
111
|
-
|
|
112
|
-
## Develop
|
|
113
|
-
|
|
114
|
-
This is a mono repository, managed with [lerna](https://lernajs.io/).
|
|
115
|
-
|
|
116
|
-
Follow the instructions for the top-level repo.
|
|
117
|
-
1. `git clone` this repo & `cd` into it as usual
|
|
118
|
-
2. Run `yarn` to install top-level dependencies.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
## Run
|
|
122
|
-
|
|
123
|
-
- `yarn dev`
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
## Test
|
|
127
|
-
|
|
128
|
-
- `yarn lint` run just eslint
|
|
129
|
-
- `yarn test` run lint and tests
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
# Copyright & License
|
|
135
|
-
|
|
136
|
-
Copyright (c) 2013-2022 Ghost Foundation - Released under the [MIT license](LICENSE).
|
package/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
module.exports = require('./lib/
|
|
1
|
+
module.exports = require('./lib/JobManager');
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const util = require('util');
|
|
3
|
+
const setTimeoutPromise = util.promisify(setTimeout);
|
|
4
|
+
const fastq = require('fastq');
|
|
5
|
+
const later = require('@breejs/later');
|
|
6
|
+
const Bree = require('bree');
|
|
7
|
+
const pWaitFor = require('p-wait-for');
|
|
8
|
+
const {UnhandledJobError, IncorrectUsageError} = require('@tryghost/errors');
|
|
9
|
+
const logging = require('@tryghost/logging');
|
|
10
|
+
const isCronExpression = require('./is-cron-expression');
|
|
11
|
+
const assembleBreeJob = require('./assemble-bree-job');
|
|
12
|
+
const JobsRepository = require('./JobsRepository');
|
|
13
|
+
|
|
14
|
+
const worker = async (task, callback) => {
|
|
15
|
+
try {
|
|
16
|
+
let result = await task();
|
|
17
|
+
await callback(null, result);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
await callback(error);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const ALL_STATUSES = {
|
|
24
|
+
started: 'started',
|
|
25
|
+
finished: 'finished',
|
|
26
|
+
failed: 'failed',
|
|
27
|
+
queued: 'queued'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {Object} ScheduledJob
|
|
32
|
+
* @property {Function | string} job - Function or path to a module defining a job
|
|
33
|
+
* @property {string} [name] - Unique job name, if not provided takes function name or job script filename
|
|
34
|
+
* @property {string | Date} [at] - Date, cron or human readable schedule format
|
|
35
|
+
* @property {Object} [data] - Data to be passed into the job
|
|
36
|
+
* @property {boolean} [offloaded=true] - If true, creates an "offloaded" job running in a worker thread. If false, runs an "inline" job on the same event loop
|
|
37
|
+
*/
|
|
38
|
+
class JobManager {
|
|
39
|
+
#domainEvents;
|
|
40
|
+
#completionPromises = new Map();
|
|
41
|
+
#config;
|
|
42
|
+
#JobModel;
|
|
43
|
+
#events;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {Object} options
|
|
47
|
+
* @param {Function} [options.errorHandler] - custom job error handler
|
|
48
|
+
* @param {Function} [options.workerMessageHandler] - custom message handler coming from workers
|
|
49
|
+
* @param {Object} [options.JobModel] - a model which can persist job data in the storage
|
|
50
|
+
* @param {Object} [options.domainEvents] - domain events emitter
|
|
51
|
+
* @param {Object} [options.config] - config
|
|
52
|
+
* @param {Object} [options.events] - events instance (for testing)
|
|
53
|
+
*/
|
|
54
|
+
constructor({errorHandler, workerMessageHandler, JobModel, domainEvents, config, events = null}) {
|
|
55
|
+
this.inlineQueue = fastq(this, worker, 3);
|
|
56
|
+
this._jobMessageHandler = this._jobMessageHandler.bind(this);
|
|
57
|
+
this._jobErrorHandler = this._jobErrorHandler.bind(this);
|
|
58
|
+
this.#domainEvents = domainEvents;
|
|
59
|
+
this.#config = config;
|
|
60
|
+
this.#JobModel = JobModel;
|
|
61
|
+
this.#events = events;
|
|
62
|
+
|
|
63
|
+
const combinedMessageHandler = workerMessageHandler
|
|
64
|
+
? ({name, message}) => {
|
|
65
|
+
workerMessageHandler({name, message});
|
|
66
|
+
this._jobMessageHandler({name, message});
|
|
67
|
+
}
|
|
68
|
+
: this._jobMessageHandler;
|
|
69
|
+
|
|
70
|
+
const combinedErrorHandler = errorHandler
|
|
71
|
+
? (error, workerMeta) => {
|
|
72
|
+
errorHandler(error, workerMeta);
|
|
73
|
+
this._jobErrorHandler(error, workerMeta);
|
|
74
|
+
}
|
|
75
|
+
: this._jobErrorHandler;
|
|
76
|
+
|
|
77
|
+
this.bree = new Bree({
|
|
78
|
+
root: false, // set this to `false` to prevent requiring a root directory of jobs
|
|
79
|
+
hasSeconds: true, // precision is needed to avoid task overlaps after immediate execution
|
|
80
|
+
outputWorkerMetadata: true,
|
|
81
|
+
logger: logging,
|
|
82
|
+
errorHandler: combinedErrorHandler,
|
|
83
|
+
workerMessageHandler: combinedMessageHandler
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
this.bree.on('worker created', (name) => {
|
|
87
|
+
this._jobMessageHandler({name, message: ALL_STATUSES.started});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (JobModel) {
|
|
91
|
+
this._jobsRepository = new JobsRepository({JobModel});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
inlineJobHandler(jobName) {
|
|
96
|
+
return async (error, result) => {
|
|
97
|
+
if (error) {
|
|
98
|
+
await this._jobErrorHandler(error, {
|
|
99
|
+
name: jobName
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
await this._jobMessageHandler({
|
|
103
|
+
name: jobName,
|
|
104
|
+
message: 'done'
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Can potentially standardize the result here
|
|
109
|
+
return result;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async _jobMessageHandler({name, message}) {
|
|
114
|
+
if (name) {
|
|
115
|
+
if (message === ALL_STATUSES.started) {
|
|
116
|
+
if (this._jobsRepository) {
|
|
117
|
+
const job = await this._jobsRepository.read(name);
|
|
118
|
+
|
|
119
|
+
if (job) {
|
|
120
|
+
await this._jobsRepository.update(job.id, {
|
|
121
|
+
status: ALL_STATUSES.started,
|
|
122
|
+
started_at: new Date()
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} else if (message === 'done') {
|
|
127
|
+
if (this._jobsRepository) {
|
|
128
|
+
const job = await this._jobsRepository.read(name);
|
|
129
|
+
|
|
130
|
+
if (job) {
|
|
131
|
+
await this._jobsRepository.update(job.id, {
|
|
132
|
+
status: ALL_STATUSES.finished,
|
|
133
|
+
finished_at: new Date()
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check completion listeners
|
|
139
|
+
if (this.#completionPromises.has(name)) {
|
|
140
|
+
for (const listeners of this.#completionPromises.get(name)) {
|
|
141
|
+
listeners.resolve();
|
|
142
|
+
}
|
|
143
|
+
// Clear the listeners
|
|
144
|
+
this.#completionPromises.delete(name);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (this.inlineQueue.length() <= 1) {
|
|
148
|
+
if (this.#completionPromises.has('all')) {
|
|
149
|
+
for (const listeners of this.#completionPromises.get('all')) {
|
|
150
|
+
listeners.resolve();
|
|
151
|
+
}
|
|
152
|
+
// Clear the listeners
|
|
153
|
+
this.#completionPromises.delete('all');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
if (typeof message === 'object' && this.#domainEvents) {
|
|
158
|
+
// Is this an event?
|
|
159
|
+
if (message.event) {
|
|
160
|
+
this.#domainEvents.dispatchRaw(message.event.type, message.event.data);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async _jobErrorHandler(error, jobMeta) {
|
|
168
|
+
if (this._jobsRepository && jobMeta.name) {
|
|
169
|
+
const job = await this._jobsRepository.read(jobMeta.name);
|
|
170
|
+
|
|
171
|
+
if (job) {
|
|
172
|
+
await this._jobsRepository.update(job.id, {
|
|
173
|
+
status: ALL_STATUSES.failed
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check completion listeners and call them with error
|
|
179
|
+
if (this.#completionPromises.has(jobMeta.name)) {
|
|
180
|
+
for (const listeners of this.#completionPromises.get(jobMeta.name)) {
|
|
181
|
+
listeners.reject(error);
|
|
182
|
+
}
|
|
183
|
+
// Clear the listeners
|
|
184
|
+
this.#completionPromises.delete(jobMeta.name);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (this.inlineQueue.length() <= 1) {
|
|
188
|
+
if (this.#completionPromises.has('all')) {
|
|
189
|
+
for (const listeners of this.#completionPromises.get('all')) {
|
|
190
|
+
listeners.reject(error);
|
|
191
|
+
}
|
|
192
|
+
// Clear the listeners
|
|
193
|
+
this.#completionPromises.delete('all');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* By default schedules an "offloaded" job. If `offloaded: true` parameter is set,
|
|
200
|
+
* puts an "inline" immediate job into the inlineQueue.
|
|
201
|
+
*
|
|
202
|
+
* @param {Object} GhostJob - job options
|
|
203
|
+
* @prop {Function | String} GhostJob.job - function or path to a module defining a job
|
|
204
|
+
* @prop {String} [GhostJob.name] - unique job name, if not provided takes function name or job script filename
|
|
205
|
+
* @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
|
|
206
|
+
* @prop {Object} [GhostJob.data] - data to be passed into the job
|
|
207
|
+
* @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
|
|
208
|
+
*/
|
|
209
|
+
addJob({name, at, job, data, offloaded = true}) {
|
|
210
|
+
if (offloaded) {
|
|
211
|
+
logging.info('Adding offloaded job to the inline job queue');
|
|
212
|
+
let schedule;
|
|
213
|
+
|
|
214
|
+
if (!name) {
|
|
215
|
+
if (typeof job === 'string') {
|
|
216
|
+
name = path.parse(job).name;
|
|
217
|
+
} else {
|
|
218
|
+
throw new IncorrectUsageError({
|
|
219
|
+
message: 'Name parameter should be present if job is a function'
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (at && !(at instanceof Date)) {
|
|
225
|
+
if (isCronExpression(at)) {
|
|
226
|
+
schedule = later.parse.cron(at, true);
|
|
227
|
+
} else {
|
|
228
|
+
schedule = later.parse.text(at);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if ((schedule.error && schedule.error !== -1) || schedule.schedules.length === 0) {
|
|
232
|
+
throw new IncorrectUsageError({
|
|
233
|
+
message: 'Invalid schedule format'
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
logging.info(`Scheduling job ${name} at ${at}. Next run on: ${later.schedule(schedule).next()}`);
|
|
238
|
+
} else if (at !== undefined) {
|
|
239
|
+
logging.info(`Scheduling job ${name} at ${at}`);
|
|
240
|
+
} else {
|
|
241
|
+
logging.info(`Scheduling job ${name} to run immediately`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const breeJob = assembleBreeJob(at, job, data, name);
|
|
245
|
+
this.bree.add(breeJob);
|
|
246
|
+
return this.bree.start(name);
|
|
247
|
+
} else {
|
|
248
|
+
logging.info(`Adding one-off job to inlineQueue with current length = ${this.inlineQueue.length()} called '${name || 'anonymous'}'`);
|
|
249
|
+
|
|
250
|
+
this.inlineQueue.push(async () => {
|
|
251
|
+
try {
|
|
252
|
+
// NOTE: setting the status here otherwise it is impossible to
|
|
253
|
+
// distinguish between states when the job fails immediately
|
|
254
|
+
await this._jobMessageHandler({
|
|
255
|
+
name: name,
|
|
256
|
+
message: ALL_STATUSES.started
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (typeof job === 'function') {
|
|
260
|
+
await job(data);
|
|
261
|
+
} else {
|
|
262
|
+
await require(job)(data);
|
|
263
|
+
}
|
|
264
|
+
} catch (err) {
|
|
265
|
+
// NOTE: each job should be written in a safe way and handle all errors internally
|
|
266
|
+
// if the error is caught here jobs implementation should be changed
|
|
267
|
+
logging.error(new UnhandledJobError({
|
|
268
|
+
context: (typeof job === 'function') ? 'function' : job,
|
|
269
|
+
err
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
throw err;
|
|
273
|
+
}
|
|
274
|
+
}, this.inlineJobHandler(name));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Adds a job that could ever be executed once. In case the job fails
|
|
280
|
+
* can be "added" again, effectively restarting the failed job.
|
|
281
|
+
*
|
|
282
|
+
* @param {Object} GhostJob - job options
|
|
283
|
+
* @prop {Function | String} GhostJob.job - function or path to a module defining a job
|
|
284
|
+
* @prop {String} GhostJob.name - unique job name, if not provided takes function name or job script filename
|
|
285
|
+
* @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
|
|
286
|
+
* @prop {Object} [GhostJob.data] - data to be passed into the job
|
|
287
|
+
* @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
|
|
288
|
+
*/
|
|
289
|
+
async addOneOffJob({name, job, data, offloaded = true}) {
|
|
290
|
+
if (!name) {
|
|
291
|
+
throw new IncorrectUsageError({
|
|
292
|
+
message: `The name parameter is required for a one off job.`
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const persistedJob = await this._jobsRepository.read(name);
|
|
297
|
+
|
|
298
|
+
if (persistedJob && (persistedJob.get('status') !== ALL_STATUSES.failed)) {
|
|
299
|
+
throw new IncorrectUsageError({
|
|
300
|
+
message: `A "${name}" one off job has already been executed.`
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (persistedJob && (persistedJob.get('status') === ALL_STATUSES.failed)) {
|
|
305
|
+
await this._jobsRepository.update(persistedJob.id, {
|
|
306
|
+
status: ALL_STATUSES.queued
|
|
307
|
+
});
|
|
308
|
+
} else {
|
|
309
|
+
await this._jobsRepository.add({
|
|
310
|
+
name,
|
|
311
|
+
status: ALL_STATUSES.queued
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// NOTE: there's a assumption the job with the same name failed while
|
|
316
|
+
// running under different instance of job manager (bree).
|
|
317
|
+
// For example, it failed and the process was restarted.
|
|
318
|
+
// If we want to be able to restart within the same instance,
|
|
319
|
+
// we'd need to handle job restart/removal in Bree first
|
|
320
|
+
this.addJob({name, job, data, offloaded});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Checks if the one-off job has ever been executed successfully.
|
|
325
|
+
* @param {String} name one-off job name
|
|
326
|
+
*/
|
|
327
|
+
async hasExecutedSuccessfully(name) {
|
|
328
|
+
if (this._jobsRepository) {
|
|
329
|
+
const persistedJob = await this._jobsRepository.read(name);
|
|
330
|
+
|
|
331
|
+
if (!persistedJob) {
|
|
332
|
+
return false;
|
|
333
|
+
} else {
|
|
334
|
+
return (persistedJob.get('status') !== ALL_STATUSES.failed);
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Awaits completion of the offloaded one-off job.
|
|
343
|
+
* CAUTION: it might take a long time to resolve!
|
|
344
|
+
* @param {string} name one-off job name
|
|
345
|
+
* @returns resolves with a Job model at current state
|
|
346
|
+
*/
|
|
347
|
+
async awaitOneOffCompletion(name) {
|
|
348
|
+
const persistedJob = await this._jobsRepository.read(name);
|
|
349
|
+
|
|
350
|
+
if (!persistedJob || ![ALL_STATUSES.finished, ALL_STATUSES.failed].includes(persistedJob.get('status'))) {
|
|
351
|
+
// NOTE: can implement exponential backoff here if that's ever needed
|
|
352
|
+
await setTimeoutPromise(500);
|
|
353
|
+
|
|
354
|
+
return this.awaitOneOffCompletion(name);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return persistedJob;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/***
|
|
361
|
+
* Create this promise before you add the job you want to listen for. Then await the returned promise.
|
|
362
|
+
* Resolves if the job has been executed successfully.
|
|
363
|
+
* Throws an error if the job has failed execution.
|
|
364
|
+
*/
|
|
365
|
+
async awaitCompletion(name) {
|
|
366
|
+
const promise = new Promise((resolve, reject) => {
|
|
367
|
+
this.#completionPromises.set(name, [
|
|
368
|
+
...(this.#completionPromises.get(name) ?? []),
|
|
369
|
+
{resolve, reject}
|
|
370
|
+
]);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
return promise;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Wait for all inline jobs to be completed.
|
|
378
|
+
*/
|
|
379
|
+
async allSettled() {
|
|
380
|
+
const name = 'all';
|
|
381
|
+
|
|
382
|
+
return new Promise((resolve, reject) => {
|
|
383
|
+
if (this.inlineQueue.idle()) {
|
|
384
|
+
resolve();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
this.#completionPromises.set(name, [
|
|
389
|
+
...(this.#completionPromises.get(name) ?? []),
|
|
390
|
+
{resolve, reject}
|
|
391
|
+
]);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Removes an "offloaded" job from scheduled jobs inlineQueue.
|
|
397
|
+
* It's NOT yet possible to remove "inline" jobs (will be possible when scheduling is added https://github.com/breejs/bree/issues/68).
|
|
398
|
+
* The method will throw an Error if job with provided name does not exist.
|
|
399
|
+
*
|
|
400
|
+
* NOTE: current implementation does not guarante running job termination
|
|
401
|
+
* for details see https://github.com/breejs/bree/pull/64
|
|
402
|
+
*
|
|
403
|
+
* @param {String} name - job name
|
|
404
|
+
*/
|
|
405
|
+
async removeJob(name) {
|
|
406
|
+
await this.bree.remove(name);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* @param {import('p-wait-for').Options} [options]
|
|
411
|
+
*/
|
|
412
|
+
async shutdown(options) {
|
|
413
|
+
await this.bree.stop();
|
|
414
|
+
|
|
415
|
+
if (this.inlineQueue.idle()) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
logging.warn('Waiting for busy job in inline job queue');
|
|
420
|
+
|
|
421
|
+
await pWaitFor(() => this.inlineQueue.idle() === true, options);
|
|
422
|
+
|
|
423
|
+
logging.warn('Inline job queue finished');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
module.exports = JobManager;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @class JobsRepository
|
|
5
|
+
* @description Repository class for managing job-related operations.
|
|
6
|
+
*/
|
|
7
|
+
class JobsRepository {
|
|
8
|
+
/**
|
|
9
|
+
* @constructor
|
|
10
|
+
* @param {Object} options - The options object.
|
|
11
|
+
* @param {Object} options.JobModel - The Job model for database operations.
|
|
12
|
+
*/
|
|
13
|
+
constructor({JobModel}) {
|
|
14
|
+
// NOTE: We ought to clean this up. We want to use bookshelf models for all db operations,
|
|
15
|
+
// but we use knex directly in a few places still largely for performance reasons.
|
|
16
|
+
this._JobModel = JobModel;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @method add
|
|
21
|
+
* @async
|
|
22
|
+
* @description Adds a new job to the database.
|
|
23
|
+
* @param {Object} data - The job data to be added.
|
|
24
|
+
* @returns {Promise<Object>} The added job object.
|
|
25
|
+
*/
|
|
26
|
+
async add(data) {
|
|
27
|
+
const job = await this._JobModel.add(data);
|
|
28
|
+
return job;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @method read
|
|
33
|
+
* @async
|
|
34
|
+
* @description Reads a job from the database by name.
|
|
35
|
+
* @param {string} name - The name of the job to read.
|
|
36
|
+
* @returns {Promise<Object|null>} The job object if found, null otherwise.
|
|
37
|
+
*/
|
|
38
|
+
async read(name) {
|
|
39
|
+
const job = await this._JobModel.findOne({name});
|
|
40
|
+
return job;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @method update
|
|
45
|
+
* @async
|
|
46
|
+
* @description Updates a job in the database.
|
|
47
|
+
* @param {string} id - The ID of the job to update.
|
|
48
|
+
* @param {Object} data - The updated job data.
|
|
49
|
+
* @returns {Promise<void>}
|
|
50
|
+
*/
|
|
51
|
+
async update(id, data) {
|
|
52
|
+
await this._JobModel.edit(data, {id});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @method delete
|
|
57
|
+
* @async
|
|
58
|
+
* @description Deletes a job from the database.
|
|
59
|
+
* @param {string} id - The ID of the job to delete.
|
|
60
|
+
* @returns {Promise<void>}
|
|
61
|
+
*/
|
|
62
|
+
async delete(id) {
|
|
63
|
+
try {
|
|
64
|
+
await this._JobModel.destroy({id});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
logging.error(`Error deleting job ${id}:`, error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = JobsRepository;
|
package/package.json
CHANGED
|
@@ -1,39 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tryghost/job-manager",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"repository": "https://github.com/TryGhost/Utils/tree/main/packages/job-manager",
|
|
3
|
+
"version": "1.0.0",
|
|
5
4
|
"author": "Ghost Foundation",
|
|
6
5
|
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
7
9
|
"main": "index.js",
|
|
8
10
|
"scripts": {
|
|
9
11
|
"dev": "echo \"Implement me!\"",
|
|
10
|
-
"test": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
12
|
+
"test:unit": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura -- mocha --reporter dot './test/**/*.test.js'",
|
|
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"
|
|
13
17
|
},
|
|
14
18
|
"files": [
|
|
15
19
|
"index.js",
|
|
16
20
|
"lib"
|
|
17
21
|
],
|
|
18
|
-
"publishConfig": {
|
|
19
|
-
"access": "public"
|
|
20
|
-
},
|
|
21
22
|
"devDependencies": {
|
|
22
|
-
"@sinonjs/fake-timers": "
|
|
23
|
-
"c8": "
|
|
24
|
-
"date-fns": "2.
|
|
23
|
+
"@sinonjs/fake-timers": "11.3.1",
|
|
24
|
+
"c8": "8.0.1",
|
|
25
|
+
"date-fns": "2.30.0",
|
|
25
26
|
"delay": "5.0.0",
|
|
26
|
-
"mocha": "10.
|
|
27
|
+
"mocha": "10.8.2",
|
|
28
|
+
"rewire": "7.0.0",
|
|
27
29
|
"should": "13.2.3",
|
|
28
|
-
"sinon": "
|
|
30
|
+
"sinon": "15.2.0"
|
|
29
31
|
},
|
|
30
32
|
"dependencies": {
|
|
31
|
-
"@breejs/later": "
|
|
32
|
-
"@tryghost/
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
33
|
+
"@breejs/later": "4.2.0",
|
|
34
|
+
"@tryghost/errors": "1.3.7",
|
|
35
|
+
"@tryghost/logging": "2.4.21",
|
|
36
|
+
"bree": "6.5.0",
|
|
37
|
+
"cron-validate": "1.4.5",
|
|
38
|
+
"fastq": "1.19.1",
|
|
39
|
+
"p-wait-for": "3.2.0",
|
|
40
|
+
"workerpool": "9.2.0"
|
|
37
41
|
},
|
|
38
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "feac71f0032c5d92f56a8dfa7ccd5a2d883ad53d"
|
|
39
43
|
}
|
package/lib/job-manager.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
|
-
const fastq = require('fastq');
|
|
3
|
-
const later = require('@breejs/later');
|
|
4
|
-
const Bree = require('bree');
|
|
5
|
-
const pWaitFor = require('p-wait-for');
|
|
6
|
-
const {UnhandledJobError, IncorrectUsageError} = require('@tryghost/errors');
|
|
7
|
-
const logging = require('@tryghost/logging');
|
|
8
|
-
const isCronExpression = require('./is-cron-expression');
|
|
9
|
-
const assembleBreeJob = require('./assemble-bree-job');
|
|
10
|
-
|
|
11
|
-
const worker = async (task, callback) => {
|
|
12
|
-
try {
|
|
13
|
-
let result = await task();
|
|
14
|
-
callback(null, result);
|
|
15
|
-
} catch (error) {
|
|
16
|
-
callback(error);
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const handler = (error, result) => {
|
|
21
|
-
if (error) {
|
|
22
|
-
// TODO: this handler should not be throwing as this blocks the queue
|
|
23
|
-
// throw error;
|
|
24
|
-
}
|
|
25
|
-
// Can potentially standardise the result here
|
|
26
|
-
return result;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
class JobManager {
|
|
30
|
-
/**
|
|
31
|
-
* @param {Object} options
|
|
32
|
-
* @param {Function} [options.errorHandler] - custom job error handler
|
|
33
|
-
* @param {Function} [options.workerMessageHandler] - custom message handler coming from workers
|
|
34
|
-
*/
|
|
35
|
-
constructor({errorHandler, workerMessageHandler}) {
|
|
36
|
-
this.queue = fastq(this, worker, 1);
|
|
37
|
-
|
|
38
|
-
this.bree = new Bree({
|
|
39
|
-
root: false, // set this to `false` to prevent requiring a root directory of jobs
|
|
40
|
-
hasSeconds: true, // precision is needed to avoid task overlaps after immediate execution
|
|
41
|
-
outputWorkerMetadata: true,
|
|
42
|
-
logger: logging,
|
|
43
|
-
errorHandler: errorHandler,
|
|
44
|
-
workerMessageHandler: workerMessageHandler
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* By default schedules an "offloaded" job. If `offloaded: true` parameter is set,
|
|
50
|
-
* puts an "inline" immediate job into the queue.
|
|
51
|
-
*
|
|
52
|
-
* @param {Object} GhostJob - job options
|
|
53
|
-
* @prop {Function | String} GhostJob.job - function or path to a module defining a job
|
|
54
|
-
* @prop {String} [GhostJob.name] - unique job name, if not provided takes function name or job script filename
|
|
55
|
-
* @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
|
|
56
|
-
* @prop {Object} [GhostJob.data] - data to be passed into the job
|
|
57
|
-
* @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
|
|
58
|
-
*/
|
|
59
|
-
addJob({name, at, job, data, offloaded = true}) {
|
|
60
|
-
if (offloaded) {
|
|
61
|
-
logging.info('Adding offloaded job to the queue');
|
|
62
|
-
let schedule;
|
|
63
|
-
|
|
64
|
-
if (!name) {
|
|
65
|
-
if (typeof job === 'string') {
|
|
66
|
-
name = path.parse(job).name;
|
|
67
|
-
} else {
|
|
68
|
-
throw new IncorrectUsageError({
|
|
69
|
-
message: 'Name parameter should be present if job is a function'
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (at && !(at instanceof Date)) {
|
|
75
|
-
if (isCronExpression(at)) {
|
|
76
|
-
schedule = later.parse.cron(at, true);
|
|
77
|
-
} else {
|
|
78
|
-
schedule = later.parse.text(at);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if ((schedule.error && schedule.error !== -1) || schedule.schedules.length === 0) {
|
|
82
|
-
throw new IncorrectUsageError({
|
|
83
|
-
message: 'Invalid schedule format'
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
logging.info(`Scheduling job ${name} at ${at}. Next run on: ${later.schedule(schedule).next()}`);
|
|
88
|
-
} else if (at !== undefined) {
|
|
89
|
-
logging.info(`Scheduling job ${name} at ${at}`);
|
|
90
|
-
} else {
|
|
91
|
-
logging.info(`Scheduling job ${name} to run immediately`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const breeJob = assembleBreeJob(at, job, data, name);
|
|
95
|
-
this.bree.add(breeJob);
|
|
96
|
-
return this.bree.start(name);
|
|
97
|
-
} else {
|
|
98
|
-
logging.info('Adding one off inline job to the queue');
|
|
99
|
-
|
|
100
|
-
this.queue.push(async () => {
|
|
101
|
-
try {
|
|
102
|
-
if (typeof job === 'function') {
|
|
103
|
-
await job(data);
|
|
104
|
-
} else {
|
|
105
|
-
await require(job)(data);
|
|
106
|
-
}
|
|
107
|
-
} catch (err) {
|
|
108
|
-
// NOTE: each job should be written in a safe way and handle all errors internally
|
|
109
|
-
// if the error is caught here jobs implementaton should be changed
|
|
110
|
-
logging.error(new UnhandledJobError({
|
|
111
|
-
context: (typeof job === 'function') ? 'function' : job,
|
|
112
|
-
err
|
|
113
|
-
}));
|
|
114
|
-
|
|
115
|
-
throw err;
|
|
116
|
-
}
|
|
117
|
-
}, handler);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Removes an "offloaded" job from scheduled jobs queue.
|
|
123
|
-
* It's NOT yet possible to remove "inline" jobs (will be possible when scheduling is added https://github.com/breejs/bree/issues/68).
|
|
124
|
-
* The method will throw an Error if job with provided name does not exist.
|
|
125
|
-
*
|
|
126
|
-
* NOTE: current implementation does not guarante running job termination
|
|
127
|
-
* for details see https://github.com/breejs/bree/pull/64
|
|
128
|
-
*
|
|
129
|
-
* @param {String} name - job name
|
|
130
|
-
*/
|
|
131
|
-
async removeJob(name) {
|
|
132
|
-
await this.bree.remove(name);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* @param {import('p-wait-for').Options} [options]
|
|
137
|
-
*/
|
|
138
|
-
async shutdown(options) {
|
|
139
|
-
await this.bree.stop();
|
|
140
|
-
|
|
141
|
-
if (this.queue.idle()) {
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
logging.warn('Waiting for busy job queue');
|
|
146
|
-
|
|
147
|
-
await pWaitFor(() => this.queue.idle() === true, options);
|
|
148
|
-
|
|
149
|
-
logging.warn('Job queue finished');
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
module.exports = JobManager;
|