resque-scheduler 4.4.0 → 4.10.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -24,7 +24,7 @@ module Resque
24
24
  def enqueue_at_with_queue(queue, timestamp, klass, *args)
25
25
  return false unless plugin.run_before_schedule_hooks(klass, *args)
26
26
 
27
- if Resque.inline? || timestamp.to_i < Time.now.to_i
27
+ if Resque.inline? || timestamp.to_i <= Time.now.to_i
28
28
  # Just create the job and let resque perform it right away with
29
29
  # inline. If the class is a custom job class, call self#scheduled
30
30
  # on it. This allows you to do things like
@@ -33,7 +33,7 @@ module Resque
33
33
  if klass.respond_to?(:scheduled)
34
34
  klass.scheduled(queue, klass.to_s, *args)
35
35
  else
36
- Resque::Job.create(queue, klass, *args)
36
+ Resque.enqueue_to(queue, klass, *args)
37
37
  end
38
38
  else
39
39
  delayed_push(timestamp, job_to_hash_with_queue(queue, klass, args))
@@ -63,16 +63,34 @@ module Resque
63
63
  klass, *args)
64
64
  end
65
65
 
66
+ # Update the delayed timestamp of any matching delayed jobs or enqueue a
67
+ # new job if no matching jobs are found. Returns the number of delayed or
68
+ # enqueued jobs.
69
+ def delay_or_enqueue_at(timestamp, klass, *args)
70
+ count = remove_delayed(klass, *args)
71
+ count = 1 if count == 0
72
+
73
+ count.times do
74
+ enqueue_at(timestamp, klass, *args)
75
+ end
76
+ end
77
+
78
+ # Identical to +delay_or_enqueue_at+, except it takes
79
+ # number_of_seconds_from_now instead of a timestamp
80
+ def delay_or_enqueue_in(number_of_seconds_from_now, klass, *args)
81
+ delay_or_enqueue_at(Time.now + number_of_seconds_from_now, klass, *args)
82
+ end
83
+
66
84
  # Used internally to stuff the item into the schedule sorted list.
67
- # +timestamp+ can be either in seconds or a datetime object Insertion
68
- # if O(log(n)). Returns true if it's the first job to be scheduled at
69
- # that time, else false
85
+ # +timestamp+ can be either in seconds or a datetime object. The
86
+ # insertion time complexity is O(log(n)). Returns true if it's
87
+ # the first job to be scheduled at that time, else false.
70
88
  def delayed_push(timestamp, item)
71
89
  # First add this item to the list for this timestamp
72
90
  redis.rpush("delayed:#{timestamp.to_i}", encode(item))
73
91
 
74
92
  # Store the timestamps at with this item occurs
75
- redis.sadd("timestamps:#{encode(item)}", "delayed:#{timestamp.to_i}")
93
+ redis.sadd("timestamps:#{encode(item)}", ["delayed:#{timestamp.to_i}"])
76
94
 
77
95
  # Now, add this timestamp to the zsets. The score and the value are
78
96
  # the same since we'll be querying by timestamp, and we don't have
@@ -88,6 +106,7 @@ module Resque
88
106
  end
89
107
 
90
108
  # Returns the size of the delayed queue schedule
109
+ # this does not represent the number of items in the queue to be scheduled
91
110
  def delayed_queue_schedule_size
92
111
  redis.zcard :delayed_queue_schedule
93
112
  end
@@ -121,7 +140,7 @@ module Resque
121
140
  key = "delayed:#{timestamp.to_i}"
122
141
 
123
142
  encoded_item = redis.lpop(key)
124
- redis.srem("timestamps:#{encoded_item}", key)
143
+ redis.srem("timestamps:#{encoded_item}", [key])
125
144
  item = decode(encoded_item)
126
145
 
127
146
  # If the list is empty, remove it.
@@ -134,10 +153,7 @@ module Resque
134
153
  Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |item|
135
154
  key = "delayed:#{item}"
136
155
  items = redis.lrange(key, 0, -1)
