resque-scheduler 2.5.5 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +12 -6
  3. data/.rubocop.yml +18 -113
  4. data/.rubocop_todo.yml +29 -0
  5. data/.simplecov +3 -1
  6. data/.travis.yml +12 -4
  7. data/.vagrant-provision-as-vagrant.sh +15 -0
  8. data/.vagrant-provision.sh +23 -0
  9. data/.vagrant-skel/bash_profile +7 -0
  10. data/.vagrant-skel/bashrc +7 -0
  11. data/AUTHORS.md +5 -0
  12. data/Gemfile +1 -2
  13. data/HISTORY.md +18 -0
  14. data/README.md +39 -11
  15. data/ROADMAP.md +0 -6
  16. data/Rakefile +11 -19
  17. data/Vagrantfile +14 -0
  18. data/bin/resque-scheduler +2 -2
  19. data/examples/Rakefile +1 -1
  20. data/examples/config/initializers/resque-web.rb +2 -2
  21. data/examples/dynamic-scheduling/app/jobs/fix_schedules_job.rb +2 -4
  22. data/examples/dynamic-scheduling/app/jobs/send_email_job.rb +1 -1
  23. data/examples/dynamic-scheduling/app/models/user.rb +2 -2
  24. data/examples/dynamic-scheduling/lib/tasks/resque.rake +2 -2
  25. data/lib/resque-scheduler.rb +3 -1
  26. data/lib/resque/scheduler.rb +112 -168
  27. data/lib/resque/scheduler/cli.rb +144 -0
  28. data/lib/resque/scheduler/configuration.rb +73 -0
  29. data/lib/resque/scheduler/delaying_extensions.rb +278 -0
  30. data/lib/resque/scheduler/env.rb +61 -0
  31. data/lib/resque/scheduler/extension.rb +13 -0
  32. data/lib/resque/scheduler/lock.rb +2 -1
  33. data/lib/resque/scheduler/lock/base.rb +6 -2
  34. data/lib/resque/scheduler/lock/basic.rb +4 -5
  35. data/lib/resque/scheduler/lock/resilient.rb +30 -37
  36. data/lib/resque/scheduler/locking.rb +94 -0
  37. data/lib/resque/scheduler/logger_builder.rb +72 -0
  38. data/lib/resque/scheduler/plugin.rb +31 -0
  39. data/lib/resque/scheduler/scheduling_extensions.rb +150 -0
  40. data/lib/resque/scheduler/server.rb +246 -0
  41. data/lib/{resque_scheduler → resque/scheduler}/server/views/delayed.erb +2 -1
  42. data/lib/{resque_scheduler → resque/scheduler}/server/views/delayed_schedules.erb +0 -0
  43. data/lib/{resque_scheduler → resque/scheduler}/server/views/delayed_timestamp.erb +0 -0
  44. data/lib/{resque_scheduler → resque/scheduler}/server/views/requeue-params.erb +0 -0
  45. data/lib/{resque_scheduler → resque/scheduler}/server/views/scheduler.erb +16 -1
  46. data/lib/{resque_scheduler → resque/scheduler}/server/views/search.erb +2 -1
  47. data/lib/{resque_scheduler → resque/scheduler}/server/views/search_form.erb +0 -0
  48. data/lib/resque/scheduler/signal_handling.rb +40 -0
  49. data/lib/{resque_scheduler → resque/scheduler}/tasks.rb +3 -5
  50. data/lib/resque/scheduler/util.rb +41 -0
  51. data/lib/resque/scheduler/version.rb +7 -0
  52. data/resque-scheduler.gemspec +21 -19
  53. data/script/migrate_to_timestamps_set.rb +5 -3
  54. data/tasks/resque_scheduler.rake +1 -1
  55. data/test/cli_test.rb +26 -69
  56. data/test/delayed_queue_test.rb +262 -169
  57. data/test/env_test.rb +41 -0
  58. data/test/resque-web_test.rb +169 -48
  59. data/test/scheduler_args_test.rb +73 -41
  60. data/test/scheduler_hooks_test.rb +9 -8
  61. data/test/scheduler_locking_test.rb +55 -36
  62. data/test/scheduler_setup_test.rb +52 -15
  63. data/test/scheduler_task_test.rb +15 -10
  64. data/test/scheduler_test.rb +215 -114
  65. data/test/support/redis_instance.rb +32 -33
  66. data/test/test_helper.rb +33 -36
  67. data/test/util_test.rb +11 -0
  68. metadata +113 -57
  69. data/lib/resque/scheduler_locking.rb +0 -91
  70. data/lib/resque_scheduler.rb +0 -386
  71. data/lib/resque_scheduler/cli.rb +0 -160
  72. data/lib/resque_scheduler/logger_builder.rb +0 -72
  73. data/lib/resque_scheduler/plugin.rb +0 -28
  74. data/lib/resque_scheduler/server.rb +0 -183
  75. data/lib/resque_scheduler/util.rb +0 -34
  76. data/lib/resque_scheduler/version.rb +0 -5
  77. data/test/redis-test.conf +0 -108
