resque-scheduler 4.1.0 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of resque-scheduler might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b1c100ee487f2bbb303e4bd2efae46172ac89322
4
- data.tar.gz: eccb0c76e7ed34839c8c585fa6751bc3c114f521
3
+ metadata.gz: ac7465394b46317c0dcce4eff62880f4bd74a823
4
+ data.tar.gz: 1e6070308b4cce022aa2045b7980c25ac4fc2418
5
5
  SHA512:
6
- metadata.gz: 9c8b1b82cf0d16eed3f711cfba27787232b4b282f8e329e98708f3a4a348a43b416acf74879165b3e1671b7a83c2d7a1aad25884ba5de5c64736c1a099a38696
7
- data.tar.gz: ccd696fa8683b597401da6e0adbeaf5edf58ded176fd61a63ad8fbec9f6e5e052b0432e79b4c974f57778454553ce8bf4c25a5e024f3e23a44c807dc551ee2af
6
+ metadata.gz: 1ccd3427d6b1fc19366ae68ccdce9b69fba9502bf6aee13111aa3a5f486fd6334d0158291714de187b54b48000fafa878529a32413214306b86d6d914ccb37ab
7
+ data.tar.gz: 418d846ccd4be98b42e8ead43e44b361e2e1ae27b2f6e1ff761d3724dd54d318d8b339b3b399ab680f096c2d2492f85eedfe10fd4cc0465a6d2a1c93b1bc18c7
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2016-02-10 07:59:57 -0500 using RuboCop version 0.35.1.
3
+ # on 2016-04-22 13:22:42 -0400 using RuboCop version 0.35.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -13,7 +13,7 @@ Lint/AssignmentInCondition:
13
13
  - 'lib/resque/scheduler/delaying_extensions.rb'
14
14
  - 'lib/resque/scheduler/env.rb'
15
15
 
16
- # Offense count: 15
16
+ # Offense count: 16
17
17
  Metrics/AbcSize:
18
18
  Max: 36
19
19
 
@@ -21,7 +21,7 @@ Metrics/AbcSize:
21
21
  Metrics/CyclomaticComplexity:
22
22
  Max: 12
23
23
 
24
- # Offense count: 1
24
+ # Offense count: 2
25
25
  # Configuration parameters: AllowURI, URISchemes.
26
26
  Metrics/LineLength:
27
27
  Max: 87
@@ -34,7 +34,7 @@ Metrics/MethodLength:
34
34
  # Offense count: 2
35
35
  # Configuration parameters: CountComments.
36
36
  Metrics/ModuleLength:
37
- Max: 294
37
+ Max: 314
38
38
 
39
39
  # Offense count: 1
40
40
  Style/CaseEquality:
@@ -54,10 +54,9 @@ Style/FileName:
54
54
  - 'lib/resque-scheduler.rb'
55
55
  - 'test/resque-web_test.rb'
56
56
 
57
- # Offense count: 6
57
+ # Offense count: 5
58
58
  # Configuration parameters: MinBodyLength.
59
59
  Style/GuardClause:
60
60
  Exclude:
61
61
  - 'lib/resque/scheduler.rb'
62
62
  - 'lib/resque/scheduler/lock/basic.rb'
63
- - 'test/support/redis_instance.rb'
data/AUTHORS.md CHANGED
@@ -26,6 +26,7 @@ Resque Scheduler authors
26
26
  - Giovanni Cappellotto
27
27
  - Harry Lascelles
28
28
  - Henrik Nyh
29
+ - Hormoz Kheradmand
29
30
  - James Le Cuirot
30
31
  - Jarkko Mönkkönen
31
32
  - John Crepezzi
@@ -56,6 +57,7 @@ Resque Scheduler authors
56
57
  - Ryan Carver
57
58
  - Sameer Siruguri
58
59
  - Scott Francis
60
+ - Sean Stephens
59
61
  - Sebastian Kippe
60
62
  - Spring MC
61
63
  - tbprojects
@@ -66,11 +68,14 @@ Resque Scheduler authors
66
68
  - Vladislav Shub
67
69
  - V Sreekanth
68
70
  - Warren Sangster
71
+ - Yuri Kasperovich
69
72
  - andreas
70
73
  - bbauer
71
74
  - camol
75
+ - d4rk5eed
72
76
  - fallwith
73
77
  - gravis
74
78
  - hpoydar
75
79
  - malomalo
76
80
  - sawanoboly
81
+ - serek
data/HISTORY.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Resque Scheduler History / ChangeLog / Release Notes
2
2
 
3
+ ## 4.2.0 (2016-04-29)
4
+ * Bugfix for a race condition in concurrent restarts
5
+ * Clean up and simplify the scheduling extension
6
+ * Make `Resque::Scheduler.logger` accessible to user
7
+ * Failure hook support for better extensibility
8
+ * Default failure handler now outputs stacktrace
9
+ * Add index column to scheduler tab
10
+ * Update rufus-scheduler
11
+ * Bugfix for displaying schedules appropriate to the `env`
12
+ in scheduler UI
13
+
14
+ ## 4.1.0 (2016-02-10)
15
+ * View helper to cut down on repetition
16
+ * Bugfix to check thread life only if present
17
+ * New `Resque.(find|enqueue)_delayed_selection` methods to complement
18
+ `Resque.remove_delayed_selection`
19
+ * Leave undefined env vars unset in internal options hash
20
+ * Insulate checking `Rails.env`
21
+ * Documentation updates and typo fixes
22
+
3
23
  ## 4.0.0 (2014-12-21)
4
24
  * Bump rufus-scheduler dependency to `~> 3.0`
5
25
  * Address warning from redis-namespace related to `#unwatch`
data/README.md CHANGED
@@ -170,7 +170,11 @@ Delayed jobs are one-off jobs that you want to be put into a queue at some point
170
170
  in the future. The classic example is sending email:
171
171
 
172
172
  ```ruby
173
- Resque.enqueue_in(5.days, SendFollowUpEmail, :user_id => current_user.id)
173
+ Resque.enqueue_in(
174
+ 5.days,
175
+ SendFollowUpEmail,
176
+ user_id: current_user.id
177
+ )
174
178
  ```
