-
Notifications
You must be signed in to change notification settings - Fork 222
[Obsolete draft] Fix dropped jobs issue #197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,28 +1,24 @@ | ||
| --[[ | ||
| key 1 -> bq:name:id (job ID counter) | ||
| key 2 -> bq:name:jobs | ||
| key 3 -> bq:name:delayed | ||
| key 4 -> bq:name:earlierDelayed | ||
| key 1 -> bq:name:jobs | ||
| key 2 -> bq:name:delayed | ||
| key 3 -> bq:name:earlierDelayed | ||
| arg 1 -> job id | ||
| arg 2 -> job data | ||
| arg 3 -> job delay timestamp | ||
| ]] | ||
|
|
||
| local jobId = ARGV[1] | ||
| if jobId == "" then | ||
| jobId = "" .. redis.call("incr", KEYS[1]) | ||
| end | ||
| if redis.call("hexists", KEYS[2], jobId) == 1 then return nil end | ||
| redis.call("hset", KEYS[2], jobId, ARGV[2]) | ||
| redis.call("zadd", KEYS[3], tonumber(ARGV[3]), jobId) | ||
| if redis.call("hexists", KEYS[1], jobId) == 1 then return nil end | ||
| redis.call("hset", KEYS[1], jobId, ARGV[2]) | ||
| redis.call("zadd", KEYS[2], tonumber(ARGV[3]), jobId) | ||
|
|
||
| -- if this job is the new head, alert the workers that they need to update their timers | ||
| -- if we try to do something tricky like checking the delta between this job and the next job, we | ||
| -- can enter a pathological case where jobs incrementally creep sooner, and each one never updates | ||
| -- the timers | ||
| local head = redis.call("zrange", KEYS[3], 0, 0) | ||
| local head = redis.call("zrange", KEYS[2], 0, 0) | ||
| if head[1] == jobId then | ||
| redis.call("publish", KEYS[4], ARGV[3]) | ||
| redis.call("publish", KEYS[3], ARGV[3]) | ||
| end | ||
|
|
||
| return jobId |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,11 @@ | ||
| --[[ | ||
| key 1 -> bq:name:id (job ID counter) | ||
| key 2 -> bq:name:jobs | ||
| key 3 -> bq:name:waiting | ||
| key 1 -> bq:name:jobs | ||
| key 2 -> bq:name:waiting | ||
| arg 1 -> job id | ||
| arg 2 -> job data | ||
| ]] | ||
|
|
||
| local jobId = ARGV[1] | ||
| if jobId == "" then | ||
| jobId = "" .. redis.call("incr", KEYS[1]) | ||
| end | ||
| if redis.call("hexists", KEYS[2], jobId) == 1 then return nil end | ||
| redis.call("hset", KEYS[2], jobId, ARGV[2]) | ||
| redis.call("lpush", KEYS[3], jobId) | ||
|
|
||
| return jobId | ||
| if redis.call("hexists", KEYS[1], jobId) == 1 then return end | ||
| redis.call("hset", KEYS[1], jobId, ARGV[2]) | ||
| redis.call("lpush", KEYS[2], jobId) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -420,11 +420,20 @@ class Queue extends Emitter { | |
| return promise; | ||
| } | ||
|
|
||
| _waitForJob() { | ||
| const idPromise = helpers.deferred(); | ||
| _waitForJob(testingCb) { | ||
| let idPromise = helpers.deferred(); | ||
| this.bclient.brpoplpush(this.toKey('waiting'), this.toKey('active'), 0, | ||
| idPromise.defer()); | ||
|
|
||
| // The only reason for this callback is to support deterministic testing of | ||
| // removeJob between the time brpoplpush resolves and Job.fromId() runs | ||
| if (testingCb) { | ||
| idPromise = idPromise.then(async (jobId) => { | ||
| await testingCb(jobId); | ||
| return jobId; | ||
| }); | ||
| } | ||
|
|
||
| return idPromise.then((jobId) => { | ||
| // Note that the job may be null in the case that the client has removed | ||
| // the job before processing can take place, but after the brpoplpush has | ||
|
|
@@ -725,6 +734,7 @@ class Queue extends Emitter { | |
| .then((results) => { | ||
| const numRaised = results[0], nextOpportunity = results[1]; | ||
| if (numRaised) { | ||
| // not documented | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. heh yeah This seems unrelated to the overarching PR, and maybe we could document it instead of leaving it more explicitly undocumented. |
||
| this.emit('raised jobs', numRaised); | ||
| } | ||
| this._delayedTimer.schedule(parseInt(nextOpportunity, 10)); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -342,7 +342,7 @@ describe('Queue', (it) => { | |||||
| const jobs = spitter(); | ||||||
| queue.process((job) => jobs.pushSuspend(job)); | ||||||
|
|
||||||
| await queue.createJob({}).save(); | ||||||
| const job = await queue.createJob({}).save(); | ||||||
| const [, finishJob] = await jobs.shift(); | ||||||
|
|
||||||
| await t.throws(queue.close(10)); | ||||||
|
|
@@ -352,7 +352,7 @@ describe('Queue', (it) => { | |||||
|
|
||||||
| const errors = t.context.queueErrors, count = errors.length; | ||||||
| t.context.queueErrors = errors.filter((err) => { | ||||||
| return err.message !== 'unable to update the status of succeeded job 1'; | ||||||
| return err.message !== `unable to update the status of succeeded job ${job.id}`; | ||||||
| }); | ||||||
| t.is(t.context.queueErrors.length, count - 1); | ||||||
| t.context.handleErrors(t); | ||||||
|
|
@@ -460,12 +460,12 @@ describe('Queue', (it) => { | |||||
| // Override _waitForJob. | ||||||
| const waitJob = queue._waitForJob, wait = helpers.deferred(); | ||||||
| let waitDone = wait.defer(); | ||||||
| queue._waitForJob = function (...args) { | ||||||
| queue._waitForJob = function () { | ||||||
| if (waitDone) { | ||||||
| waitDone(); | ||||||
| waitDone = null; | ||||||
| } | ||||||
| return waitJob.apply(this, args); | ||||||
| return waitJob.apply(this); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right. |
||||||
| }; | ||||||
|
|
||||||
| await wait; | ||||||
|
|
@@ -878,33 +878,31 @@ describe('Queue', (it) => { | |||||
| }); | ||||||
|
|
||||||
| it.describe('removeJob', (it) => { | ||||||
| it('should not cause an error if immediately removed', async (t) => { | ||||||
| it('should not error if active then removed before running', async (t) => { | ||||||
| const queue = t.context.makeQueue(); | ||||||
|
|
||||||
| queue.process(async (job) => { | ||||||
| if (job.id === 'deadjob') { | ||||||
| t.fail('should not be able to process the job'); | ||||||
| } | ||||||
| }); | ||||||
|
|
||||||
| // Inject removeJob() inside _waitForJob the first time it's called | ||||||
| const waitJob = queue._waitForJob, wait = helpers.deferred(); | ||||||
| let waitDone = wait.defer(); | ||||||
| queue._waitForJob = function (...args) { | ||||||
| queue._waitForJob = function () { | ||||||
| const args = []; | ||||||
| if (waitDone) { | ||||||
| args.push((jobId) => this.removeJob(jobId)); | ||||||
| waitDone(); | ||||||
| waitDone = null; | ||||||
| } | ||||||
| return waitJob.apply(this, args); | ||||||
| }; | ||||||
|
|
||||||
| await wait; | ||||||
| queue.process(async (job) => { | ||||||
| if (job.id === 'deadjob') { | ||||||
| t.fail('should not be able to process the job'); | ||||||
| } | ||||||
| }); | ||||||
|
|
||||||
| const job = queue.createJob({foo: 'bar'}).setId('deadjob'); | ||||||
| await Promise.all([ | ||||||
| job.save(), | ||||||
| queue.removeJob(job.id), | ||||||
| queue.createJob({foo: 'bar'}).setId('goodjob').save(), | ||||||
| ]); | ||||||
| await wait; | ||||||
| await queue.createJob({foo: 'bar'}).setId('deadjob').save(); | ||||||
| await queue.createJob({foo: 'bar'}).setId('goodjob').save(); | ||||||
|
|
||||||
| const goodJob = await helpers.waitOn(queue, 'succeeded'); | ||||||
| t.is(goodJob.id, 'goodjob'); | ||||||
|
|
@@ -1163,7 +1161,7 @@ describe('Queue', (it) => { | |||||
| const [[failedJob, err]] = fail.args; | ||||||
|
|
||||||
| t.truthy(failedJob); | ||||||
| t.is(job.id, '1'); | ||||||
| t.is(job.id, failedJob.id); | ||||||
| t.is(failedJob.data.foo, 'bar'); | ||||||
| t.is(err.message, `Job ${job.id} timed out (10 ms)`); | ||||||
| }); | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of modifying the source to support the test, what do you think about modifying the test to either stub the
Job.fromIdstatic method or modify thebrpoplpushfunction to provide this hook?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I had thought about wrapping
Job.fromId()-- I'll revisit that approach.