@@ -1,91 +0,0 @@
1
- # ### Locking the scheduler process
2
- #
3
- # There are two places in resque-scheduler that need to be synchonized
4
- # in order to be able to run redundant scheduler processes while ensuring jobs don't
5
- # get queued multiple times when the master process changes.
6
- #
7
- # 1) Processing the delayed queues (jobs that are created from enqueue_at/enqueue_in, etc)
8
- # 2) Processing the scheduled (cron-like) jobs from rufus-scheduler
9
- #
10
- # Protecting the delayed queues (#1) is relatively easy. A simple SETNX in
11
- # redis would suffice. However, protecting the scheduled jobs is trickier
12
- # because the clocks on machines could be slightly off or actual firing times
13
- # could vary slightly due to load. If scheduler A's clock is slightly ahead
14
- # of scheduler B's clock (since they are on different machines), when
15
- # scheduler A dies, we need to ensure that scheduler B doesn't queue jobs
16
- # that A already queued before it's death. (This all assumes that it is
17
- # better to miss a few scheduled jobs than it is to run them multiple times
18
- # for the same iteration.)
19
- #
20
- # To avoid queuing multiple jobs in the case of master fail-over, the master
21
- # should remain the master as long as it can rather than a simple SETNX which
22
- # would result in the master roll being passed around frequently.
23
- #
24
- # Locking Scheme:
25
- # Each resque-scheduler process attempts to get the master lock via SETNX.
26
- # Once obtained, it sets the expiration for 3 minutes (configurable). The
27
- # master process continually updates the timeout on the lock key to be 3
28
- # minutes in the future in it's loop(s) (see `run`) and when jobs come out of
29
- # rufus-scheduler (see `load_schedule_job`). That ensures that a minimum of
30
- # 3 minutes must pass since the last queuing operation before a new master is
31
- # chosen. If, for whatever reason, the master fails to update the expiration
32
- # for 3 minutes, the key expires and the lock is up for grabs. If
33
- # miraculously the original master comes back to life, it will realize it is
34
- # no longer the master and stop processing jobs.
35
- #
36
- # The clocks on the scheduler machines can then be up to 3 minutes off from
37
- # each other without the risk of queueing the same scheduled job twice during
38
- # a master change. The catch is, in the event of a master change, no
39
- # scheduled jobs will be queued during those 3 minutes. So, there is a trade
40
- # off: the higher the timeout, the less likely scheduled jobs will be fired
41
- # twice but greater chances of missing scheduled jobs. The lower the timeout,
42
- # less likely jobs will be missed, greater the chances of jobs firing twice. If
43
- # you don't care about jobs firing twice or are certain your machines' clocks
44
- # are well in sync, a lower timeout is preferable. One thing to keep in mind:
45
- # this only effects *scheduled* jobs - delayed jobs will never be lost or
46
- # skipped since eventually a master will come online and it will process
47
- # everything that is ready (no matter how old it is). Scheduled jobs work
48
- # like cron - if you stop cron, no jobs fire while it's stopped and it doesn't
49
- # fire jobs that were missed when it starts up again.
50
-
51
- require 'resque/scheduler/lock'
52
-
53
- module Resque
54
- module SchedulerLocking
55
- def master_lock
56
- @master_lock ||= build_master_lock
57
- end
58
-
59
- def supports_lua?
60
- redis_master_version >= 2.5
61
- end
62
-
63
- def is_master?
64
- master_lock.acquire! || master_lock.locked?
65
- end
66
-
67
- def release_master_lock!
68
- master_lock.release!
69
- end
70
-
71
- private
72
-
73
- def build_master_lock
74
- if supports_lua?
75
- Resque::Scheduler::Lock::Resilient.new(master_lock_key)
76
- else
77
- Resque::Scheduler::Lock::Basic.new(master_lock_key)
78
- end
79
- end
80
-
81
- def master_lock_key
82
- lock_prefix = ENV['RESQUE_SCHEDULER_MASTER_LOCK_PREFIX'] || ''
83
- lock_prefix += ':' if lock_prefix != ''
84
- "#{Resque.redis.namespace}:#{lock_prefix}resque_scheduler_master_lock"
85
- end
86
-
87
- def redis_master_version
88
- Resque.redis.info['redis_version'].to_f
89
- end
90
- end
91
- end
@@ -1,386 +0,0 @@
1
- require 'rubygems'
2
- require 'resque'
3
- require 'resque_scheduler/version'
4
- require 'resque_scheduler/util'
5
- require 'resque/scheduler'
6
- require 'resque_scheduler/plugin'
7
-
8
- module ResqueScheduler
9
- autoload :Cli, 'resque_scheduler/cli'
10
-
11
- #
12
- # Accepts a new schedule configuration of the form:
13
- #
14
- # {
15
- # "MakeTea" => {
16
- # "every" => "1m" },
17
- # "some_name" => {
18
- # "cron" => "5/* * * *",
19
- # "class" => "DoSomeWork",
20
- # "args" => "work on this string",
21
- # "description" => "this thing works it"s butter off" },
22
- # ...
23
- # }
24
- #
25
- # Hash keys can be anything and are used to describe and reference
26
- # the scheduled job. If the "class" argument is missing, the key
27
- # is used implicitly as "class" argument - in the "MakeTea" example,
28
- # "MakeTea" is used both as job name and resque worker class.
29
- #
30
- # Any jobs that were in the old schedule, but are not
31
- # present in the new schedule, will be removed.
32
- #
33
- # :cron can be any cron scheduling string
34
- #
35
- # :every can be used in lieu of :cron. see rufus-scheduler's 'every' usage
36
- # for valid syntax. If :cron is present it will take precedence over :every.
37
- #
38
- # :class must be a resque worker class. If it is missing, the job name (hash key)
39
- # will be used as :class.
40
- #
41
- # :args can be any yaml which will be converted to a ruby literal and
42
- # passed in a params. (optional)
43
- #
44
- # :rails_envs is the list of envs where the job gets loaded. Envs are
45
- # comma separated (optional)
46
- #
47
- # :description is just that, a description of the job (optional). If
48
- # params is an array, each element in the array is passed as a separate
49
- # param, otherwise params is passed in as the only parameter to perform.
50
- def schedule=(schedule_hash)
51
- # clean the schedules as it exists in redis
52
- clean_schedules
53
-
54
- schedule_hash = prepare_schedule(schedule_hash)
55
-
56
- # store all schedules in redis, so we can retrieve them back everywhere.
57
- schedule_hash.each do |name, job_spec|
58
- set_schedule(name, job_spec)
59
- end
60
-
61
- # ensure only return the successfully saved data!
62
- reload_schedule!
63
- end
64
-
65
- # Returns the schedule hash
66
- def schedule
67
- @schedule ||= get_schedules
68
- if @schedule.nil?
69
- return {}
70
- end
71
- @schedule
72
- end
73
-
74
- # reloads the schedule from redis
75
- def reload_schedule!
76
- @schedule = get_schedules
77
- end
78
-
79
- # gets the schedules as it exists in redis
80
- def get_schedules
81
- unless redis.exists(:schedules)
82
- return nil
83
- end
84
-
85
- redis.hgetall(:schedules).tap do |h|
86
- h.each do |name, config|
87
- h[name] = decode(config)
88
- end
89
- end
90
- end
91
-
92
- # clean the schedules as it exists in redis, useful for first setup?
93
- def clean_schedules
94
- if redis.exists(:schedules)
95
- redis.hkeys(:schedules).each do |key|
96
- remove_schedule(key) if !schedule_persisted?(key)
97
- end
98
- end
99
- @schedule = nil
100
- true
101
- end
102
-
103
- # Create or update a schedule with the provided name and configuration.
104
- #
105
- # Note: values for class and custom_job_class need to be strings,
106
- # not constants.
107
- #
108
- # Resque.set_schedule('some_job', {:class => 'SomeJob',
109
- # :every => '15mins',
110
- # :queue => 'high',
111
- # :args => '/tmp/poop'})
112
- def set_schedule(name, config)
113
- existing_config = get_schedule(name)
114
- persist = config.delete(:persist) || config.delete('persist')
115
- unless existing_config && existing_config == config
116
- redis.pipelined do
117
- redis.hset(:schedules, name, encode(config))
118
- redis.sadd(:schedules_changed, name)
119
- if persist
120
- redis.sadd(:persisted_schedules, name)
121
- end
122
- end
123
- end
124
- config
125
- end
126
-
127
- # retrive the schedule configuration for the given name
128
- def get_schedule(name)
129
- decode(redis.hget(:schedules, name))
130
- end
131
-
132
- def schedule_persisted?(name)
133
- redis.sismember(:persisted_schedules, name)
134
- end
135
-
136
- # remove a given schedule by name
137
- def remove_schedule(name)
138
- redis.pipelined do
139
- redis.hdel(:schedules, name)
140
- redis.srem(:persisted_schedules, name)
141
- redis.sadd(:schedules_changed, name)
142
- end
143
- end
144
-
145
- # This method is nearly identical to +enqueue+ only it also
146
- # takes a timestamp which will be used to schedule the job
147
- # for queueing. Until timestamp is in the past, the job will
148
- # sit in the schedule list.
149
- def enqueue_at(timestamp, klass, *args)
150
- validate(klass)
151
- enqueue_at_with_queue(queue_from_class(klass), timestamp, klass, *args)
152
- end
153
-
154
- # Identical to +enqueue_at+, except you can also specify
155
- # a queue in which the job will be placed after the
156
- # timestamp has passed. It respects Resque.inline option, by
157
- # creating the job right away instead of adding to the queue.
158
- def enqueue_at_with_queue(queue, timestamp, klass, *args)
159
- return false unless Plugin.run_before_schedule_hooks(klass, *args)
160
-
161
- if Resque.inline? || timestamp.to_i < Time.now.to_i
162
- # Just create the job and let resque perform it right away with inline.
163
- # If the class is a custom job class, call self#scheduled on it. This allows you to do things like
164
- # Resque.enqueue_at(timestamp, CustomJobClass, :opt1 => val1). Otherwise, pass off to Resque.
165
- if klass.respond_to?(:scheduled)
166
- klass.scheduled(queue, klass.to_s(), *args)
167
- else
168
- Resque::Job.create(queue, klass, *args)
169
- end
170
- else
171
- delayed_push(timestamp, job_to_hash_with_queue(queue, klass, args))
172
- end
173
-
174
- Plugin.run_after_schedule_hooks(klass, *args)
175
- end
176
-
177
- # Identical to enqueue_at but takes number_of_seconds_from_now
178
- # instead of a timestamp.
179
- def enqueue_in(number_of_seconds_from_now, klass, *args)
180
- enqueue_at(Time.now + number_of_seconds_from_now, klass, *args)
181
- end
182
-
183
- # Identical to +enqueue_in+, except you can also specify
184
- # a queue in which the job will be placed after the
185
- # number of seconds has passed.
186
- def enqueue_in_with_queue(queue, number_of_seconds_from_now, klass, *args)
187
- enqueue_at_with_queue(queue, Time.now + number_of_seconds_from_now, klass, *args)
188
- end
189
-
190
- # Used internally to stuff the item into the schedule sorted list.
191
- # +timestamp+ can be either in seconds or a datetime object
192
- # Insertion if O(log(n)).
193
- # Returns true if it's the first job to be scheduled at that time, else false
194
- def delayed_push(timestamp, item)
195
- # First add this item to the list for this timestamp
196
- redis.rpush("delayed:#{timestamp.to_i}", encode(item))
197
-
198
- # Store the timestamps at with this item occurs
199
- redis.sadd("timestamps:#{encode(item)}", "delayed:#{timestamp.to_i}")
200
-
201
- # Now, add this timestamp to the zsets. The score and the value are
202
- # the same since we'll be querying by timestamp, and we don't have
203
- # anything else to store.
204
- redis.zadd :delayed_queue_schedule, timestamp.to_i, timestamp.to_i
205
- end
206
-
207
- # Returns an array of timestamps based on start and count
208
- def delayed_queue_peek(start, count)
209
- Array(redis.zrange(:delayed_queue_schedule, start, start+count-1)).collect { |x| x.to_i }
210
- end
211
-
212
- # Returns the size of the delayed queue schedule
213
- def delayed_queue_schedule_size
214
- redis.zcard :delayed_queue_schedule
215
- end
216
-
217
- # Returns the number of jobs for a given timestamp in the delayed queue schedule
218
- def delayed_timestamp_size(timestamp)
219
- redis.llen("delayed:#{timestamp.to_i}").to_i
220
- end
221
-
222
- # Returns an array of delayed items for the given timestamp
223
- def delayed_timestamp_peek(timestamp, start, count)
224
- if 1 == count
225
- r = list_range "delayed:#{timestamp.to_i}", start, count
226
- r.nil? ? [] : [r]
227
- else
228
- list_range "delayed:#{timestamp.to_i}", start, count
229
- end
230
- end
231
-
232
- # Returns the next delayed queue timestamp
233
- # (don't call directly)
234
- def next_delayed_timestamp(at_time=nil)
235
- items = redis.zrangebyscore :delayed_queue_schedule, '-inf', (at_time || Time.now).to_i, :limit => [0, 1]
236
- timestamp = items.nil? ? nil : Array(items).first
237
- timestamp.to_i unless timestamp.nil?
238
- end
239
-
240
- # Returns the next item to be processed for a given timestamp, nil if
241
- # done. (don't call directly)
242
- # +timestamp+ can either be in seconds or a datetime
243
- def next_item_for_timestamp(timestamp)
244
- key = "delayed:#{timestamp.to_i}"
245
-
246
- encoded_item = redis.lpop(key)
247
- redis.srem("timestamps:#{encoded_item}", key)
248
- item = decode(encoded_item)
249
-
250
- # If the list is empty, remove it.
251
- clean_up_timestamp(key, timestamp)
252
- item
253
- end
254
-
255
- # Clears all jobs created with enqueue_at or enqueue_in
256
- def reset_delayed_queue
257
- Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |item|
258
- key = "delayed:#{item}"
259
- items = redis.lrange(key, 0, -1)
260
- redis.pipelined do
261
- items.each { |ts_item| redis.del("timestamps:#{ts_item}") }
262
- end
263
- redis.del key
264
- end
265
-
266
- redis.del :delayed_queue_schedule
267
- end
268
-
269
- # Given an encoded item, remove it from the delayed_queue
270
- def remove_delayed(klass, *args)
271
- search = encode(job_to_hash(klass, args))
272
- timestamps = redis.smembers("timestamps:#{search}")
273
-
274
- replies = redis.pipelined do
275
- timestamps.each do |key|
276
- redis.lrem(key, 0, search)
277
- redis.srem("timestamps:#{search}", key)
278
- end
279
- end
280
-
281
- (replies.nil? || replies.empty?) ? 0 : replies.each_slice(2).collect { |slice| slice.first }.inject(:+)
282
- end
283
-
284
- # Given an encoded item, enqueue it now
285
- def enqueue_delayed(klass, *args)
286
- hash = job_to_hash(klass, args)
287
- remove_delayed(klass, *args).times { Resque::Scheduler.enqueue_from_config(hash) }
288
- end
289
-
290
- # Given a block, remove jobs that return true from a block
291
- #
292
- # This allows for removal of delayed jobs that have arguments matching certain criteria
293
- def remove_delayed_selection
294
- fail ArgumentError, "Please supply a block" unless block_given?
295
-
296
- destroyed = 0
297
- # There is no way to search Redis list entries for a partial match, so we query for all
298
- # delayed job tasks and do our matching after decoding the payload data
299
- jobs = Resque.redis.keys("delayed:*")
300
- jobs.each do |job|
301
- index = Resque.redis.llen(job) - 1
302
- while index >= 0
303
- payload = Resque.redis.lindex(job, index)
304
- decoded_payload = decode(payload)
305
- if yield(decoded_payload['args'])
306
- removed = redis.lrem job, 0, payload
307
- destroyed += removed
308
- index -= removed
309
- else
310
- index -= 1
311
- end
312
- end
313
- end
314
- destroyed
315
- end
316
-
317
- # Given a timestamp and job (klass + args) it removes all instances and
318
- # returns the count of jobs removed.
319
- #
320
- # O(N) where N is the number of jobs scheduled to fire at the given
321
- # timestamp
322
- def remove_delayed_job_from_timestamp(timestamp, klass, *args)
323
- key = "delayed:#{timestamp.to_i}"
324
- encoded_job = encode(job_to_hash(klass, args))
325
-
326
- redis.srem("timestamps:#{encoded_job}", key)
327
- count = redis.lrem(key, 0, encoded_job)
328
- clean_up_timestamp(key, timestamp)
329
-
330
- count
331
- end
332
-
333
- def count_all_scheduled_jobs
334
- total_jobs = 0
335
- Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |timestamp|
336
- total_jobs += redis.llen("delayed:#{timestamp}").to_i
337
- end
338
- total_jobs
339
- end
340
-
341
- # Returns delayed jobs schedule timestamp for +klass+, +args+.
342
- def scheduled_at(klass, *args)
343
- search = encode(job_to_hash(klass, args))
344
- redis.smembers("timestamps:#{search}").collect do |key|
345
- key.tr('delayed:', '').to_i
346
- end
347
- end
348
-
349
- private
350
-
351
- def job_to_hash(klass, args)
352
- {:class => klass.to_s, :args => args, :queue => queue_from_class(klass)}
353
- end
354
-
355
- def job_to_hash_with_queue(queue, klass, args)
356
- {:class => klass.to_s, :args => args, :queue => queue}
357
- end
358
-
359
- def clean_up_timestamp(key, timestamp)
360
- # If the list is empty, remove it.
361
-
362
- # Use a watch here to ensure nobody adds jobs to this delayed
363
- # queue while we're removing it.
364
- redis.watch key
365
- if 0 == redis.llen(key).to_i
366
- redis.multi do
367
- redis.del key
368
- redis.zrem :delayed_queue_schedule, timestamp.to_i
369
- end
370
- else
371
- redis.unwatch
372
- end
373
- end
374
-
375
- def prepare_schedule(schedule_hash)
376
- prepared_hash = {}
377
- schedule_hash.each do |name, job_spec|
378
- job_spec = job_spec.dup
379
- job_spec['class'] = name unless job_spec.key?('class') || job_spec.key?(:class)
380
- prepared_hash[name] = job_spec
381
- end
382
- prepared_hash
383
- end
384
- end
385
-
386
- Resque.extend ResqueScheduler