175
179
 
176
180
  This will store the job for 5 days in the resque delayed queue at which time
@@ -178,13 +182,22 @@ the scheduler process will pull it from the delayed queue and put it in the
178
182
  appropriate work queue for the given job and it will be processed as soon as
179
183
  a worker is available (just like any other resque job).
180
184
 
181
- NOTE: The job does not fire **exactly** at the time supplied. Rather, once that
185
+ **NOTE**: The job does not fire **exactly** at the time supplied. Rather, once that
182
186
  time is in the past, the job moves from the delayed queue to the actual resque
183
187
  work queue and will be completed as workers are free to process it.
184
188
 
185
189
  Also supported is `Resque.enqueue_at` which takes a timestamp to queue the
186
190
  job, and `Resque.enqueue_at_with_queue` which takes both a timestamp and a
187
- queue name.
191
+ queue name:
192
+
193
+ ```ruby
194
+ Resque.enqueue_at_with_queue(
195
+ 'queue_name',
196
+ 5.days.from_now,
197
+ SendFollowUpEmail,
198
+ user_id: current_user.id
199
+ )
200
+ ```
188
201
 
189
202
  The delayed queue is stored in redis and is persisted in the same way the
190
203
  standard resque jobs are persisted (redis writing to disk). Delayed jobs differ
@@ -299,7 +312,7 @@ resulting in resetting schedule time on every deploy, so it's probably a good id
299
312
  frequent jobs (like every 10-30 minutes), otherwise - when you use something like `every 20h` and deploy once-twice per day -
300
313
  it will schedule the job for 20 hours from deploy, resulting in a job to never be run.
301
314
 
302
- NOTE: Six parameter cron's are also supported (as they supported by
315
+ **NOTE**: Six parameter cron's are also supported (as they supported by
303
316
  rufus-scheduler which powers the resque-scheduler process). This allows you
304
317
  to schedule jobs per second (ie: `"30 * * * * *"` would fire a job every 30
305
318
  seconds past the minute).