137
- redis.pipelined do
138
- items.each { |ts_item| redis.del("timestamps:#{ts_item}") }
139
- end
140
- redis.del key
156
+ redis.del(key, items.map { |ts_item| "timestamps:#{ts_item}" })
141
157
  end
142
158
 
143
159
  redis.del :delayed_queue_schedule
@@ -149,6 +165,11 @@ module Resque
149
165
  remove_delayed_job(search)
150
166
  end
151
167
 
168
+ def remove_delayed_in_queue(klass, queue, *args)
169
+ search = encode(job_to_hash_with_queue(queue, klass, args))
170
+ remove_delayed_job(search)
171
+ end
172
+
152
173
  # Given an encoded item, enqueue it now
153
174
  def enqueue_delayed(klass, *args)
154
175
  hash = job_to_hash(klass, args)
@@ -157,6 +178,13 @@ module Resque
157
178
  end
158
179
  end
159
180
 
181
+ def enqueue_delayed_with_queue(klass, queue, *args)
182
+ hash = job_to_hash_with_queue(queue, klass, args)
183
+ remove_delayed_in_queue(klass, queue, *args).times do
184
+ Resque::Scheduler.enqueue_from_config(hash)
185
+ end
186
+ end
187
+
160
188
  # Given a block, remove jobs that return true from a block
161
189
  #
162
190
  # This allows for removal of delayed jobs that have arguments matching
@@ -181,7 +209,15 @@ module Resque
181
209
  found_jobs.reduce(0) do |sum, encoded_job|
182
210
  decoded_job = decode(encoded_job)
183
211
  klass = Util.constantize(decoded_job['class'])
184
- sum + enqueue_delayed(klass, *decoded_job['args'])
212
+ queue = decoded_job['queue']
213
+
214
+ if queue
215
+ jobs_queued = enqueue_delayed_with_queue(klass, queue, *decoded_job['args'])
216
+ else
217
+ jobs_queued = enqueue_delayed(klass, *decoded_job['args'])
218
+ end
219
+
220
+ jobs_queued + sum
185
221
  end
186
222
  end
187
223
 
@@ -196,9 +232,9 @@ module Resque
196
232
 
197
233
  # Beyond 100 there's almost no improvement in speed
198
234
  found = timestamps.each_slice(100).map do |ts_group|
199
- jobs = redis.pipelined do |r|
235
+ jobs = redis.pipelined do |pipeline|
200
236
  ts_group.each do |ts|
201
- r.lrange("delayed:#{ts}", 0, -1)
237
+ pipeline.lrange("delayed:#{ts}", 0, -1)
202
238
  end
203
239
  end
204
240
 
@@ -221,7 +257,7 @@ module Resque
221
257
  key = "delayed:#{timestamp.to_i}"
222
258
  encoded_job = encode(job_to_hash(klass, args))
223
259
 
224
- redis.srem("timestamps:#{encoded_job}", key)
260
+ redis.srem("timestamps:#{encoded_job}", [key])
225
261
  count = redis.lrem(key, 0, encoded_job)
226
262
  clean_up_timestamp(key, timestamp)
227
263
 
@@ -261,6 +297,22 @@ module Resque
261
297
  redis.hget('delayed:last_enqueued_at', job_name)
262
298
  end
263
299
 
300
+ def clean_up_timestamp(key, timestamp)
301
+ # Use a watch here to ensure nobody adds jobs to this delayed
302
+ # queue while we're removing it.
303
+ redis.watch(key) do
304
+ if redis.llen(key).to_i == 0
305
+ # If the list is empty, remove it.
306
+ redis.multi do |transaction|
307
+ transaction.del(key)
308
+ transaction.zrem(:delayed_queue_schedule, timestamp.to_i)
309
+ end
310
+ else
311
+ redis.redis.unwatch
312
+ end
313
+ end
314
+ end
315
+
264
316
  private
265
317
 
266
318
  def job_to_hash(klass, args)
@@ -271,38 +323,27 @@ module Resque
271
323
  { class: klass.to_s, args: args, queue: queue }
272
324
  end
273
325
 
326
+ # Removes a job from the queue, but not modify the timestamp schedule. This method
327
+ # will not effect the output of `delayed_queue_schedule_size`
274
328
  def remove_delayed_job(encoded_job)
275
329
  return 0 if Resque.inline?
276
330
 
277
331
  timestamps = redis.smembers("timestamps:#{encoded_job}")
278
332
 
279
- replies = redis.pipelined do
333
+ replies = redis.pipelined do |pipeline|
280
334
  timestamps.each do |key|
281
- redis.lrem(key, 0, encoded_job)
282
- redis.srem("timestamps:#{encoded_job}", key)
335
+ pipeline.lrem(key, 0, encoded_job)
336
+ pipeline.srem("timestamps:#{encoded_job}", [key])
283
337
  end
284
338
  end
285
339
 
340
+ # timestamp key is not removed from the schedule, this is done later
341
+ # by the scheduler loop
342
+
286
343
  return 0 if replies.nil? || replies.empty?
287
344
  replies.each_slice(2).map(&:first).inject(:+)
288
345
  end
289
346
 
290
- def clean_up_timestamp(key, timestamp)
291
- # Use a watch here to ensure nobody adds jobs to this delayed
292
- # queue while we're removing it.
293
- redis.watch(key) do
294
- if redis.llen(key).to_i == 0
295
- # If the list is empty, remove it.
296
- redis.multi do
297
- redis.del(key)
298
- redis.zrem(:delayed_queue_schedule, timestamp.to_i)
299
- end
300
- else
301
- redis.redis.unwatch
302
- end
303
- end
304
- end
305
-
306
347
  def search_first_delayed_timestamp_in_range(start_at, stop_at)
307
348
  start_at = start_at.nil? ? '-inf' : start_at.to_i
308
349
  stop_at = stop_at.nil? ? '+inf' : stop_at.to_i
@@ -38,12 +38,8 @@ module Resque
38
38
  true
39
39
  end
40
40
 
41
- unless Process.respond_to?('daemon')
42
- abort 'background option is set, which requires ruby >= 1.9'
43
- end
44
-
45
41
  Process.daemon(true, !Resque::Scheduler.quiet)
46
- Resque.redis._client.reconnect
42
+ Resque.redis.reconnect
47
43
  end
48
44
 
49
45
  def setup_pid_file
@@ -47,7 +47,7 @@ module Resque
47
47
 
48
48
  def hostname
49
49
  local_hostname = Socket.gethostname
50
- Socket.gethostbyname(local_hostname).first
50
+ Addrinfo.getaddrinfo(local_hostname, 'http').first.getnameinfo.first
51
51
  rescue
52
52
  local_hostname
53
53
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  # ### Locking the scheduler process
4
4
  #
5
- # There are two places in resque-scheduler that need to be synchonized in order
5
+ # There are two places in resque-scheduler that need to be synchronized in order
6
6
  # to be able to run redundant scheduler processes while ensuring jobs don't get
7
7
  # queued multiple times when the master process changes.
8
8
  #
@@ -15,7 +15,7 @@ module Resque
15
15
  # - :quiet if logger needs to be silent for all levels. Default - false
16
16
  # - :verbose if there is a need in debug messages. Default - false
17
17
  # - :log_dev to output logs into a desired file. Default - STDOUT
18
- # - :format log format, either 'text' or 'json'. Default - 'text'
18
+ # - :format log format, either 'text', 'json' or 'logfmt'. Default - 'text'
19
19
  #
20
20
  # Example:
21
21
  #
@@ -32,6 +32,7 @@ module Resque
32
32
  # Returns an instance of MonoLogger
33
33
  def build
34
34
  logger = MonoLogger.new(@log_dev)
35
+ logger.progname = 'resque-scheduler'.freeze
35
36
  logger.level = level
36
37
  logger.formatter = send(:"#{@format}_formatter")
37
38
  logger
