mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-30 21:40:39 +03:00
6161f94910
refs: https://github.com/TryGhost/Toolbox/issues/595 We're rolling out new rules around the node assert library, the first of which is enforcing the use of assert/strict. This means we don't need to use the strict version of methods, as the standard version will work that way by default. This caught some gotchas in our existing usage of assert where the lack of strict mode had unexpected results: - Url matching needs to be done on `url.href` seeaa58b354a4
- Null and undefined are not the same thing, there were a few cases of this being confused - Particularly questionable changes in [PostExporter tests](c1a468744b
) tracked [here](https://github.com/TryGhost/Team/issues/3505). - A typo seeeaac9c293a
Moving forward, using assert strict should help us to catch unexpected behaviour, particularly around nulls and undefineds during implementation.
705 lines
26 KiB
JavaScript
705 lines
26 KiB
JavaScript
// Switch these lines once there are useful utils
|
|
// const testUtils = require('./utils');
|
|
require('./utils');
|
|
const assert = require('assert/strict');
|
|
const path = require('path');
|
|
const sinon = require('sinon');
|
|
const delay = require('delay');
|
|
const FakeTimers = require('@sinonjs/fake-timers');
|
|
const logging = require('@tryghost/logging');
|
|
|
|
const JobManager = require('../index');
|
|
|
|
const sandbox = sinon.createSandbox();
|
|
|
|
const jobModelInstance = {
|
|
id: 'unique',
|
|
get: (field) => {
|
|
if (field === 'status') {
|
|
return 'finished';
|
|
}
|
|
}
|
|
};
|
|
|
|
describe('Job Manager', function () {
|
|
beforeEach(function () {
|
|
sandbox.stub(logging, 'info');
|
|
sandbox.stub(logging, 'warn');
|
|
sandbox.stub(logging, 'error');
|
|
});
|
|
|
|
afterEach(function () {
|
|
sandbox.restore();
|
|
});
|
|
|
|
it('public interface', function () {
|
|
const jobManager = new JobManager({});
|
|
|
|
should.exist(jobManager.addJob);
|
|
should.exist(jobManager.hasExecutedSuccessfully);
|
|
should.exist(jobManager.awaitOneOffCompletion);
|
|
});
|
|
|
|
describe('Add a job', function () {
|
|
describe('Inline jobs', function () {
|
|
it('adds a job to a queue', async function () {
|
|
const spy = sinon.spy();
|
|
const jobManager = new JobManager({
|
|
JobModel: sinon.stub().resolves()
|
|
});
|
|
|
|
jobManager.addJob({
|
|
job: spy,
|
|
data: 'test data',
|
|
offloaded: false
|
|
});
|
|
should(jobManager.queue.idle()).be.false();
|
|
|
|
// give time to execute the job
|
|
await delay(1);
|
|
|
|
should(jobManager.queue.idle()).be.true();
|
|
should(spy.called).be.true();
|
|
should(spy.args[0][0]).equal('test data');
|
|
});
|
|
|
|
it('handles failed job gracefully', async function () {
|
|
const spy = sinon.stub().throws();
|
|
const jobModelSpy = {
|
|
findOne: sinon.spy()
|
|
};
|
|
const jobManager = new JobManager({
|
|
JobModel: jobModelSpy
|
|
});
|
|
|
|
jobManager.addJob({
|
|
job: spy,
|
|
data: 'test data',
|
|
offloaded: false
|
|
});
|
|
should(jobManager.queue.idle()).be.false();
|
|
|
|
// give time to execute the job
|
|
await delay(1);
|
|
|
|
should(jobManager.queue.idle()).be.true();
|
|
should(spy.called).be.true();
|
|
should(spy.args[0][0]).equal('test data');
|
|
should(logging.error.called).be.true();
|
|
// a one-off job without a name should not have persistance
|
|
should(jobModelSpy.findOne.called).be.false();
|
|
});
|
|
});
|
|
|
|
describe('Offloaded jobs', function () {
|
|
it('fails to schedule for invalid scheduling expression', function () {
|
|
const jobManager = new JobManager({});
|
|
|
|
try {
|
|
jobManager.addJob({
|
|
at: 'invalid expression',
|
|
name: 'jobName'
|
|
});
|
|
} catch (err) {
|
|
err.message.should.equal('Invalid schedule format');
|
|
}
|
|
});
|
|
|
|
it('fails to schedule for no job name', function () {
|
|
const jobManager = new JobManager({});
|
|
|
|
try {
|
|
jobManager.addJob({
|
|
at: 'invalid expression',
|
|
job: () => {}
|
|
});
|
|
} catch (err) {
|
|
err.message.should.equal('Name parameter should be present if job is a function');
|
|
}
|
|
});
|
|
|
|
it('schedules a job using date format', async function () {
|
|
const jobManager = new JobManager({});
|
|
const timeInTenSeconds = new Date(Date.now() + 10);
|
|
const jobPath = path.resolve(__dirname, './jobs/simple.js');
|
|
|
|
const clock = FakeTimers.install({now: Date.now()});
|
|
jobManager.addJob({
|
|
at: timeInTenSeconds,
|
|
job: jobPath,
|
|
name: 'job-in-ten'
|
|
});
|
|
|
|
should(jobManager.bree.timeouts['job-in-ten']).type('object');
|
|
should(jobManager.bree.workers['job-in-ten']).type('undefined');
|
|
|
|
// allow to run the job and start the worker
|
|
await clock.nextAsync();
|
|
|
|
should(jobManager.bree.workers['job-in-ten']).type('object');
|
|
|
|
const promise = new Promise((resolve, reject) => {
|
|
jobManager.bree.workers['job-in-ten'].on('error', reject);
|
|
jobManager.bree.workers['job-in-ten'].on('exit', (code) => {
|
|
should(code).equal(0);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
// allow job to finish execution and exit
|
|
clock.next();
|
|
|
|
await promise;
|
|
|
|
should(jobManager.bree.workers['job-in-ten']).type('undefined');
|
|
|
|
clock.uninstall();
|
|
});
|
|
|
|
it('schedules a job to run immediately', async function () {
|
|
const jobManager = new JobManager({});
|
|
const clock = FakeTimers.install({now: Date.now()});
|
|
|
|
const jobPath = path.resolve(__dirname, './jobs/simple.js');
|
|
jobManager.addJob({
|
|
job: jobPath,
|
|
name: 'job-now'
|
|
});
|
|
|
|
should(jobManager.bree.timeouts['job-now']).type('object');
|
|
|
|
// allow scheduler to pick up the job
|
|
clock.tick(1);
|
|
|
|
should(jobManager.bree.workers['job-now']).type('object');
|
|
|
|
const promise = new Promise((resolve, reject) => {
|
|
jobManager.bree.workers['job-now'].on('error', reject);
|
|
jobManager.bree.workers['job-now'].on('exit', (code) => {
|
|
should(code).equal(0);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
await promise;
|
|
|
|
should(jobManager.bree.workers['job-now']).type('undefined');
|
|
|
|
clock.uninstall();
|
|
});
|
|
|
|
it('fails to schedule a job with the same name to run immediately one after another', async function () {
|
|
const jobManager = new JobManager({});
|
|
const clock = FakeTimers.install({now: Date.now()});
|
|
|
|
const jobPath = path.resolve(__dirname, './jobs/simple.js');
|
|
jobManager.addJob({
|
|
job: jobPath,
|
|
name: 'job-now'
|
|
});
|
|
|
|
should(jobManager.bree.timeouts['job-now']).type('object');
|
|
|
|
// allow scheduler to pick up the job
|
|
clock.tick(1);
|
|
|
|
should(jobManager.bree.workers['job-now']).type('object');
|
|
|
|
const promise = new Promise((resolve, reject) => {
|
|
jobManager.bree.workers['job-now'].on('error', reject);
|
|
jobManager.bree.workers['job-now'].on('exit', (code) => {
|
|
should(code).equal(0);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
await promise;
|
|
|
|
should(jobManager.bree.workers['job-now']).type('undefined');
|
|
|
|
(() => {
|
|
jobManager.addJob({
|
|
job: jobPath,
|
|
name: 'job-now'
|
|
});
|
|
}).should.throw('Job #1 has a duplicate job name of job-now');
|
|
|
|
clock.uninstall();
|
|
});
|
|
|
|
it('uses custom error handler when job fails', async function (){
|
|
let job = function namedJob() {
|
|
throw new Error('job error');
|
|
};
|
|
const spyHandler = sinon.spy();
|
|
const jobManager = new JobManager({errorHandler: spyHandler});
|
|
const completion = jobManager.awaitCompletion('will-fail');
|
|
|
|
jobManager.addJob({
|
|
job,
|
|
name: 'will-fail'
|
|
});
|
|
|
|
await assert.rejects(completion, /job error/);
|
|
|
|
should(spyHandler.called).be.true();
|
|
should(spyHandler.args[0][0].message).equal('job error');
|
|
should(spyHandler.args[0][1].name).equal('will-fail');
|
|
});
|
|
|
|
it('uses worker message handler when job sends a message', async function (){
|
|
const workerMessageHandlerSpy = sinon.spy();
|
|
const jobManager = new JobManager({workerMessageHandler: workerMessageHandlerSpy});
|
|
const completion = jobManager.awaitCompletion('will-send-msg');
|
|
|
|
jobManager.addJob({
|
|
job: path.resolve(__dirname, './jobs/message.js'),
|
|
name: 'will-send-msg'
|
|
});
|
|
jobManager.bree.run('will-send-msg');
|
|
await delay(100);
|
|
jobManager.bree.workers['will-send-msg'].postMessage('hello from Ghost!');
|
|
|
|
await completion;
|
|
|
|
should(workerMessageHandlerSpy.called).be.true();
|
|
should(workerMessageHandlerSpy.args[0][0].name).equal('will-send-msg');
|
|
should(workerMessageHandlerSpy.args[0][0].message).equal('Worker received: hello from Ghost!');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Add one off job', function () {
|
|
it('throws if name parameter is not provided', async function () {
|
|
const jobManager = new JobManager({});
|
|
|
|
try {
|
|
await jobManager.addOneOffJob({
|
|
job: () => {}
|
|
});
|
|
throw new Error('should have thrown');
|
|
} catch (err) {
|
|
should.equal(err.message, 'The name parameter is required for a one off job.');
|
|
}
|
|
});
|
|
|
|
describe('Inline jobs', function () {
|
|
it('adds job to the queue when it is a unique one', async function () {
|
|
const spy = sinon.spy();
|
|
const JobModel = {
|
|
findOne: sinon.stub().resolves(undefined),
|
|
add: sinon.stub().resolves()
|
|
};
|
|
|
|
const jobManager = new JobManager({JobModel});
|
|
await jobManager.addOneOffJob({
|
|
job: spy,
|
|
name: 'unique name',
|
|
data: 'test data',
|
|
offloaded: false
|
|
});
|
|
|
|
assert.equal(JobModel.add.called, true);
|
|
});
|
|
|
|
it('does not add a job to the queue when it already exists', async function () {
|
|
const spy = sinon.spy();
|
|
const JobModel = {
|
|
findOne: sinon.stub().resolves(jobModelInstance),
|
|
add: sinon.stub().throws('should not be called')
|
|
};
|
|
|
|
const jobManager = new JobManager({JobModel});
|
|
|
|
try {
|
|
await jobManager.addOneOffJob({
|
|
job: spy,
|
|
name: 'I am the only one',
|
|
data: 'test data',
|
|
offloaded: false
|
|
});
|
|
throw new Error('should not reach this point');
|
|
} catch (error) {
|
|
assert.equal(error.message, 'A "I am the only one" one off job has already been executed.');
|
|
}
|
|
});
|
|
|
|
it('sets a finished state on an inline job', async function () {
|
|
const JobModel = {
|
|
findOne: sinon.stub()
|
|
.onCall(0)
|
|
.resolves(null)
|
|
.resolves({id: 'unique', name: 'successful-oneoff'}),
|
|
add: sinon.stub().resolves({name: 'successful-oneoff'}),
|
|
edit: sinon.stub().resolves({name: 'successful-oneoff'})
|
|
};
|
|
|
|
const jobManager = new JobManager({JobModel});
|
|
const completion = jobManager.awaitCompletion('successful-oneoff');
|
|
|
|
jobManager.addOneOffJob({
|
|
job: async () => {
|
|
return await delay(10);
|
|
},
|
|
name: 'successful-oneoff',
|
|
offloaded: false
|
|
});
|
|
|
|
await completion;
|
|
|
|
// tracks the job queued
|
|
should(JobModel.add.args[0][0].status).equal('queued');
|
|
should(JobModel.add.args[0][0].name).equal('successful-oneoff');
|
|
|
|
// tracks the job started
|
|
should(JobModel.edit.args[0][0].status).equal('started');
|
|
should(JobModel.edit.args[0][0].started_at).not.equal(undefined);
|
|
should(JobModel.edit.args[0][1].id).equal('unique');
|
|
|
|
// tracks the job finish
|
|
should(JobModel.edit.args[1][0].status).equal('finished');
|
|
should(JobModel.edit.args[1][0].finished_at).not.equal(undefined);
|
|
should(JobModel.edit.args[1][1].id).equal('unique');
|
|
});
|
|
|
|
it('sets a failed state on a job', async function () {
|
|
const JobModel = {
|
|
findOne: sinon.stub()
|
|
.onCall(0)
|
|
.resolves(null)
|
|
.resolves({id: 'unique', name: 'failed-oneoff'}),
|
|
add: sinon.stub().resolves({name: 'failed-oneoff'}),
|
|
edit: sinon.stub().resolves({name: 'failed-oneoff'})
|
|
};
|
|
|
|
let job = function namedJob() {
|
|
throw new Error('job error');
|
|
};
|
|
const spyHandler = sinon.spy();
|
|
const jobManager = new JobManager({errorHandler: spyHandler, JobModel});
|
|
const completion = jobManager.awaitCompletion('failed-oneoff');
|
|
|
|
await jobManager.addOneOffJob({
|
|
job,
|
|
name: 'failed-oneoff',
|
|
offloaded: false
|
|
});
|
|
|
|
await assert.rejects(completion, /job error/);
|
|
|
|
// tracks the job start
|
|
should(JobModel.edit.args[0][0].status).equal('started');
|
|
should(JobModel.edit.args[0][0].started_at).not.equal(undefined);
|
|
should(JobModel.edit.args[0][1].id).equal('unique');
|
|
|
|
// tracks the job failure
|
|
should(JobModel.edit.args[1][0].status).equal('failed');
|
|
should(JobModel.edit.args[1][1].id).equal('unique');
|
|
});
|
|
|
|
it('adds job to the queue after failing', async function () {
|
|
const JobModel = {
|
|
findOne: sinon.stub()
|
|
.onCall(0)
|
|
.resolves(null)
|
|
.onCall(1)
|
|
.resolves({id: 'unique'})
|
|
.resolves({
|
|
id: 'unique',
|
|
get: (field) => {
|
|
if (field === 'status') {
|
|
return 'failed';
|
|
}
|
|
}
|
|
}),
|
|
add: sinon.stub().resolves({}),
|
|
edit: sinon.stub().resolves()
|
|
};
|
|
|
|
let job = function namedJob() {
|
|
throw new Error('job error');
|
|
};
|
|
const spyHandler = sinon.spy();
|
|
const jobManager = new JobManager({errorHandler: spyHandler, JobModel});
|
|
const completion1 = jobManager.awaitCompletion('failed-oneoff');
|
|
|
|
await jobManager.addOneOffJob({
|
|
job,
|
|
name: 'failed-oneoff',
|
|
offloaded: false
|
|
});
|
|
|
|
// give time to execute the job and fail
|
|
await assert.rejects(completion1, /job error/);
|
|
should(JobModel.edit.args[1][0].status).equal('failed');
|
|
|
|
// simulate process restart and "fresh" slate to add the job
|
|
jobManager.removeJob('failed-oneoff');
|
|
const completion2 = jobManager.awaitCompletion('failed-oneoff');
|
|
|
|
await jobManager.addOneOffJob({
|
|
job,
|
|
name: 'failed-oneoff',
|
|
offloaded: false
|
|
});
|
|
|
|
// give time to execute the job and fail AGAIN
|
|
await assert.rejects(completion2, /job error/);
|
|
should(JobModel.edit.args[3][0].status).equal('started');
|
|
should(JobModel.edit.args[4][0].status).equal('failed');
|
|
});
|
|
});
|
|
|
|
describe('Offloaded jobs', function () {
|
|
it('adds job to the queue when it is a unique one', async function () {
|
|
const spy = sinon.spy();
|
|
const JobModel = {
|
|
findOne: sinon.stub().resolves(undefined),
|
|
add: sinon.stub().resolves()
|
|
};
|
|
|
|
const jobManager = new JobManager({JobModel});
|
|
await jobManager.addOneOffJob({
|
|
job: spy,
|
|
name: 'unique name',
|
|
data: 'test data'
|
|
});
|
|
|
|
assert.equal(JobModel.add.called, true);
|
|
});
|
|
|
|
it('does not add a job to the queue when it already exists', async function () {
|
|
const spy = sinon.spy();
|
|
const JobModel = {
|
|
findOne: sinon.stub().resolves(jobModelInstance),
|
|
add: sinon.stub().throws('should not be called')
|
|
};
|
|
|
|
const jobManager = new JobManager({JobModel});
|
|
|
|
try {
|
|
await jobManager.addOneOffJob({
|
|
job: spy,
|
|
name: 'I am the only one',
|
|
data: 'test data'
|
|
});
|
|
throw new Error('should not reach this point');
|
|
} catch (error) {
|
|
assert.equal(error.message, 'A "I am the only one" one off job has already been executed.');
|
|
}
|
|
});
|
|
|
|
it('sets a finished state on a job', async function () {
|
|
const JobModel = {
|
|
findOne: sinon.stub()
|
|
.onCall(0)
|
|
.resolves(null)
|
|
.resolves({id: 'unique', name: 'successful-oneoff'}),
|
|
add: sinon.stub().resolves({name: 'successful-oneoff'}),
|
|
edit: sinon.stub().resolves({name: 'successful-oneoff'})
|
|
};
|
|
|
|
const jobManager = new JobManager({JobModel});
|
|
|
|
const jobCompletion = jobManager.awaitCompletion('successful-oneoff');
|
|
|
|
await jobManager.addOneOffJob({
|
|
job: path.resolve(__dirname, './jobs/message.js'),
|
|
name: 'successful-oneoff'
|
|
});
|
|
|
|
// allow job to get picked up and executed
|
|
await delay(100);
|
|
|
|
jobManager.bree.workers['successful-oneoff'].postMessage('be done!');
|
|
|
|
// allow the message to be passed around
|
|
await jobCompletion;
|
|
|
|
// tracks the job start
|
|
should(JobModel.edit.args[0][0].status).equal('started');
|
|
should(JobModel.edit.args[0][0].started_at).not.equal(undefined);
|
|
should(JobModel.edit.args[0][1].id).equal('unique');
|
|
|
|
// tracks the job finish
|
|
should(JobModel.edit.args[1][0].status).equal('finished');
|
|
should(JobModel.edit.args[1][0].finished_at).not.equal(undefined);
|
|
should(JobModel.edit.args[1][1].id).equal('unique');
|
|
});
|
|
|
|
it('handles a failed job', async function () {
|
|
const JobModel = {
|
|
findOne: sinon.stub()
|
|
.onCall(0)
|
|
.resolves(null)
|
|
.resolves(jobModelInstance),
|
|
add: sinon.stub().resolves({name: 'failed-oneoff'}),
|
|
edit: sinon.stub().resolves({name: 'failed-oneoff'})
|
|
};
|
|
|
|
let job = function namedJob() {
|
|
throw new Error('job error');
|
|
};
|
|
const spyHandler = sinon.spy();
|
|
const jobManager = new JobManager({errorHandler: spyHandler, JobModel});
|
|
|
|
const completion = jobManager.awaitCompletion('failed-oneoff');
|
|
|
|
await jobManager.addOneOffJob({
|
|
job,
|
|
name: 'failed-oneoff'
|
|
});
|
|
|
|
await assert.rejects(completion, /job error/);
|
|
|
|
// still calls the original error handler
|
|
should(spyHandler.called).be.true();
|
|
should(spyHandler.args[0][0].message).equal('job error');
|
|
should(spyHandler.args[0][1].name).equal('failed-oneoff');
|
|
|
|
// tracks the job start
|
|
should(JobModel.edit.args[0][0].status).equal('started');
|
|
should(JobModel.edit.args[0][0].started_at).not.equal(undefined);
|
|
should(JobModel.edit.args[0][1].id).equal('unique');
|
|
|
|
// tracks the job failure
|
|
should(JobModel.edit.args[1][0].status).equal('failed');
|
|
should(JobModel.edit.args[1][1].id).equal('unique');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Job execution progress', function () {
|
|
it('checks if job has ever been executed', async function () {
|
|
const JobModel = {
|
|
findOne: sinon.stub()
|
|
.withArgs('solovei')
|
|
.onCall(0)
|
|
.resolves(null)
|
|
.onCall(1)
|
|
.resolves({
|
|
id: 'unique',
|
|
get: (field) => {
|
|
if (field === 'status') {
|
|
return 'finished';
|
|
}
|
|
}
|
|
})
|
|
.onCall(2)
|
|
.resolves({
|
|
id: 'unique',
|
|
get: (field) => {
|
|
if (field === 'status') {
|
|
return 'failed';
|
|
}
|
|
}
|
|
})
|
|
};
|
|
|
|
const jobManager = new JobManager({JobModel});
|
|
let executed = await jobManager.hasExecutedSuccessfully('solovei');
|
|
should.equal(executed, false);
|
|
|
|
executed = await jobManager.hasExecutedSuccessfully('solovei');
|
|
should.equal(executed, true);
|
|
|
|
executed = await jobManager.hasExecutedSuccessfully('solovei');
|
|
should.equal(executed, false);
|
|
});
|
|
|
|
it('can wait for job completion', async function () {
|
|
const spy = sinon.spy();
|
|
let status = 'queued';
|
|
const jobWithDelay = async () => {
|
|
await delay(80);
|
|
status = 'finished';
|
|
spy();
|
|
};
|
|
const JobModel = {
|
|
findOne: sinon.stub()
|
|
// first call when adding a job
|
|
.withArgs('solovei')
|
|
.onCall(0)
|
|
// first call when adding a job
|
|
.resolves(null)
|
|
.onCall(1)
|
|
.resolves(null)
|
|
.resolves({
|
|
id: 'unique',
|
|
get: () => status
|
|
}),
|
|
add: sinon.stub().resolves()
|
|
};
|
|
|
|
const jobManager = new JobManager({JobModel});
|
|
|
|
await jobManager.addOneOffJob({
|
|
job: jobWithDelay,
|
|
name: 'solovei',
|
|
offloaded: false
|
|
});
|
|
|
|
should.equal(spy.called, false);
|
|
await jobManager.awaitOneOffCompletion('solovei');
|
|
should.equal(spy.called, true);
|
|
});
|
|
});
|
|
|
|
describe('Remove a job', function () {
|
|
it('removes a scheduled job from the queue', async function () {
|
|
const jobManager = new JobManager({});
|
|
|
|
const timeInTenSeconds = new Date(Date.now() + 10);
|
|
const jobPath = path.resolve(__dirname, './jobs/simple.js');
|
|
|
|
jobManager.addJob({
|
|
at: timeInTenSeconds,
|
|
job: jobPath,
|
|
name: 'job-in-ten'
|
|
});
|
|
jobManager.bree.config.jobs[0].name.should.equal('job-in-ten');
|
|
|
|
await jobManager.removeJob('job-in-ten');
|
|
|
|
should(jobManager.bree.config.jobs[0]).be.undefined;
|
|
});
|
|
});
|
|
|
|
describe('Shutdown', function () {
|
|
it('gracefully shuts down an inline jobs', async function () {
|
|
const jobManager = new JobManager({});
|
|
|
|
jobManager.addJob({
|
|
job: require('./jobs/timed-job'),
|
|
data: 200,
|
|
offloaded: false
|
|
});
|
|
|
|
should(jobManager.queue.idle()).be.false();
|
|
|
|
await jobManager.shutdown();
|
|
|
|
should(jobManager.queue.idle()).be.true();
|
|
});
|
|
|
|
it('gracefully shuts down an interval job', async function () {
|
|
const jobManager = new JobManager({});
|
|
|
|
jobManager.addJob({
|
|
at: 'every 5 seconds',
|
|
job: path.resolve(__dirname, './jobs/graceful.js')
|
|
});
|
|
|
|
await delay(1); // let the job execution kick in
|
|
|
|
should(Object.keys(jobManager.bree.workers).length).equal(0);
|
|
should(Object.keys(jobManager.bree.timeouts).length).equal(0);
|
|
should(Object.keys(jobManager.bree.intervals).length).equal(1);
|
|
|
|
await jobManager.shutdown();
|
|
|
|
should(Object.keys(jobManager.bree.intervals).length).equal(0);
|
|
});
|
|
});
|
|
});
|