Skip to content

Commit accc303

Browse files
committed
add flag for batch enqueue delayed
1 parent b88d6a3 commit accc303

5 files changed

Lines changed: 147 additions & 60 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ nbproject
1616
.env
1717
.env.*
1818
/nul
19-
vendor/
19+
vendor/
20+
.vscode

README.md

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -129,24 +129,18 @@ Both the Rake task and standalone executable support the following
129129
environment variables:
130130

131131
* `APP_NAME` - Application name used in procline (`$0`) (default empty)
132-
* `BACKGROUND` - [Run in the background](#running-in-the-background) if
133-
non-empty (via `Process.daemon`, if supported) (default `false`)
134-
* `DYNAMIC_SCHEDULE` - Enables [dynamic scheduling](#dynamic-schedules)
135-
if non-empty (default `false`)
136-
* `RAILS_ENV` - Environment to use in procline (`$0`) (default empty)
137-
* `INITIALIZER_PATH` - Path to a Ruby file that will be loaded *before*
138-
requiring `resque` and `resque/scheduler` (default empty).
139-
* `RESQUE_SCHEDULER_INTERVAL` - Interval in seconds for checking if a
140-
scheduled job must run (coerced with `Kernel#Float()`) (default `5`)
132+
* `BACKGROUND` - [Run in the background](#running-in-the-background) if non-empty (via `Process.daemon`, if supported) (default `false`)
133+
* `DELAYED_REQUEUE_BATCH_SIZE` - Set the delayed job batch size if enabled (default `100`). If `<= 1`, this disables batching.
134+
* `DISABLE_DELAYED_REQUEUE_BATCH` - Disable batched delayed job queuing (default `false`) - [See section below on consequences](#batched-delayed-job-and-resque-enqueue-hooks)
135+
* `DYNAMIC_SCHEDULE` - Enables [dynamic scheduling](#dynamic-schedules) if non-empty (default `false`)
136+
* `INITIALIZER_PATH` - Path to a Ruby file that will be loaded *before* requiring `resque` and `resque/scheduler` (default empty).
141137
* `LOGFILE` - Log file name (default empty, meaning `$stdout`)
142-
* `LOGFORMAT` - Log output format to use (either `'text'`, `'json'` or `'logfmt'`,
143-
default `'text'`)
138+
* `LOGFORMAT` - Log output format to use (either `'text'`, `'json'` or `'logfmt'`, default `'text'`)
144139
* `PIDFILE` - If non-empty, write process PID to file (default empty)
145-
* `QUIET` - Silence most output if non-empty (equivalent to a level of
146-
`MonoLogger::FATAL`, default `false`)
147-
* `VERBOSE` - Maximize log verbosity if non-empty (equivalent to a level
148-
of `MonoLogger::DEBUG`, default `false`)
149-
140+
* `QUIET` - Silence most output if non-empty (equivalent to a level of `MonoLogger::FATAL`, default `false`)
141+
* `RAILS_ENV` - Environment to use in procline (`$0`) (default empty)
142+
* `RESQUE_SCHEDULER_INTERVAL` - Interval in seconds for checking if a scheduled job must run (coerced with `Kernel#Float()`) (default `5`)
143+
* `VERBOSE` - Maximize log verbosity if non-empty (equivalent to a level of `MonoLogger::DEBUG`, default `false`)
150144

151145
### Resque Pool integration
152146

@@ -755,6 +749,24 @@ This table explains the version requirements for rufus-scheduler
755749
| `~> 4.0` | `~> 3.0` |
756750
| `< 4.0` | `~> 2.0` |
757751

752+
##### Batched delayed job and resque enqueue hooks
753+
754+
Batching delayed job queuing can speed up when per-second job counts grows,
755+
avoiding situations that may cause delayed enqueues to fall behind. This
756+
batching wraps enqueues in a `multi` pipeline, making far fewer roundtrips to
757+
the server.
758+
759+
However, in `redis` gem `>= 4.0`, any operations to redis within the `multi`
760+
block must use the multi handle so that the actions are captured. Resque's hooks
761+
do not currently have a way to pass this around, and so compatibility with other
762+
resque plugins or hooks which access redis at enqueue time is impacted with
763+
batch mode. In these cases, you should consider disabling the batching by setting
764+
the `DISABLE_DELAYED_REQUEUE_BATCH` environment variable to `true`.
765+
766+
Detecting when this occurs can be tricky, you must watch for logs
767+
emitted by your `resque-scheduler` process such as `Redis::CommandError: ERR
768+
MULTI calls can not be nested` or `NoMethodError: undefined method nil? for
769+
<Redis::Future`, and delayed jobs you expect would not be enqueued.
758770

759771
### Contributing
760772

lib/resque/scheduler.rb

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -204,25 +204,36 @@ def enqueue_next_item(timestamp)
204204
item
205205
end
206206

207+
def batch_delayed_items?
208+
!disable_delayed_requeue_batches && delayed_requeue_batch_size > 1
209+
end
210+
207211
# Enqueues all delayed jobs for a timestamp
208212
def enqueue_delayed_items_for_timestamp(timestamp)
209213
count = 0
210-
batch_size = delayed_requeue_batch_size
211-
actual_batch_size = nil
214+
batch_size = batch_delayed_items? ? delayed_requeue_batch_size : 1
212215

213-
log "Processing delayed items for timestamp #{timestamp}, in batches of #{batch_size}"
216+
message = "Processing delayed items for timestamp #{timestamp}"
217+
message += ", in batches of #{batch_size}" if batch_delayed_items?
218+
log message
214219

215220
loop do
221+
actual_batch_size = 0
222+
216223
handle_shutdown do
217224
# Continually check that it is still the master
218225
if am_master
219-
actual_batch_size = enqueue_items_in_batch_for_timestamp(timestamp,
220-
batch_size)
226+
if batch_delayed_items?
227+
actual_batch_size = enqueue_items_in_batch_for_timestamp(timestamp, batch_size)
228+
log "queued batch of #{actual_batch_size} jobs" if actual_batch_size != -1
229+
else
230+
item = enqueue_next_item(timestamp)
231+
actual_batch_size = item.nil? ? 0 : 1
232+
end
221233
end
222234
end
223235

224236
count += actual_batch_size
225-
log "queued #{count} jobs" if actual_batch_size != -1
226237

227238
# continue processing until there are no more items in this
228239
# timestamp. If we don't have a full batch, this is the last one.
@@ -231,7 +242,7 @@ def enqueue_delayed_items_for_timestamp(timestamp)
231242
break if actual_batch_size < batch_size
232243
end
233244

234-
log "finished queueing #{count} total jobs for timestamp #{timestamp}" if count != -1
245+
log "finished queueing #{count} total jobs for timestamp #{timestamp}"
235246
end
236247

237248
def timestamp_key(timestamp)

lib/resque/scheduler/configuration.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,20 @@ def app_name
6565
@app_name ||= environment['APP_NAME']
6666
end
6767

68+
attr_writer :delayed_requeue_batch_size
69+
6870
def delayed_requeue_batch_size
6971
@delayed_requeue_batch_size ||= \
7072
ENV['DELAYED_REQUEUE_BATCH_SIZE'].to_i if environment['DELAYED_REQUEUE_BATCH_SIZE']
7173
@delayed_requeue_batch_size ||= 100
7274
end
7375

76+
attr_writer :disable_delayed_requeue_batches
77+
78+
def disable_delayed_requeue_batches
79+
@enable_delayed_requeue_batches ||= to_bool(environment['DISABLE_DELAYED_REQUEUE_BATCH'])
80+
end
81+
7482
# Amount of time in seconds to sleep between polls of the delayed
7583
# queue. Defaults to 5
7684
attr_writer :poll_sleep_amount

test/delayed_queue_test.rb

Lines changed: 91 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -383,60 +383,115 @@
383383
assert_equal(1, Resque.delayed_timestamp_peek(t, 0, 3).length)
384384
end
385385

386-
test 'enqueue_delayed_items_for_timestamp enqueues jobs in 2 batches' do
387-
t = Time.now + 60
386+
context "non-batch delayed item queue" do
387+
batch_disabled = Resque::Scheduler.disable_delayed_requeue_batches
388+
batch_size = Resque::Scheduler.delayed_requeue_batch_size
389+
setup do
390+
Resque::Scheduler.quiet = true
391+
Resque.data_store.redis.flushall
392+
Resque::Scheduler.disable_delayed_requeue_batches = true
393+
Resque::Scheduler.delayed_requeue_batch_size = 1
394+
end
388395

389-
# create 120 jobs
390-
120.times { Resque.enqueue_at(t, SomeIvarJob) }
391-
assert_equal(120, Resque.delayed_timestamp_size(t))
396+
teardown do
397+
Resque::Scheduler.disable_delayed_requeue_batches = batch_disabled
398+
Resque::Scheduler.delayed_requeue_batch_size = batch_size
399+
end
392400

393-
Resque::Scheduler.enqueue_delayed_items_for_timestamp(t)
394-
assert_equal(0, Resque.delayed_timestamp_size(t))
401+
test 'enqueue_delayed_items_for_timestamp enqueues jobs for the timestamp' do
402+
t = Time.now + 60
395403

396-
# assert that the active queue is now 120
397-
assert_equal(120, Resque.size(Resque.queue_from_class(SomeIvarJob)))
398-
end
404+
Resque::Scheduler.expects(:enqueue_items_in_batch_for_timestamp).never
399405

400-
test 'enqueue_delayed_items_for_timestamp enqueues jobs in one batch for the timestamp' do
401-
t = Time.now + 60
406+
# create 90 jobs
407+
90.times { Resque.enqueue_at(t, SomeIvarJob) }
408+
assert_equal(90, Resque.delayed_timestamp_size(t))
402409

403-
# create 90 jobs
404-
90.times { Resque.enqueue_at(t, SomeIvarJob) }
405-
assert_equal(90, Resque.delayed_timestamp_size(t))
410+
Resque::Scheduler.enqueue_delayed_items_for_timestamp(t)
411+
assert_equal(0, Resque.delayed_timestamp_size(t))
406412

407-
Resque::Scheduler.enqueue_delayed_items_for_timestamp(t)
408-
assert_equal(0, Resque.delayed_timestamp_size(t))
413+
# assert that the active queue is now 90
414+
assert_equal(90, Resque.size(Resque.queue_from_class(SomeIvarJob)))
415+
end
409416

410-
# assert that the active queue is now 90
411-
assert_equal(90, Resque.size(Resque.queue_from_class(SomeIvarJob)))
417+
# TODO clean up timestamp tests?
412418
end
413419

414-
# test to make sure the timestamp is cleaned up
420+
context "batch delayed item queue" do
421+
batch_disabled = Resque::Scheduler.disable_delayed_requeue_batches
422+
batch_size = Resque::Scheduler.delayed_requeue_batch_size
423+
setup do
424+
Resque::Scheduler.quiet = true
425+
Resque.data_store.redis.flushall
426+
Resque::Scheduler.disable_delayed_requeue_batches = false
427+
Resque::Scheduler.delayed_requeue_batch_size = 100
428+
end
415429

416-
test 'enqueue_delayed_items_for_timestamp handles a watch failure' do
417-
t = Time.now + 60
430+
teardown do
431+
Resque::Scheduler.disable_delayed_requeue_batches = batch_disabled
432+
Resque::Scheduler.delayed_requeue_batch_size = batch_size
433+
end
418434

419-
# create 100 jobs
420-
100.times { Resque.enqueue_at(t, SomeIvarJob) }
421-
assert_equal(100, Resque.delayed_timestamp_size(t))
435+
test 'enqueue_delayed_items_for_timestamp enqueues jobs in 2 batches' do
436+
t = Time.now + 60
422437

423-
Resque.redis.stubs(:watch).returns(nil)
438+
Resque::Scheduler.expects(:enqueue_next_item).never
424439

425-
Resque.expects(:clean_up_timestamp).never
440+
# create 120 jobs
441+
120.times { Resque.enqueue_at(t, SomeIvarJob) }
442+
assert_equal(120, Resque.delayed_timestamp_size(t))
426443

427-
Resque::Scheduler.enqueue_delayed_items_for_timestamp(t)
428-
end
444+
Resque::Scheduler.enqueue_delayed_items_for_timestamp(t)
445+
assert_equal(0, Resque.delayed_timestamp_size(t))
429446

430-
test 'enqueue_delayed_items_for_timestamp cleans up a timestamp' do
431-
t = Time.now + 60
447+
# assert that the active queue is now 120
448+
assert_equal(120, Resque.size(Resque.queue_from_class(SomeIvarJob)))
449+
end
432450

433-
# create 100 jobs
434-
100.times { Resque.enqueue_at(t, SomeIvarJob) }
435-
assert_equal(100, Resque.delayed_timestamp_size(t))
451+
test 'enqueue_delayed_items_for_timestamp enqueues jobs in one batch for the timestamp' do
452+
t = Time.now + 60
436453

437-
Resque.expects(:clean_up_timestamp).once
454+
Resque::Scheduler.expects(:enqueue_next_item).never
455+
456+
# create 90 jobs
457+
90.times { Resque.enqueue_at(t, SomeIvarJob) }
458+
assert_equal(90, Resque.delayed_timestamp_size(t))
459+
460+
Resque::Scheduler.enqueue_delayed_items_for_timestamp(t)
461+
assert_equal(0, Resque.delayed_timestamp_size(t))
462+
463+
# assert that the active queue is now 90
464+
assert_equal(90, Resque.size(Resque.queue_from_class(SomeIvarJob)))
465+
end
466+
467+
# test to make sure the timestamp is cleaned up
468+
469+
test 'enqueue_delayed_items_for_timestamp handles a watch failure' do
470+
t = Time.now + 60
471+
472+
# create 100 jobs
473+
100.times { Resque.enqueue_at(t, SomeIvarJob) }
474+
assert_equal(100, Resque.delayed_timestamp_size(t))
475+
476+
Resque.redis.stubs(:watch).returns(nil)
477+
478+
Resque.expects(:clean_up_timestamp).never
479+
480+
Resque::Scheduler.enqueue_delayed_items_for_timestamp(t)
481+
end
482+
483+
test 'enqueue_delayed_items_for_timestamp cleans up a timestamp' do
484+
t = Time.now + 60
485+
486+
# create 100 jobs
487+
100.times { Resque.enqueue_at(t, SomeIvarJob) }
488+
assert_equal(100, Resque.delayed_timestamp_size(t))
489+
490+
Resque.expects(:clean_up_timestamp).once
491+
492+
Resque::Scheduler.enqueue_delayed_items_for_timestamp(t)
493+
end
438494

439-
Resque::Scheduler.enqueue_delayed_items_for_timestamp(t)
440495
end
441496

442497
test 'enqueue_delayed_items_for_timestamp creates jobs ' \

0 commit comments

Comments
 (0)