diff --git a/tests/Queue.test.js b/tests/Queue.test.js index 73feb7f..4ee72be 100644 --- a/tests/Queue.test.js +++ b/tests/Queue.test.js @@ -1,5 +1,5 @@ // Define globals for eslint. -/* global describe it beforeEach process jest */ +/* global describe it beforeEach afterEach jest */ // Load dependencies import should from 'should'; // eslint-disable-line no-unused-vars @@ -13,10 +13,15 @@ describe('Models/Queue', function() { queue.flushQueue(); }); + afterEach(() => { + jest.useRealTimers(); + }); + // // QUEUE LIFESPAN TESTING // it('#start(lifespan) queue with lifespan does not process jobs that have no timeout set.', async () => { + jest.useFakeTimers(); const queue = await QueueFactory(); const jobName = 'job-name'; @@ -40,12 +45,14 @@ describe('Models/Queue', function() { // wait a tick await new Promise((resolve) => { setTimeout(resolve, 0); + jest.advanceTimersByTime(0); }); queue.status.should.equal('inactive'); }); it('#start(lifespan) FUNDAMENTAL TEST (queue started with lifespan will process a job with job timeout set).', async () => { + jest.useFakeTimers(); const queue = await QueueFactory(); queue.flushQueue(); const jobName = 'job-name'; @@ -56,6 +63,11 @@ describe('Models/Queue', function() { queue.addWorker(jobName, async (id, payload) => { // Track jobs that exec executedJobs.push(payload.trackingName); + + await new Promise((resolve) => { + setTimeout(resolve, 100); + jest.advanceTimersByTime(100); + }); }); // Create a job but don't auto-start queue @@ -75,24 +87,24 @@ describe('Models/Queue', function() { // wait a bit for queue to process job await new Promise((resolve) => { setTimeout(resolve, 750); + jest.advanceTimersByTime(750); }); //Check that the correct jobs executed. executedJobs.should.deepEqual(['job-name']); + // switch to real timers so we can get the updated queue value + jest.useRealTimers(); + await new Promise((resolve) => { + setTimeout(resolve, 0); + }); + // Queue should have stopped. queue.status.should.equal('inactive'); }); it('#start(lifespan) BASIC TEST (One job type, default job/worker options): queue will process jobs with timeout set as expected until lifespan ends.', async () => { - // This test will intermittently fail in CI environments like travis-ci. - // Intermittent failure is a result of the poor performance of CI environments - // causing the timeouts in this test to become really flakey (setTimeout can't - // guarantee exact time of function execution, and in a high load env execution can - // be significantly delayed. - if (process.env.CI === true) { - return true; - } + jest.useFakeTimers(); const queue = await QueueFactory(); queue.flushQueue(); @@ -123,6 +135,7 @@ describe('Models/Queue', function() { await new Promise((resolve) => { setTimeout(resolve, payload.payloadTimeout); + jest.advanceTimersByTime(payload.payloadOptionsTimeout); }); }, { concurrency: 1}); @@ -141,7 +154,7 @@ describe('Models/Queue', function() { // in order to control the order jobs come off the queue (since they are time sorted) // If multiple jobs are written in the same ms, Realm can't be deterministic about job // ordering when we pop jobs off the top of the queue. - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(jobName, { trackingName: 'job2', @@ -150,7 +163,7 @@ describe('Models/Queue', function() { }, { timeout: 300 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(jobName, { trackingName: 'job3', @@ -159,7 +172,7 @@ describe('Models/Queue', function() { }, { timeout: 1100 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(jobName, { trackingName: 'job4', @@ -168,7 +181,7 @@ describe('Models/Queue', function() { }, { timeout: 600 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(jobName, { trackingName: 'job5', @@ -177,7 +190,7 @@ describe('Models/Queue', function() { }, { timeout: 75 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(jobName, { trackingName: 'job6', @@ -186,7 +199,7 @@ describe('Models/Queue', function() { }, { timeout: 100 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(jobName, { trackingName: 'job7', @@ -195,7 +208,7 @@ describe('Models/Queue', function() { }, { timeout: 1200 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); // startQueue is false so queue should not have started. queue.status.should.equal('inactive'); @@ -236,14 +249,7 @@ describe('Models/Queue', function() { }); it('#start(lifespan) ADVANCED TEST FULL (Multiple job names, job timeouts, concurrency, priority) - ONLY RUN IN NON-CI ENV: queue will process jobs with timeout set as expected until lifespan ends.', async () => { - // This test will intermittently fail in CI environments like travis-ci. - // Intermittent failure is a result of the poor performance of CI environments - // causing the timeouts in this test to become really flakey (setTimeout can't - // guarantee exact time of function execution, and in a high load env execution can - // be significantly delayed. - if (process.env.CI === true) { - return true; - } + jest.useFakeTimers(); const queue = await QueueFactory(); queue.flushQueue(); @@ -277,6 +283,7 @@ describe('Models/Queue', function() { await new Promise((resolve) => { setTimeout(resolve, payload.payloadTimeout); + jest.advanceTimersByTime(payload.payloadTimeout); }); }, { concurrency: 1}); @@ -294,6 +301,7 @@ describe('Models/Queue', function() { await new Promise((resolve) => { setTimeout(resolve, payload.payloadTimeout); + jest.advanceTimersByTime(payload.payloadTimeout); }); }, { concurrency: 1}); @@ -310,6 +318,13 @@ describe('Models/Queue', function() { await new Promise((resolve) => { setTimeout(resolve, payload.payloadTimeout); + + // use the payloadOptionsTimeout if timeout exceeds the max queue lifespan + if (payload.trackingName === 'job6-timeout-job-name-payloadTimeout(10000)-timeout(500)-priority(0)') { + jest.advanceTimersByTime(payload.payloadOptionsTimeout); + } else { + jest.advanceTimersByTime(payload.payloadTimeout); + } }); }, { concurrency: 1}); @@ -330,6 +345,13 @@ describe('Models/Queue', function() { await new Promise((resolve) => { setTimeout(resolve, payload.payloadTimeout); + // Since these all run concurrently, only subtract the job with the longest + // timeout that will presumably finish last. + if (payload.trackingName === 'job11-concurrent-job-name-payloadTimeout(600)-timeout(700)-priority(0)') { + jest.advanceTimersByTime(600); + } else { + jest.advanceTimersByTime(0); + } }); }, { concurrency: 4}); @@ -348,7 +370,7 @@ describe('Models/Queue', function() { // in order to control the order jobs come off the queue (since they are time sorted) // If multiple jobs are written in the same ms, Realm can't be deterministic about job // ordering when we pop jobs off the top of the queue. - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(anotherJobName, { trackingName: 'job2-another-job-name-payloadTimeout(1000)-timeout(1100)-priority(0)', @@ -357,7 +379,7 @@ describe('Models/Queue', function() { }, { timeout: 1100 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(anotherJobName, { trackingName: 'job3-another-job-name-payloadTimeout(750)-timeout(800)-priority(10)', @@ -367,7 +389,7 @@ describe('Models/Queue', function() { timeout: 800, priority: 10 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(jobName, { trackingName: 'job4-job-name-payloadTimeout(10000)-timeout(10100)-priority(0)', @@ -376,7 +398,7 @@ describe('Models/Queue', function() { }, { timeout: 10100 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(jobName, { trackingName: 'job5-job-name-payloadTimeout(400)-timeout(500)-priority(0)', @@ -385,7 +407,7 @@ describe('Models/Queue', function() { }, { timeout: 500 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(timeoutJobName, { trackingName: 'job6-timeout-job-name-payloadTimeout(10000)-timeout(500)-priority(0)', @@ -394,7 +416,7 @@ describe('Models/Queue', function() { }, { timeout: 500 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(jobName, { trackingName: 'job7-job-name-payloadTimeout(1000)-timeout(1100)-priority(1)', @@ -404,7 +426,7 @@ describe('Models/Queue', function() { timeout: 1100, priority: 1 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); // Create concurrent jobs queue.createJob(concurrentJobName, { @@ -415,7 +437,7 @@ describe('Models/Queue', function() { timeout: 600, priority: 0 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(concurrentJobName, { trackingName: 'job9-concurrent-job-name-payloadTimeout(510)-timeout(600)-priority(0)', @@ -425,7 +447,7 @@ describe('Models/Queue', function() { timeout: 600, priority: 0 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(concurrentJobName, { trackingName: 'job10-concurrent-job-name-payloadTimeout(10000)-timeout(10100)-priority(0)', // THIS JOB WILL BE SKIPPED BY getConcurrentJobs() due to timeout too long. @@ -435,7 +457,7 @@ describe('Models/Queue', function() { timeout: 10100, priority: 0 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(concurrentJobName, { trackingName: 'job11-concurrent-job-name-payloadTimeout(600)-timeout(700)-priority(0)', @@ -445,7 +467,7 @@ describe('Models/Queue', function() { timeout: 700, priority: 0 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(jobName, { trackingName: 'job12-job-name-payloadTimeout(100)-timeout(200)-priority(0)', @@ -454,7 +476,7 @@ describe('Models/Queue', function() { }, { timeout: 200 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(jobName, { trackingName: 'job13-job-name-payloadTimeout(400)-timeout(500)-priority(0)', // THIS JOB WON'T BE RUN BECAUSE THE TIMEOUT IS 500 AND ONLY 950ms left by this pount. 950 - 500 = 450 and 500 remaining is min for job to be pulled. @@ -463,7 +485,7 @@ describe('Models/Queue', function() { }, { timeout: 500 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(jobName, { trackingName: 'job14-job-name-payloadTimeout(100)-timeout(200)-priority(0)', @@ -472,7 +494,7 @@ describe('Models/Queue', function() { }, { timeout: 200 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); + await new Promise((resolve) => { setTimeout(resolve, 25); jest.advanceTimersByTime(25); }); queue.createJob(jobName, { trackingName: 'job15-job-name-payloadTimeout(500)-timeout(600)-priority(0)', // THIS JOB WON'T BE RUN BECAUSE out of time! @@ -481,7 +503,6 @@ describe('Models/Queue', function() { }, { timeout: 600 }, false); - await new Promise((resolve) => { setTimeout(resolve, 25); }); // startQueue is false so queue should not have started. queue.status.should.equal('inactive'); @@ -537,7 +558,7 @@ describe('Models/Queue', function() { 'job15-job-name-payloadTimeout(500)-timeout(600)-priority(0)' ]); - }, 10000); // Increase timeout of this advanced test to 10 seconds. + }); it('#start(lifespan) "Zero lifespanRemaining" edge case #1 is properly handled.', async () => { // Mock Date.now() @@ -788,6 +809,7 @@ describe('Models/Queue', function() { }); it('#start() should start queue.', async () => { + jest.useFakeTimers(); const queue = await QueueFactory(); const jobName = 'job-name'; const payload = { data: 'example-data' }; @@ -813,6 +835,14 @@ describe('Models/Queue', function() { setTimeout(() => { resolve(true); }, 1000); + + jest.advanceTimersByTime(1000); + }); + + // switch to real timers so we can get the updated queue value + jest.useRealTimers(); + await new Promise((resolve) => { + setTimeout(resolve, 0); }); // Queue should be finished with no jobs left. @@ -827,6 +857,7 @@ describe('Models/Queue', function() { }); it('#start() called when queue is already active should NOT fire up a concurrent queue.', async () => { + jest.useFakeTimers(); const queue = await QueueFactory(); const jobName = 'job-name'; const payload = { data: 'example-data' }; @@ -837,6 +868,8 @@ describe('Models/Queue', function() { // Make queue take some time to process. await new Promise( resolve => { setTimeout(resolve, 1000); + + jest.advanceTimersByTime(1000); }); }); @@ -1303,6 +1336,7 @@ describe('Models/Queue', function() { }); it('#processJob() handles a job timeout as expected', async () => { + jest.useFakeTimers(); const queue = await QueueFactory(); const jobName = 'job-name'; const jobOptions = { priority: 0, timeout: 500, attempts: 1}; @@ -1312,6 +1346,8 @@ describe('Models/Queue', function() { setTimeout(() => { resolve(true); }, 2000); + + jest.advanceTimersByTime(2000); }); }); @@ -1410,14 +1446,7 @@ describe('Models/Queue', function() { //// it('onStart lifecycle callback fires before job begins processing.', async () => { - // This test will intermittently fail in CI environments like travis-ci. - // Intermittent failure is a result of the poor performance of CI environments - // causing the timeouts in this test to become really flakey (setTimeout can't - // guarantee exact time of function execution, and in a high load env execution can - // be significantly delayed. - if (process.env.CI === true) { - return true; - } + jest.useFakeTimers(); const queue = await QueueFactory(); queue.flushQueue(); @@ -1433,6 +1462,8 @@ describe('Models/Queue', function() { jobProcessed = true; resolve(); }, 0); + + jest.advanceTimersByTime(0); }); }, { onStart: () => { @@ -1455,14 +1486,7 @@ describe('Models/Queue', function() { }); it('onSuccess, onComplete lifecycle callbacks fire after job begins processing.', async () => { - // This test will intermittently fail in CI environments like travis-ci. - // Intermittent failure is a result of the poor performance of CI environments - // causing the timeouts in this test to become really flakey (setTimeout can't - // guarantee exact time of function execution, and in a high load env execution can - // be significantly delayed. - if (process.env.CI === true) { - return true; - } + jest.useFakeTimers(); const queue = await QueueFactory(); queue.flushQueue(); @@ -1480,6 +1504,8 @@ describe('Models/Queue', function() { jobProcessed = true; resolve(); }, 300); + + jest.advanceTimersByTime(300); }); }, { @@ -1518,14 +1544,7 @@ describe('Models/Queue', function() { }); it('onFailure, onFailed lifecycle callbacks fire after job begins processing.', async () => { - // This test will intermittently fail in CI environments like travis-ci. - // Intermittent failure is a result of the poor performance of CI environments - // causing the timeouts in this test to become really flakey (setTimeout can't - // guarantee exact time of function execution, and in a high load env execution can - // be significantly delayed. - if (process.env.CI === true) { - return true; - } + jest.useFakeTimers(); const queue = await QueueFactory(); queue.flushQueue(); @@ -1540,6 +1559,8 @@ describe('Models/Queue', function() { jobProcessStarted = true; reject(new Error('Job failed.')); }, 300); + + jest.advanceTimersByTime(300); }); }, { @@ -1570,6 +1591,7 @@ describe('Models/Queue', function() { }); it('onFailure, onFailed lifecycle callbacks work as expected.', async () => { + jest.useFakeTimers(); const queue = await QueueFactory(); queue.flushQueue(); const jobName = 'job-name'; @@ -1584,6 +1606,8 @@ describe('Models/Queue', function() { jobAttemptCounter++; reject(new Error('Job failed.')); }, 0); + + jest.advanceTimersByTime(0); }); }, { onFailure: () => { @@ -1609,6 +1633,7 @@ describe('Models/Queue', function() { }); it('onComplete fires only once on job with multiple attempts that ends in success.', async () => { + jest.useFakeTimers(); const queue = await QueueFactory(); queue.flushQueue(); const jobName = 'job-name'; @@ -1628,6 +1653,8 @@ describe('Models/Queue', function() { setTimeout(() => { reject(new Error('Job failed.')); }, 0); + + jest.advanceTimersByTime(0); }); } else { // Simulate work that succeeds @@ -1635,6 +1662,8 @@ describe('Models/Queue', function() { setTimeout(() => { resolve(); }, 0); + + jest.advanceTimersByTime(0); }); } }, { @@ -1663,6 +1692,7 @@ describe('Models/Queue', function() { }); it('onComplete fires only once on job with multiple attempts that ends in failure.', async () => { + jest.useFakeTimers(); const queue = await QueueFactory(); queue.flushQueue(); const jobName = 'job-name'; @@ -1680,6 +1710,8 @@ describe('Models/Queue', function() { setTimeout(() => { reject(new Error('Job failed.')); }, 0); + + jest.advanceTimersByTime(0); }); }, { onFailure: () => { @@ -1707,6 +1739,7 @@ describe('Models/Queue', function() { }); it('onStart, onSuccess, onComplete Job lifecycle callbacks do not block job processing.', async () => { + jest.useFakeTimers(); const queue = await QueueFactory(); queue.flushQueue(); const jobName = 'job-name'; @@ -1720,6 +1753,8 @@ describe('Models/Queue', function() { workTracker.push(payload.random); tracker.push('job processed'); setTimeout(resolve, 0); + + jest.advanceTimersByTime(0); }); }, { @@ -1784,6 +1819,7 @@ describe('Models/Queue', function() { }); it('onFailure, onFailed Job lifecycle callbacks do not block job processing.', async () => { + jest.useFakeTimers(); const queue = await QueueFactory(); queue.flushQueue(); const jobName = 'job-name'; @@ -1798,6 +1834,8 @@ describe('Models/Queue', function() { tracker.push('job attempted'); reject(new Error('job failed')); }, 0); + + jest.advanceTimersByTime(0); }); }, { onFailure: async () => {