@@ -50,21 +51,31 @@ module Resque
50
51
  end
51
52
 
52
53
  def text_formatter
53
- proc do |severity, datetime, _progname, msg|
54
- "resque-scheduler: [#{severity}] #{datetime.iso8601}: #{msg}\n"
54
+ proc do |severity, datetime, progname, msg|
55
+ "#{progname}: [#{severity}] #{datetime.iso8601}: #{msg}\n"
55
56
  end
56
57
  end
57
58
 
58
59
  def json_formatter
59
60
  proc do |severity, datetime, progname, msg|
60
61
  require 'json'
61
- JSON.dump(
62
- name: 'resque-scheduler',
62
+ log_data = {
63
+ name: progname,
63
64
  progname: progname,
64
65
  level: severity,
65
66
  timestamp: datetime.iso8601,
66
67
  msg: msg
67
- ) + "\n"
68
+ }
69
+ JSON.dump(log_data) + "\n"
70
+ end
71
+ end
72
+
73
+ def logfmt_formatter
74
+ proc do |severity, datetime, progname, msg|
75
+ "timestamp=\"#{datetime.iso8601}\" " \
76
+ "level=\"#{severity}\" " \
77
+ "progname=\"#{progname}\" " \
78
+ "msg=\"#{msg}\"\n"
68
79
  end
69
80
  end
70
81
  end
@@ -36,7 +36,7 @@ module Resque
36
36
  # :args can be any yaml which will be converted to a ruby literal and
37
37
  # passed in a params. (optional)
38
38
  #
39
- # :rails_envs is the list of envs where the job gets loaded. Envs are
39
+ # :rails_env is the list of envs where the job gets loaded. Envs are
40
40
  # comma separated (optional)
41
41
  #
42
42
  # :description is just that, a description of the job (optional). If
@@ -91,7 +91,7 @@ module Resque
91
91
  non_persistent_schedules[name] = decode(encode(config))
92
92
  end
93
93
 
94
- redis.sadd(:schedules_changed, name)
94
+ redis.sadd(:schedules_changed, [name])
95
95
  reload_schedule! if reload
96
96
  end
97
97
 
@@ -101,12 +101,13 @@ module Resque
101
101
  end
102
102
 
103
103
  # remove a given schedule by name
104
- def remove_schedule(name)
104
+ # Preventing a reload is optional and available to batch operations
105
+ def remove_schedule(name, reload = true)
105
106
  non_persistent_schedules.delete(name)
106
107
  redis.hdel(:persistent_schedules, name)
107
- redis.sadd(:schedules_changed, name)
108
+ redis.sadd(:schedules_changed, [name])
108
109
 
109
- reload_schedule!
110
+ reload_schedule! if reload
110
111
  end
111
112
 
112
113
  private
@@ -1,9 +1,9 @@
1
1
  <h1>Delayed Jobs</h1>
2
- <%- size = resque.delayed_queue_schedule_size %>
2
+ <% size = resque.delayed_queue_schedule_size %>
3
3
 
4
4
  <%= scheduler_view :search_form, layout: false %>
5
5
 
6
- <p style="font-color: red; font-weight: bold;">
6
+ <p style="color: red; font-weight: bold;">
7
7
  <%= @error_message %>
8
8
  </p>
9
9
 
@@ -16,6 +16,14 @@
16
16
  Showing <%= start = params[:start].to_i %> to <%= start + 20 %> of <b><%= size %></b> timestamps
17
17
  </p>
18
18
 
19
+ <% if size > 0 %>
20
+ <div style="padding-bottom: 10px">
21
+ <form method="POST" action="<%= u 'delayed/clear' %>" class='clear-delayed confirmSubmission'>
22
+ <input type='submit' name='' value='Clear Delayed Jobs' />
23
+ </form>
24
+ </div>
25
+ <% end %>
26
+
19
27
  <table>
20
28
  <tr>
21
29
  <th></th>
@@ -27,7 +35,7 @@
27
35
  </tr>