@@ -320,6 +333,10 @@ must pass the following to `resque-scheduler` initialization (see *Installation*
320
333
  Resque::Scheduler.dynamic = true
321
334
  ```
322
335
 
336
+ **NOTE**: In order to delete dynamic schedules via `resque-web` in the
337
+ "Schedule" tab, you must include the `Rack::MethodOverride` middleware (in
338
+ `config.ru` or equivalent).
339
+
323
340
  Dynamic schedules allow for greater flexibility than static schedules as they can be set,
324
341
  unset or changed without having to restart `resque-scheduler`. You can specify, if the schedule
325
342
  must survive a resque-scheduler restart or not. This is done by setting the `persist` configuration
@@ -398,6 +415,13 @@ Similar to the `before_enqueue`- and `after_enqueue`-hooks provided in Resque
398
415
  removed from the delayed queue, but not yet put on a normal queue. It is
399
416
  called before `before_enqueue`-hooks, and on the same job instance as the
400
417
  `before_enqueue`-hooks will be invoked on. Return values are ignored.
418
+ * `on_enqueue_failure`: Called with the job args and the exception that was raised
419
+ while enqueueing a job to resque or external application fails. Return
420
+ values are ignored. For example:
421
+
422
+ ```ruby
423
+ Resque::Scheduler.failure_handler = ExceptionHandlerClass
424
+ ```
401
425
 
402
426
  #### Support for resque-status (and other custom jobs)
403
427
 
@@ -511,7 +535,6 @@ require 'resque/scheduler/server'
511
535
 
512
536
  That should make the scheduler tabs show up in `resque-web`.
513
537
 
514
-
515
538
  #### Changes as of 2.0.0
516
539
 
517
540
  As of resque-scheduler 2.0.0, it's no longer necessary to have the resque-web
@@ -5,6 +5,7 @@ require_relative 'scheduler/configuration'
5
5
  require_relative 'scheduler/locking'
6
6
  require_relative 'scheduler/logger_builder'
7
7
  require_relative 'scheduler/signal_handling'
8
+ require_relative 'scheduler/failure_handler'
8
9
 
9
10
  module Resque
10
11
  module Scheduler
@@ -22,9 +23,14 @@ module Resque
22
23
  public
23
24
 
24
25
  class << self
26
+ attr_writer :logger
27
+
25
28
  # the Rufus::Scheduler jobs that are scheduled
26
29
  attr_reader :scheduled_jobs
27
30
 
31
+ # allow user to set an additional failure handler
32
+ attr_writer :failure_handler
33
+
28
34
  # Schedule all jobs and continually look for delayed jobs (never returns)
29
35
  def run
30
36
  procline 'Starting'
@@ -137,7 +143,7 @@ module Resque
137
143
  if master?
138
144
  log! "queueing #{config['class']} (#{name})"
139
145
  Resque.last_enqueued_at(name, Time.now.to_s)
140
- handle_errors { enqueue_from_config(config) }
146
+ enqueue(config)
141
147
  end
142
148
  end
143
149
  @scheduled_jobs[name] = job
@@ -182,19 +188,24 @@ module Resque
182
188
  end
183
189
  end
184
190
 
191
+ def enqueue_next_item(timestamp)
192
+ item = Resque.next_item_for_timestamp(timestamp)
193
+
194
+ if item
195
+ log "queuing #{item['class']} [delayed]"
196
+ enqueue(item)
197
+ end
198
+
199
+ item
200
+ end
201
+
185
202
  # Enqueues all delayed jobs for a timestamp
186
203
  def enqueue_delayed_items_for_timestamp(timestamp)
187
204
  item = nil
188
205
  loop do
189
206
  handle_shutdown do
190
207
  # Continually check that it is still the master
191
- if master?
192
- item = Resque.next_item_for_timestamp(timestamp)
193
- if item
194
- log "queuing #{item['class']} [delayed]"
195
- handle_errors { enqueue_from_config(item) }
196
- end
197
- end
208
+ item = enqueue_next_item(timestamp) if master?
198
209
  end
199
210
  # continue processing until there are no more ready items in this
200
211
  # timestamp
@@ -202,18 +213,18 @@ module Resque
202
213
  end
203
214
  end
204
215
 
216
+ def enqueue(config)
217
+ enqueue_from_config(config)
218
+ rescue => e
219
+ Resque::Scheduler.failure_handler.on_enqueue_failure(config, e)
220
+ end
221
+
205
222
  def handle_shutdown
206
223
  exit if @shutdown
207
224
  yield
208
225
  exit if @shutdown
209
226
  end
210
227
 
211
- def handle_errors
212
- yield
213
- rescue => e
214
- log_error "#{e.class.name}: #{e.message}"
215
- end
216
-
217
228
  # Enqueues a job based on a config hash
218
229
  def enqueue_from_config(job_config)
219
230
  args = job_config['args'] || job_config[:args]
@@ -330,24 +341,39 @@ module Resque
330
341
 
331
342
  def poll_sleep_loop
332
343
  @sleeping = true
333
- start = Time.now
334
- loop do
335
- elapsed_sleep = (Time.now - start)
336
- remaining_sleep = poll_sleep_amount - elapsed_sleep
337
- break if remaining_sleep <= 0
338
- begin
339
- sleep(remaining_sleep)
340
- handle_signals
341
- rescue Interrupt
342
- if @shutdown
343
- Resque.clean_schedules
344
- release_master_lock
344
+ if poll_sleep_amount > 0
345
+ start = Time.now
346
+ loop do
347
+ elapsed_sleep = (Time.now - start)
348
+ remaining_sleep = poll_sleep_amount - elapsed_sleep
349
+ @do_break = false
350
+ if remaining_sleep <= 0
351
+ @do_break = true
352
+ else
353
+ @do_break = handle_signals_with_operation do
354
+ sleep(remaining_sleep)
355
+ end
345
356
  end
346
- break
357
+ break if @do_break
347
358
  end
359
+ else
360
+ handle_signals_with_operation
348
361
  end
349
362
  end
350
363
 
364
+ def handle_signals_with_operation
365
+ yield if block_given?
366
+ handle_signals
367
+ false
368
+ rescue Interrupt
369
+ before_shutdown if @shutdown
370
+ true
371
+ end
372
+
373
+ def before_shutdown
374
+ release_master_lock
375
+ end
376
+
351
377
  # Sets the shutdown flag, clean schedules and exits if sleeping
352
378
  def shutdown
353
379
  return if @shutdown
@@ -375,9 +401,9 @@ module Resque
375
401
  $0 = argv0
376
402
  end
377
403
 
378
- private
379
-
380
- attr_writer :logger
404
+ def failure_handler
405
+ @failure_handler ||= Resque::Scheduler::FailureHandler
406
+ end
381
407
 
382
408
  def logger
383
409
  @logger ||= Resque::Scheduler::LoggerBuilder.new(
@@ -388,6 +414,8 @@ module Resque
388
414
  ).build
389
415
  end
390
416
 
417
+ private
418
+
391
419
  def app_str
392
420
  app_name ? "[#{app_name}]" : ''
393
421
  end
@@ -210,6 +210,8 @@ module Resque
210
210
  # O(N) where N is the number of jobs scheduled to fire at the given
211
211
  # timestamp
212
212
  def remove_delayed_job_from_timestamp(timestamp, klass, *args)
213
+ return 0 if Resque.inline?
214
+
213
215
  key = "delayed:#{timestamp.to_i}"
214
216
  encoded_job = encode(job_to_hash(klass, args))
215
217
 
@@ -264,6 +266,8 @@ module Resque
264
266
  end
265
267
 
266
268
  def remove_delayed_job(encoded_job)
269
+ return 0 if Resque.inline?
270
+
267
271
  timestamps = redis.smembers("timestamps:#{encoded_job}")
268
272
 
269
273
  replies = redis.pipelined do
@@ -0,0 +1,11 @@
1
+ module Resque
2
+ module Scheduler
3
+ class FailureHandler
4
+ def self.on_enqueue_failure(_, e)
5
+ Resque::Scheduler.log_error(
6
+ "#{e.class.name}: #{e.message} #{e.backtrace.inspect}"
7
+ )
8
+ end
9
+ end
10
+ end
11
+ end
@@ -44,33 +44,11 @@ module Resque
44
44
  # param, otherwise params is passed in as the only parameter to
45
45
  # perform.
46
46
  def schedule=(schedule_hash)
47
- # This operation tries to be as atomic as possible.
48
- # It needs to read the existing schedules outside the transaction.
49
- # Unlikely, but this could still cause a race condition.
50
- #
51
- # A more robust solution would be to SCRIPT it, but that would change
52
- # the required version of Redis.
53
-
54
- # select schedules to remove
55
- if redis.exists(:schedules)
56
- clean_keys = non_persistent_schedules
57
- else
58
- clean_keys = []
59
- end
60
-
61
- # Start the transaction. If this is not atomic and more than one
62
- # process is calling `schedule=` the clean_schedules might overlap a
63
- # set_schedule and cause the schedules to become corrupt.
64
- redis.multi do
65
- clean_schedules(clean_keys)
47
+ @non_persistent_schedules = nil
48
+ prepared_schedules = prepare_schedules(schedule_hash)
66
49
 
67
- schedule_hash = prepare_schedule(schedule_hash)
68
-
69
- # store all schedules in redis, so we can retrieve them back
70
- # everywhere.
71
- schedule_hash.each do |name, job_spec|
72
- set_schedule(name, job_spec)
73
- end
50
+ prepared_schedules.each do |schedule, config|
51
+ set_schedule(schedule, config, false)
74
52
  end
75
53
 
76
54
  # ensure only return the successfully saved data!
@@ -83,33 +61,14 @@ module Resque
83
61
  @schedule || {}
84
62
  end
85
63
 
86
- # reloads the schedule from redis
64
+ # reloads the schedule from redis and memory
87
65
  def reload_schedule!
88
66
  @schedule = all_schedules
89
67
  end
90
68
 
91
69
  # gets the schedules as it exists in redis
92
70
  def all_schedules
93
- return nil unless redis.exists(:schedules)
94
-
95
- redis.hgetall(:schedules).tap do |h|
96
- h.each do |name, config|
97
- h[name] = decode(config)
98
- end
99
- end
100
- end
101
-
102
- # clean the schedules as it exists in redis, useful for first setup?
103
- def clean_schedules(keys = non_persistent_schedules)
104
- keys.each do |key|
105
- remove_schedule(key)
106
- end
107
- @schedule = nil
108
- true
109
- end
110
-
111
- def non_persistent_schedules
112
- redis.hkeys(:schedules).select { |k| !schedule_persisted?(k) }
71
+ non_persistent_schedules.merge(persistent_schedules)
113
72
  end
114
73
 
115
74
  # Create or update a schedule with the provided name and configuration.
@@ -121,35 +80,52 @@ module Resque
121
80
  # :every => '15mins',
122
81
  # :queue => 'high',
123
82
  # :args => '/tmp/poop'})
124
- def set_schedule(name, config)
83
+ #
84
+ # Preventing a reload is optional and available to batch operations
85
+ def set_schedule(name, config, reload = true)
125
86
  persist = config.delete(:persist) || config.delete('persist')
126
- redis.pipelined do
127
- redis.hset(:schedules, name, encode(config))
128
- redis.sadd(:schedules_changed, name)
129
- redis.sadd(:persisted_schedules, name) if persist
87
+
88
+ if persist
89
+ redis.hset(:persistent_schedules, name, encode(config))
90
+ else
91
+ non_persistent_schedules[name] = decode(encode(config))
130
92
  end
131
- config
93
+
94
+ redis.sadd(:schedules_changed, name)
95
+ reload_schedule! if reload
132
96
  end
133
97
 
134
98
  # retrive the schedule configuration for the given name
135
99
  def fetch_schedule(name)
136
- decode(redis.hget(:schedules, name))
137
- end
138
-
139
- def schedule_persisted?(name)
140
- redis.sismember(:persisted_schedules, name)
100
+ schedule[name]
141
101
  end
142
102
 
143
103
  # remove a given schedule by name
144
104
  def remove_schedule(name)
145
- redis.hdel(:schedules, name)
146
- redis.srem(:persisted_schedules, name)
105
+ non_persistent_schedules.delete(name)
106
+ redis.hdel(:persistent_schedules, name)
147
107
  redis.sadd(:schedules_changed, name)
108
+
109
+ reload_schedule!
148
110
  end
149
111
 
150
112
  private
151
113
 
152
- def prepare_schedule(schedule_hash)
114
+ # we store our non-persistent schedules in this hash
115
+ def non_persistent_schedules
116
+ @non_persistent_schedules ||= {}
117
+ end
118
+
119
+ # reads the persistent schedules from redis
120
+ def persistent_schedules
121
+ redis.hgetall(:persistent_schedules).tap do |h|
122
+ h.each do |name, config|
123
+ h[name] = decode(config)
124
+ end
125
+ end
126
+ end
127
+
128
+ def prepare_schedules(schedule_hash)
153
129
  prepared_hash = {}
154
130
  schedule_hash.each do |name, job_spec|
155
131
  job_spec = job_spec.dup
@@ -8,6 +8,8 @@ require 'json'
8
8
  module Resque
9
9
  module Scheduler
10
10
  module Server
11
+ TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S %z'
12
+
11
13
  unless defined?(::Resque::Scheduler::Server::VIEW_PATH)
12
14
  VIEW_PATH = File.join(File.dirname(__FILE__), 'server', 'views')
13
15
  end
@@ -105,10 +107,19 @@ module Resque
105
107
 
106
108
  def delayed_queue_now
107
109
  timestamp = params['timestamp'].to_i
110
+ formatted_time = Time.at(timestamp).strftime(
111
+ ::Resque::Scheduler::Server::TIMESTAMP_FORMAT
112
+ )
113
+
108
114
  if timestamp > 0
109
- Resque::Scheduler.enqueue_delayed_items_for_timestamp(timestamp)
115
+ unless Resque::Scheduler.enqueue_next_item(timestamp)
116
+ @error_message = "Unable to remove item at #{formatted_time}"
117
+ end
118
+ else
119
+ @error_message = "Incorrect timestamp #{formatted_time}"
110
120
  end
111
- redirect u('/overview')
121
+
122
+ erb scheduler_template('delayed')
112
123
  end
113
124
 
114
125
  def delayed_cancel_now
@@ -127,7 +138,11 @@ module Resque
127
138
 
128
139
  module HelperMethods
129
140
  def format_time(t)
130
- t.strftime('%Y-%m-%d %H:%M:%S %z')
141
+ t.strftime(::Resque::Scheduler::Server::TIMESTAMP_FORMAT)
142
+ end
143
+
144
+ def show_job_arguments(args)
145
+ Array(args).map(&:inspect).join("\n")
131
146
  end
132
147
 
133
148
  def queue_from_class_name(class_name)
@@ -196,12 +211,12 @@ module Resque
196
211
  end
197
212
 
198
213
  def scheduled_in_this_env?(name)
199
- return true if Resque.schedule[name]['rails_env'].nil?
214
+ return true if rails_env(name).nil?
200
215
  rails_env(name).split(/[\s,]+/).include?(Resque::Scheduler.env)
201
216
  end
202
217
 
203
218
  def rails_env(name)
204
- Resque.schedule[name]['rails_env']
219
+ Resque.schedule[name]['rails_env'] || Resque.schedule[name]['env']
205
220
  end
206
221
 
207
222
  def scheduler_view(filename, options = {}, locals = {})
@@ -3,6 +3,10 @@
3
3
 
4
4
  <%= scheduler_view :search_form, layout: false %>
5
5
 
6
+ <p style="font-color: red; font-weight: bold;">
7
+ <%= @error_message %>
8
+ </p>
9
+
6
10
  <p class='intro'>
7
11
  This list below contains the timestamps for scheduled delayed jobs.
8
12
  Server local time: <%= Time.now %>
@@ -39,7 +43,7 @@
39
43
  <a href="<%= u "delayed/#{timestamp}" %>">see details</a>
40
44
  <% end %>
41
45
  </td>
42
- <td><%= h(job['args'].inspect) if job && delayed_timestamp_size == 1 %></td>
46
+ <td><%= h(show_job_arguments(job['args'])) if job && delayed_timestamp_size == 1 %></td>
43
47
  <td>
44
48
  <% if job %>
45
49
  <a href="<%=u URI("/delayed/jobs/#{job['class']}?args=" + URI.encode(job['args'].to_json)) %>">All schedules</a>
@@ -1,4 +1,4 @@
1
- <h1>Delayed jobs scheduled for <%= params[:klass] %> (<%= @args %>)</h1>
1
+ <h1>Delayed jobs scheduled for <%= params[:klass] %> (<%= 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 job['args'].inspect %></td>
16
+ <td class='args'><%=h show_job_arguments(job['args']) %></td>
17
17
  </tr>
18
18
  <% end %>
19
19
  <% if jobs.empty? %>
@@ -3,15 +3,20 @@
3
3
  <p class='intro'>
4
4
  The list below contains all scheduled jobs. Click &quot;Queue now&quot; to queue
5
5
  a job immediately.
6
- Server local time: <%= Time.now %>
7
- Current master: <%= Resque.redis.get(Resque::Scheduler.master_lock.key) %>
6
+ <br/> Server local time: <%= Time.now %>
7
+ <br/> Server Environment: <%= Resque::Scheduler.env %>
8
+ <br/> Current master: <%= Resque.redis.get(Resque::Scheduler.master_lock.key) %>
9
+ </p>
10
+ <p class='intro'>
11
+ The highlighted jobs are skipped for current environment.
8
12
  </p>
9
13
  <div style="overflow-y: auto; width:100%; padding: 0px 5px;">
10
14
  <table>
11
15
  <tr>
16
+ <th>Index</th>
12
17
  <% if Resque::Scheduler.dynamic %>
13
18
  <th></th>
14
- <% end %>
19
+ <% end %>
15
20
  <th></th>
16
21
  <th>Name</th>
17
22
  <th>Description</th>
@@ -21,9 +26,10 @@
21
26
  <th>Arguments</th>
22
27
  <th>Last Enqueued</th>
23
28
  </tr>
24
- <% Resque.schedule.keys.sort.select { |n| scheduled_in_this_env?(n) }.each do |name| %>
29
+ <% Resque.schedule.keys.sort.each_with_index do |name, index| %>
25
30
  <% config = Resque.schedule[name] %>
26
- <tr>
31
+ <tr style="<%= scheduled_in_this_env?(name) ? '' : 'color: #9F6000;background: #FEEFB3;' %>">
32
+ <td style="padding-left: 15px;"><%= index+ 1 %>.</td>
27
33
  <% if Resque::Scheduler.dynamic %>
28
34
  <td style="padding-top: 12px; padding-bottom: 2px; width: 10px">
29
35
  <form action="<%= u "/schedule" %>" method="post" style="margin-left: 0">
@@ -44,7 +50,7 @@
44
50
  <td style="white-space:nowrap"><%= h schedule_interval(config) %></td>
45
51
  <td><%= h schedule_class(config) %></td>
46
52
  <td><%= h config['queue'] || queue_from_class_name(config['class']) %></td>
47
- <td><%= h config['args'].inspect %></td>
53
+ <td><%= h show_job_arguments(config['args']) %></td>
48
54
  <td><%= h Resque.get_last_enqueued_at(name) || 'Never' %></td>
49
55
  </tr>
50
56
  <% end %>
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Resque
4
4
  module Scheduler
5
- VERSION = '4.1.0'
5
+ VERSION = '4.2.0'
6
6
  end
7
7
  end
@@ -41,5 +41,5 @@ Gem::Specification.new do |spec|
41
41
  spec.add_runtime_dependency 'mono_logger', '~> 1.0'
42
42
  spec.add_runtime_dependency 'redis', '~> 3.0'
43
43
  spec.add_runtime_dependency 'resque', '~> 1.25'
44
- spec.add_runtime_dependency 'rufus-scheduler', '~> 3.0'
44
+ spec.add_runtime_dependency 'rufus-scheduler', '~> 3.2'
45
45
  end
@@ -265,6 +265,15 @@ context 'DelayedQueue' do
265
265
  end
266
266
  end
267
267
 
268
+ test 'enqueue_next_item picks one job' do
269
+ t = Time.now + 60
270
+
271
+ Resque.enqueue_at(t, SomeIvarJob)
272
+ Resque.enqueue_at(t, SomeIvarJob)
273
+ Resque::Scheduler.enqueue_next_item(t)
274
+ assert_equal(1, Resque.delayed_timestamp_peek(t, 0, 3).length)
275
+ end
276
+
268
277
  test 'enqueue_delayed_items_for_timestamp creates jobs ' \
269
278
  'and empties the delayed queue' do
270
279
  t = Time.now + 60
@@ -335,6 +344,20 @@ context 'DelayedQueue' do
335
344
  assert_equal(0, Resque.redis.scard("timestamps:#{encoded_job}"))
336
345
  end
337
346
 
347
+ test "when Resque.inline = true, remove_delayed doesn't remove the job" \
348
+ 'and returns 0' do
349
+ begin
350
+ Resque.inline = true
351
+
352
+ timestamp = Time.now + 120
353
+ Resque.enqueue_at(timestamp, SomeIvarJob, 'foo', 'bar')
354
+
355
+ assert_equal(0, Resque.remove_delayed(SomeIvarJob))
356
+ ensure
357
+ Resque.inline = false
358
+ end
359
+ end
360
+
338
361
  test 'scheduled_at returns an array containing job schedule time' do
339
362
  t = Time.now + 120
340
363
  Resque.enqueue_at(t, SomeIvarJob)
@@ -816,6 +839,20 @@ context 'DelayedQueue' do
816
839
  assert_equal 0, Resque.delayed_timestamp_size(t2)
817
840
  end
818
841
 
842
+ test 'when Resque.inline = true, remove_delayed_job_from_timestamp' \
843
+ "doesn't remove any jobs and returns 0" do
844
+ begin
845
+ Resque.inline = true
846
+
847
+ timestamp = Time.now + 120
848
+ Resque.enqueue_at(timestamp, SomeIvarJob, 'foo', 'bar')
849
+
850
+ assert_equal(0, Resque.delayed_timestamp_size(timestamp))
851
+ ensure
852
+ Resque.inline = false
853
+ end
854
+ end
855
+
819
856
  test 'remove_delayed_job_from_timestamp removes nothing if there ' \
820
857
  'are no matches' do
821
858
  t = Time.now + 120
@@ -3,35 +3,123 @@ require_relative 'test_helper'
3
3
 
4
4
  context 'Multi Process' do
5
5
  test 'setting schedule= from many process does not corrupt the schedules' do
6
- schedules = {}
7
- counts = []
8
- threads = []
6
+ # more info on why we're not using threads:
7
+ # https://github.com/resque/resque-scheduler/pull/439#discussion_r16788812
8
+ omit('forking is not supported by jruby but this behaviour' \
9
+ ' is best tested using forks') if RUBY_ENGINE == 'jruby'
10
+ schedules_1 = {}
11
+ schedules_2 = {}
12
+ schedules = []
13
+ pids = []
9
14
 
10
15
  # This number may need to be increased if this test is not failing
11
- processes = 20
16
+ processes = 100
12
17
 
13
- schedule_count = 200
18
+ schedule_count = 300
14
19
 
15
20
  schedule_count.times do |n|
16
- schedules["job #{n}"] = { cron: '0 1 0 0 0' }
21
+ schedules_1["1_job_#{n}"] = { cron: '0 1 0 0 0' }
22
+ schedules_2["2_job_#{n}"] = { cron: '0 1 0 0 0' }
17
23
  end
18
24
 
19
25
  processes.times do |n|
20
- threads << Thread.new do
26
+ pids << fork_with_marshalled_pipe_and_result do
21
27
  sleep n * 0.1
22
- Resque.schedule = schedules
23
- counts << Resque.schedule.size
28
+ Resque.schedule = n.even? ? schedules_2 : schedules_1
29
+ Resque.schedule
24
30
  end
25
31
  end
26
32
 
27
- # doing this outside the threads increases the odds of failure
33
+ schedules += get_results_from_children(pids)
34
+
35
+ assert_equal processes, schedules.size,
36
+ 'missing some schedules, did a process die?'
37
+ schedules.each_with_index do |schedule, i|
38
+ assert_equal schedule_count, schedule.size,
39
+ "schedule count is incorrect (schedule[#{i}]: #{schedule})"
40
+ end
41
+ end
42
+
43
+ test 'concurrent shutdowns and startups do not corrupt the schedule' do
44
+ omit('forking is not supported by jruby but this behaviour' \
45
+ ' is best tested using forks') if RUBY_ENGINE == 'jruby'
46
+ counts = []
47
+ children = []
48
+
49
+ processes = 40
50
+
51
+ schedules = {}
52
+ schedule_count = 300
53
+ schedule_count.times do |n|
54
+ schedules["job_#{n}"] = { 'cron' => '0 1 0 0 0' }
55
+ end
56
+
28
57
  Resque.schedule = schedules
29
- counts << Resque.schedule.size
30
58
 
31
- threads.each(&:join)
59
+ processes.times do |n|
60
+ children << fork_with_marshalled_pipe_and_result do
61
+ sleep Random.rand(3) * 0.1
62
+ if n.even?
63
+ Resque.schedule = schedules
64
+ Resque.schedule.size
65
+ else
66
+ Resque::Scheduler.before_shutdown
67
+ nil
68
+ end
69
+ end
70
+ end
71
+
72
+ counts += get_results_from_children(children).compact
32
73
 
74
+ assert_equal processes / 2, counts.size,
75
+ 'missing some counts, did a process die?'
33
76
  counts.each_with_index do |c, i|
34
77
  assert_equal schedule_count, c, "schedule count is incorrect (c: #{i})"
35
78
  end
36
79
  end
80
+
81
+ private
82
+
83
+ def fork_with_marshalled_pipe_and_result
84
+ pipe_read, pipe_write = IO.pipe
85
+ pid = fork do
86
+ pipe_read.close
87
+ result = begin
88
+ [yield, nil]
89
+ rescue StandardError => exc
90
+ [nil, exc]
91
+ end
92
+ pipe_write.syswrite(Marshal.dump(result))
93
+ # exit true the process to get around fork issues on minitest 5
94
+ # see https://github.com/seattlerb/minitest/issues/467
95
+ Process.exit!(true)
96
+ end
97
+ pipe_write.close
98
+
99
+ [pid, pipe_read]
100
+ end
101
+
102
+ def get_results_from_children(children)
103
+ results = []
104
+ children.each do |pid, pipe|
105
+ wait_for_child_process_to_terminate(pid)
106
+
107
+ fail "forked process failed with #{$CHILD_STATUS}" unless $CHILD_STATUS.success?
108
+ result, exc = Marshal.load(pipe.read)
109
+ fail exc if exc
110
+ results << result
111
+ end
112
+ results
113
+ end
114
+
115
+ def wait_for_child_process_to_terminate(pid = -1, timeout = 30)
116
+ Timeout.timeout(timeout) do
117
+ Process.wait(pid)
118
+ end
119
+ rescue Timeout::Error
120
+ Process.kill('KILL', pid)
121
+ # collect status so it doesn't stick around as zombie process
122
+ Process.wait(pid)
123
+ flunk 'Child process did not terminate in time.'
124
+ end
37
125
  end
@@ -51,8 +51,8 @@ context 'on GET to /schedule with scheduled jobs' do
51
51
  assert last_response.body.include?('SomeIvarJob')
52
52
  end
53
53
 
54
- test 'excludes jobs for other envs' do
55
- assert !last_response.body.include?('SomeFancyJob')
54
+ test 'include(highlight) jobs for other envs' do
55
+ assert last_response.body.include?('SomeFancyJob')
56
56
  end
57
57
 
58
58
  test 'includes job used in multiple environments' do
@@ -256,11 +256,10 @@ context 'on POST to /delayed/clear' do
256
256
  end
257
257
 
258
258
  context 'on POST to /delayed/queue_now' do
259
- setup { post '/delayed/queue_now' }
259
+ setup { post '/delayed/queue_now', timestamp: 0 }
260
260
 
261
- test 'redirects to overview' do
262
- assert last_response.status == 302
263
- assert last_response.header['Location'].include? '/overview'
261
+ test 'returns ok status' do
262
+ assert last_response.status == 200
264
263
  end
265
264
  end
266
265
 
@@ -21,4 +21,35 @@ context 'scheduling jobs with hooks' do
21
21
  assert_equal(0, Resque.delayed_timestamp_size(enqueue_time.to_i),
22
22
  'job should not be enqueued')
23
23
  end
24
+
25
+ test 'default failure hooks are called when enqueueing a job fails' do
26
+ config = {
27
+ 'cron' => '* * * * *',
28
+ 'class' => 'SomeRealClass',
29
+ 'args' => '/tmp'
30
+ }
31
+
32
+ e = RuntimeError.new('custom error')
33
+ Resque::Scheduler.expects(:enqueue_from_config).raises(e)
34
+
35
+ Resque::Scheduler::FailureHandler.expects(:on_enqueue_failure).with(config, e)
36
+ Resque::Scheduler.enqueue(config)
37
+ end
38
+
39
+ test 'failure hooks are called when enqueueing a job fails' do
40
+ with_failure_handler(ExceptionHandlerClass) do
41
+ config = {
42
+ 'cron' => '* * * * *',
43
+ 'class' => 'SomeRealClass',
44
+ 'args' => '/tmp'
45
+ }
46
+
47
+ e = RuntimeError.new('custom error')
48
+ Resque::Scheduler.expects(:enqueue_from_config).raises(e)
49
+
50
+ ExceptionHandlerClass.expects(:on_enqueue_failure).with(config, e)
51
+
52
+ Resque::Scheduler.enqueue(config)
53
+ end
54
+ end
24
55
  end
@@ -14,8 +14,17 @@ context 'Resque::Scheduler' do
14
14
 
15
15
  test 'set custom logger' do
16
16
  custom_logger = MonoLogger.new('/dev/null')
17
- Resque::Scheduler.send(:logger=, custom_logger)
18
- assert_equal(custom_logger, Resque::Scheduler.send(:logger))
17
+ Resque::Scheduler.logger = custom_logger
18
+
19
+ custom_logger.expects(:error).once
20
+ Resque::Scheduler.log_error('test')
21
+ end
22
+
23
+ test 'custom logger is accessible' do
24
+ custom_logger = MonoLogger.new('/dev/null')
25
+ Resque::Scheduler.logger = custom_logger
26
+
27
+ assert_equal custom_logger, Resque::Scheduler.logger
19
28
  end
20
29
 
21
30
  test 'configure block' do
@@ -37,4 +37,36 @@ context 'Resque::Scheduler' do
37
37
  Resque::Scheduler.unstub(:release_master_lock)
38
38
  Resque::Scheduler.release_master_lock
39
39
  end
40
+
41
+ test 'can start successfully' do
42
+ Resque::Scheduler.poll_sleep_amount = nil
43
+
44
+ @pid = Process.pid
45
+ Thread.new do
46
+ sleep(0.15)
47
+ Process.kill(:TERM, @pid)
48
+ end
49
+
50
+ assert_raises SystemExit do
51
+ Resque::Scheduler.run
52
+ end
53
+ end
54
+
55
+ test 'sending TERM to scheduler breaks out when poll_sleep_amount = 0' do
56
+ Resque::Scheduler.poll_sleep_amount = 0
57
+ Resque::Scheduler.expects(:release_master_lock)
58
+
59
+ @pid = Process.pid
60
+ Thread.new do
61
+ sleep(0.05)
62
+ Process.kill(:TERM, @pid)
63
+ end
64
+
65
+ assert_raises SystemExit do
66
+ Resque::Scheduler.run
67
+ end
68
+
69
+ Resque::Scheduler.unstub(:release_master_lock)
70
+ Resque::Scheduler.release_master_lock
71
+ end
40
72
  end
@@ -31,16 +31,16 @@ context 'Resque::Scheduler' do
31
31
  Resque::Scheduler.env = 'production'
32
32
  config = {
33
33
  'cron' => '* * * * *',
34
- 'class' => 'SomeRealClass',
34
+ 'class' => 'SomeJobWithResqueHooks',
35
35
  'args' => '/tmp'
36
36
  }
37
37
 
38
38
  Resque::Job.expects(:create).with(
39
- SomeRealClass.queue, SomeRealClass, '/tmp'
39
+ SomeJobWithResqueHooks.queue, SomeJobWithResqueHooks, '/tmp'
40
40
  )
41
- SomeRealClass.expects(:before_delayed_enqueue_example).with('/tmp')
42
- SomeRealClass.expects(:before_enqueue_example).with('/tmp')
43
- SomeRealClass.expects(:after_enqueue_example).with('/tmp')
41
+ SomeJobWithResqueHooks.expects(:before_delayed_enqueue_example).with('/tmp')
42
+ SomeJobWithResqueHooks.expects(:before_enqueue_example).with('/tmp')
43
+ SomeJobWithResqueHooks.expects(:after_enqueue_example).with('/tmp')
44
44
 
45
45
  Resque::Scheduler.enqueue_from_config(config)
46
46
  end
@@ -59,7 +59,7 @@ context 'Resque::Scheduler' do
59
59
  assert_equal(0, Resque::Scheduler.rufus_scheduler.jobs.size)
60
60
 
61
61
  Resque.schedule = {
62
- some_ivar_job: {
62
+ 'some_ivar_job' => {
63
63
  'cron' => '* * * * *',
64
64
  'class' => 'SomeIvarJob',
65
65
  'args' => '/tmp'
@@ -87,15 +87,13 @@ context 'Resque::Scheduler' do
87
87
  assert Resque::Scheduler.scheduled_jobs.include?('some_ivar_job')
88
88
 
89
89
  Resque.redis.del(:schedules)
90
- Resque.redis.hset(
91
- :schedules,
92
- 'some_ivar_job2',
93
- Resque.encode(
90
+ Resque.schedule = {
91
+ 'some_ivar_job2' => {
94
92
  'cron' => '* * * * *',
95
93
  'class' => 'SomeIvarJob',
96
94
  'args' => '/tmp/2'
97
- )
98
- )
95
+ }
96
+ }
99
97
 
100
98
  Resque::Scheduler.reload_schedule!
101
99
 
@@ -313,7 +311,7 @@ context 'Resque::Scheduler' do
313
311
  }
314
312
  assert_equal(
315
313
  { 'cron' => '* * * * *', 'class' => 'SomeIvarJob', 'args' => '/tmp/75' },
316
- Resque.decode(Resque.redis.hget(:schedules, 'my_ivar_job'))
314
+ Resque.schedule['my_ivar_job']
317
315
  )
318
316
  end
319
317
 
@@ -346,7 +344,7 @@ context 'Resque::Scheduler' do
346
344
  } }
347
345
  assert_equal(
348
346
  { 'cron' => '* * * * *', 'class' => 'SomeIvarJob', 'args' => '/tmp/75' },
349
- Resque.decode(Resque.redis.hget(:schedules, 'SomeIvarJob'))
347
+ Resque.schedule['SomeIvarJob']
350
348
  )
351
349
  assert_equal('SomeIvarJob', Resque.schedule['SomeIvarJob']['class'])
352
350
  end
@@ -366,21 +364,19 @@ context 'Resque::Scheduler' do
366
364
  )
367
365
  assert_equal(
368
366
  { 'cron' => '* * * * *', 'class' => 'SomeIvarJob', 'args' => '/tmp/22' },
369
- Resque.decode(Resque.redis.hget(:schedules, 'some_ivar_job'))
367
+ Resque.schedule['some_ivar_job']
370
368
  )
371
369
  assert Resque.redis.sismember(:schedules_changed, 'some_ivar_job')
372
370
  end
373
371
 
374
372
  test 'fetch_schedule returns a schedule' do
375
- Resque.redis.hset(
376
- :schedules,
377
- 'some_ivar_job2',
378
- Resque.encode(
373
+ Resque.schedule = {
374
+ 'some_ivar_job2' => {
379
375
  'cron' => '* * * * *',
380
376
  'class' => 'SomeIvarJob',
381
377
  'args' => '/tmp/33'
382
- )
383
- )
378
+ }
379
+ }
384
380
  assert_equal(
385
381
  { 'cron' => '* * * * *', 'class' => 'SomeIvarJob', 'args' => '/tmp/33' },
386
382
  Resque.fetch_schedule('some_ivar_job2')
@@ -49,6 +49,10 @@ unless defined?(Rails)
49
49
  end
50
50
  end
51
51
 
52
+ class ExceptionHandlerClass
53
+ def self.on_enqueue_failure(_, _); end
54
+ end
55
+
52
56
  class FakeCustomJobClass
53
57
  def self.scheduled(_, _, *_); end
54
58
  end
@@ -89,6 +93,12 @@ class SomeRealClass
89
93
  end
90
94
  end
91
95
 
96
+ class SomeJobWithResqueHooks < SomeRealClass
97
+ def before_enqueue_example; end
98
+
99
+ def after_enqueue_example; end
100
+ end
101
+
92
102
  class JobWithParams
93
103
  def self.perform(*args)
94
104
  @args = args
@@ -115,7 +125,7 @@ def nullify_logger
115
125
  c.quiet = nil
116
126
  c.verbose = nil
117
127
  c.logfile = nil
118
- c.send(:logger=, nil)
128
+ c.logger = nil
119
129
  end
120
130
 
121
131
  ENV['LOGFILE'] = nil
@@ -126,4 +136,12 @@ def restore_devnull_logfile
126
136
  ENV['LOGFILE'] = '/dev/null'
127
137
  end
128
138
 
139
+ def with_failure_handler(handler)
140
+ original_handler = Resque::Scheduler.failure_handler
141
+ Resque::Scheduler.failure_handler = handler
142
+ yield
143
+ ensure
144
+ Resque::Scheduler.failure_handler = original_handler
145
+ end
146
+
129
147
  restore_devnull_logfile
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resque-scheduler
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.0
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben VandenBos
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-02-10 00:00:00.000000000 Z
11
+ date: 2016-04-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -226,14 +226,14 @@ dependencies:
226
226
  requirements:
227
227
  - - "~>"
228
228
  - !ruby/object:Gem::Version
229
- version: '3.0'
229
+ version: '3.2'
230
230
  type: :runtime
231
231
  prerelease: false
232
232
  version_requirements: !ruby/object:Gem::Requirement
233
233
  requirements:
234
234
  - - "~>"
235
235
  - !ruby/object:Gem::Version
236
- version: '3.0'
236
+ version: '3.2'
237
237
  description: |2
238
238
  Light weight job scheduling on top of Resque.
239
239
  Adds methods enqueue_at/enqueue_in to schedule jobs in the future.
@@ -280,6 +280,7 @@ files:
280
280
  - lib/resque/scheduler/delaying_extensions.rb
281
281
  - lib/resque/scheduler/env.rb
282
282
  - lib/resque/scheduler/extension.rb
283
+ - lib/resque/scheduler/failure_handler.rb
283
284
  - lib/resque/scheduler/lock.rb
284
285
  - lib/resque/scheduler/lock/base.rb
285
286
  - lib/resque/scheduler/lock/basic.rb
@@ -336,7 +337,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
336
337
  version: '0'
337
338
  requirements: []
338
339
  rubyforge_project:
339
- rubygems_version: 2.5.1
340
+ rubygems_version: 2.2.3
340
341
  signing_key:
341
342
  specification_version: 4
342
343
  summary: Light weight job scheduling on top of Resque
@@ -354,4 +355,3 @@ test_files:
354
355
  - test/scheduler_test.rb
355
356
  - test/test_helper.rb
356
357
  - test/util_test.rb
357
- has_rdoc: