resque-scheduler 2.5.5 → 3.0.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.

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