28
36
  <% resque.delayed_queue_peek(start, 20).each do |timestamp| %>
29
37
  <tr>
30
- <td>
38
+ <td style="padding-top: 12px; padding-bottom: 2px; width: 10px">
31
39
  <form action="<%= u "/delayed/queue_now" %>" method="post">
32
40
  <input type="hidden" name="timestamp" value="<%= timestamp.to_i %>">
33
41
  <input type="submit" value="Queue now">
@@ -46,18 +54,11 @@
46
54
  <td><%= h(show_job_arguments(job['args'])) if job && delayed_timestamp_size == 1 %></td>
47
55
  <td>
48
56
  <% if job %>
49
- <a href="<%=u URI("/delayed/jobs/#{URI.escape(job['class'])}?args=" + URI.encode(job['args'].to_json)) %>">All schedules</a>
57
+ <a href="<%= u URI("/delayed/jobs/#{CGI.escape(job['class'])}?args=" + CGI.escape(job['args'].to_json)) %>">All schedules</a>
50
58
  <% end %>
51
59
  </td>
52
60
  </tr>
53
61
  <% end %>
54
62
  </table>
55
63
 
56
- <% if size > 0 %>
57
- <br>
58
- <form method="POST" action="<%=u 'delayed/clear'%>" class='clear-delayed'>
59
- <input type='submit' name='' value='Clear Delayed Jobs' />
60
- </form>
61
- <% end %>
62
-
63
64
  <%= partial :next_more, :start => start, :size => size %>
@@ -1,4 +1,4 @@
1
- <h1>Delayed jobs scheduled for <%= params[:klass] %> (<%= show_job_arguments(@args) %>)</h1>
1
+ <h1>Delayed jobs scheduled for <%=h params[:klass] %> (<%=h show_job_arguments(@args) %>)</h1>
2
2
 
3
3
  <table class='jobs'>
4
4
  <tr>
@@ -13,7 +13,7 @@
13
13
  <% jobs.each do |job| %>
14
14
  <tr>
15
15
  <td class='class'><%= job['class'] %></td>
16
- <td class='args'><%=h show_job_arguments(job['args']) %></td>
16
+ <td class='args'><%= h show_job_arguments(job['args']) %></td>
17
17
  </tr>
18
18
  <% end %>
19
19
  <% if jobs.empty? %>
@@ -8,7 +8,7 @@
8
8
  <br/> Current master: <%= Resque.redis.get(Resque::Scheduler.master_lock.key) %>
9
9
  </p>
10
10
  <p class='intro'>
11
- The highlighted jobs are skipped for current environment.
11
+ The highlighted jobs are skipped for current environment.
12
12
  </p>
13
13
  <div style="overflow-y: auto; width:100%; padding: 0px 5px;">
14
14
  <table>
@@ -29,7 +29,7 @@
29
29
  <% Resque.schedule.keys.sort.each_with_index do |name, index| %>
30
30
  <% config = Resque.schedule[name] %>
31
31
  <tr style="<%= scheduled_in_this_env?(name) ? '' : 'color: #9F6000;background: #FEEFB3;' %>">
32
- <td style="padding-left: 15px;"><%= index+ 1 %>.</td>
32
+ <td style="padding-left: 15px;"><%= index + 1 %>.</td>
33
33
  <% if Resque::Scheduler.dynamic %>
34
34
  <td style="padding-top: 12px; padding-bottom: 2px; width: 10px">
35
35
  <form action="<%= u "/schedule" %>" method="post" style="margin-left: 0">
@@ -13,13 +13,13 @@
13
13
  </tr>
14
14
  <% delayed.each do |job| %>
15
15
  <tr>
16
- <td>
16
+ <td style="padding-top: 12px; padding-bottom: 2px; width: 10px">
17
17
  <form action="<%= u "/delayed/queue_now" %>" method="post">
18
18
  <input type="hidden" name="timestamp" value="<%= job['timestamp'].to_i %>">
19
19
  <input type="submit" value="Queue now">
20
20
  </form>
21
21
  </td>
22
- <td>
22
+ <td style="padding-top: 12px; padding-bottom: 2px; width: 10px">
23
23
  <form action="<%= u "/delayed/cancel_now" %>" method="post">
24
24
  <input type="hidden" name="timestamp" value="<%= job['timestamp'].to_i %>">
25
25
  <input type="hidden" name="klass" value="<%= job['class'] %>">
@@ -33,7 +33,6 @@
33
33
  </tr>
34
34
  <% end %>
35
35
  </table>
36
- </h1>
37
36
 
38
37
  <% queued = @jobs.select { |j| j['where_at'] == 'queued' } %>
39
38
  <h1>Queued jobs</h1>
@@ -68,5 +67,3 @@
68
67
  </tr>
69
68
  <% end %>
70
69
  </table>
71
-
72
-
@@ -1,8 +1,4 @@
1
1
  <form method="POST" action="<%= u 'delayed/search' %>">
2
- <input type='input' name='search' value="<%= params[:search] %>"/>
2
+ <input type='input' name='search' value="<%= h params[:search] %>"/>
3
3
  <input type='submit' value='Search'/>
4
4
  </form>
5
-
6
-
7
-
8
-
@@ -87,7 +87,7 @@ module Resque
87
87
  def delayed_jobs_klass
88
88
  begin
89
89
  klass = Resque::Scheduler::Util.constantize(params[:klass])
90
- @args = JSON.load(URI.decode(params[:args]))
90
+ @args = JSON.load(CGI.unescape(params[:args]))
91
91
  @timestamps = Resque.scheduled_at(klass, *@args)
92
92
  rescue
93
93
  @timestamps = []
@@ -10,13 +10,13 @@ module Resque
10
10
  end
11
11
 
12
12
  # For all signals, set the shutdown flag and wait for current
13
- # poll/enqueing to finish (should be almost instant). In the
13
+ # poll/enqueuing to finish (should be almost instant). In the
14
14
  # case of sleeping, exit immediately.
15
15
  def register_signal_handlers
16
16
  (Signal.list.keys & %w(INT TERM USR1 USR2 QUIT)).each do |sig|
17
17
  trap(sig) do
18
18
  signal_queue << sig
19
- # break sleep in the primary scheduler thread, alowing
19
+ # break sleep in the primary scheduler thread, allowing
20
20
  # the signal queue to get processed as soon as possible.
21
21
  @th.wakeup if @th && @th.alive?
22
22
  end
@@ -4,7 +4,7 @@ module Resque
4
4
  module Scheduler
5
5
  class Util
6
6
  # In order to upgrade to resque(1.25) which has deprecated following
7
- # methods, we just added these usefull helpers back to use in Resque
7
+ # methods, we just added these useful helpers back to use in Resque
8
8
  # Scheduler. refer to:
9
9
  # https://github.com/resque/resque-scheduler/pull/273
10
10
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Resque
4
4
  module Scheduler
5
- VERSION = '4.4.0'.freeze
5
+ VERSION = '4.10.2'.freeze
6
6
  end
7
7
  end
@@ -1,5 +1,6 @@
1
1
  # vim:fileencoding=utf-8
2
2
 
3
+ require 'redis/errors'
3
4
  require 'rufus/scheduler'
4
5
  require_relative 'scheduler/configuration'
5
6
  require_relative 'scheduler/locking'
@@ -205,16 +206,80 @@ module Resque
205
206
 
206
207
  # Enqueues all delayed jobs for a timestamp
207
208
  def enqueue_delayed_items_for_timestamp(timestamp)
208
- item = nil
209
+ count = 0
210
+ batch_size = delayed_requeue_batch_size
211
+ actual_batch_size = nil
212
+
213
+ log "Processing delayed items for timestamp #{timestamp}, in batches of #{batch_size}"
214
+
209
215
  loop do
210
216
  handle_shutdown do
211
217
  # Continually check that it is still the master
212
- item = enqueue_next_item(timestamp) if am_master
218
+ if am_master
219
+ actual_batch_size = enqueue_items_in_batch_for_timestamp(timestamp,
220
+ batch_size)
221
+ end
213
222
  end
214
- # continue processing until there are no more ready items in this
215
- # timestamp
216
- break if item.nil?
223
+
224
+ count += actual_batch_size
225
+ log "queued #{count} jobs" if actual_batch_size != -1
226
+
227
+ # continue processing until there are no more items in this
228
+ # timestamp. If we don't have a full batch, this is the last one.
229
+ # This also breaks us in the event of a redis transaction failure
230
+ # i.e. enqueue_items_in_batch_for_timestamp returned -1
231
+ break if actual_batch_size < batch_size
217
232
  end
233
+
234
+ log "finished queueing #{count} total jobs for timestamp #{timestamp}" if count != -1
235
+ end
236
+
237
+ def timestamp_key(timestamp)
238
+ "delayed:#{timestamp.to_i}"
239
+ end
240
+
241
+ def enqueue_items_in_batch_for_timestamp(timestamp, batch_size)
242
+ timestamp_bucket_key = timestamp_key(timestamp)
243
+
244
+ encoded_jobs_to_requeue = Resque.redis.lrange(timestamp_bucket_key, 0, batch_size - 1)
245
+
246
+ # Watch is used to ensure that the timestamp bucket we are operating on
247
+ # is not altered by any other clients between the watch call and when we call exec
248
+ # (to execute the multi block). We should error catch on the redis.exec return value
249
+ # as that will indicate if the entire transaction was aborted or not. Though we should
250
+ # be safe as our ltrim is inside the multi block and therefore also would have been
251
+ # aborted. So nothing would have been queued, but also nothing lost from the bucket.
252
+ watch_result = Resque.redis.watch(timestamp_bucket_key) do
253
+ Resque.redis.multi do |pipeline|
254
+ encoded_jobs_to_requeue.each do |encoded_job|
255
+ pipeline.srem("timestamps:#{encoded_job}", timestamp_bucket_key)
256
+
257
+ decoded_job = Resque.decode(encoded_job)
258
+ enqueue(decoded_job)
259
+ end
260
+
261
+ pipeline.ltrim(timestamp_bucket_key, batch_size, -1)
262
+ end
263
+ end
264
+
265
+ # Did the multi block successfully remove from this timestamp and enqueue the jobs?
266
+ success = !watch_result.nil?
267
+
268
+ # If this was the last batch in this timestamp bucket, clean up
269
+ if success && encoded_jobs_to_requeue.count < batch_size
270
+ Resque.clean_up_timestamp(timestamp_bucket_key, timestamp)
271
+ end
272
+
273
+ unless success
274
+ # Our batched transaction failed in Redis due to the timestamp_bucket_key value
275
+ # being modified while we built our multi block. We return -1 to ensure we break
276
+ # out of the loop iterating on this timestamp so it can be re-processed via the
277
+ # loop in handle_delayed_items.
278
+ return -1
279
+ end
280
+
281
+ # will return 0 if none were left to batch
282
+ encoded_jobs_to_requeue.count
218
283
  end
219
284
 
220
285
  def enqueue(config)
@@ -250,7 +315,7 @@ module Resque
250
315
  if job_klass && job_klass != 'Resque::Job'
251
316
  # The custom job class API must offer a static "scheduled" method. If
252
317
  # the custom job class can not be constantized (via a requeue call
253
- # from the web perhaps), fall back to enqueing normally via
318
+ # from the web perhaps), fall back to enqueuing normally via
254
319
  # Resque::Job.create.
255
320
  begin
256
321
  Resque::Scheduler::Util.constantize(job_klass).scheduled(
@@ -376,7 +441,6 @@ module Resque
376
441
 
377
442
  def stop_rufus_scheduler
378
443
  rufus_scheduler.shutdown(:wait)
379
- rufus_scheduler.join
380
444
  end
381
445
 
382
446
  def before_shutdown