resque-scheduler 2.2.0 → 4.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.github/dependabot.yml +12 -0
  3. data/.github/funding.yml +4 -0
  4. data/.github/workflows/codeql-analysis.yml +59 -0
  5. data/.github/workflows/rubocop.yml +27 -0
  6. data/.github/workflows/ruby.yml +81 -0
  7. data/AUTHORS.md +31 -0
  8. data/CHANGELOG.md +539 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/Gemfile +26 -2
  11. data/LICENSE +1 -1
  12. data/README.md +423 -91
  13. data/Rakefile +12 -35
  14. data/exe/resque-scheduler +5 -0
  15. data/lib/resque/scheduler/cli.rb +147 -0
  16. data/lib/resque/scheduler/configuration.rb +102 -0
  17. data/lib/resque/scheduler/delaying_extensions.rb +371 -0
  18. data/lib/resque/scheduler/env.rb +85 -0
  19. data/lib/resque/scheduler/extension.rb +13 -0
  20. data/lib/resque/scheduler/failure_handler.rb +11 -0
  21. data/lib/resque/scheduler/lock/base.rb +12 -3
  22. data/lib/resque/scheduler/lock/basic.rb +4 -5
  23. data/lib/resque/scheduler/lock/resilient.rb +52 -43
  24. data/lib/resque/scheduler/lock.rb +2 -1
  25. data/lib/resque/scheduler/locking.rb +104 -0
  26. data/lib/resque/scheduler/logger_builder.rb +83 -0
  27. data/lib/resque/scheduler/plugin.rb +31 -0
  28. data/lib/resque/scheduler/scheduling_extensions.rb +142 -0
  29. data/lib/{resque_scheduler → resque/scheduler}/server/views/delayed.erb +23 -8
  30. data/lib/resque/scheduler/server/views/delayed_schedules.erb +20 -0
  31. data/lib/{resque_scheduler → resque/scheduler}/server/views/delayed_timestamp.erb +1 -1
  32. data/lib/resque/scheduler/server/views/scheduler.erb +58 -0
  33. data/lib/resque/scheduler/server/views/search.erb +69 -0
  34. data/lib/resque/scheduler/server/views/search_form.erb +4 -0
  35. data/lib/resque/scheduler/server.rb +268 -0
  36. data/lib/resque/scheduler/signal_handling.rb +40 -0
  37. data/lib/resque/scheduler/tasks.rb +25 -0
  38. data/lib/resque/scheduler/util.rb +39 -0
  39. data/lib/resque/scheduler/version.rb +7 -0
  40. data/lib/resque/scheduler.rb +333 -149
  41. data/lib/resque-scheduler.rb +4 -0
  42. data/resque-scheduler.gemspec +56 -20
  43. metadata +205 -104
  44. data/.gitignore +0 -8
  45. data/.rubocop.yml +0 -120
  46. data/.travis.yml +0 -10
  47. data/HISTORY.md +0 -180
  48. data/lib/resque/scheduler_locking.rb +0 -90
  49. data/lib/resque_scheduler/logger_builder.rb +0 -51
  50. data/lib/resque_scheduler/plugin.rb +0 -25
  51. data/lib/resque_scheduler/server/views/scheduler.erb +0 -44
  52. data/lib/resque_scheduler/server.rb +0 -92
  53. data/lib/resque_scheduler/tasks.rb +0 -40
  54. data/lib/resque_scheduler/version.rb +0 -3
  55. data/lib/resque_scheduler.rb +0 -355
  56. data/script/migrate_to_timestamps_set.rb +0 -14
  57. data/tasks/resque_scheduler.rake +0 -2
  58. data/test/delayed_queue_test.rb +0 -383
  59. data/test/redis-test.conf +0 -108
  60. data/test/resque-web_test.rb +0 -116
  61. data/test/scheduler_args_test.rb +0 -156
  62. data/test/scheduler_hooks_test.rb +0 -23
  63. data/test/scheduler_locking_test.rb +0 -180
  64. data/test/scheduler_setup_test.rb +0 -59
  65. data/test/scheduler_test.rb +0 -256
  66. data/test/support/redis_instance.rb +0 -129
  67. data/test/test_helper.rb +0 -92
  68. /data/lib/{resque_scheduler → resque/scheduler}/server/views/requeue-params.erb +0 -0
@@ -1,355 +0,0 @@
1
- require 'rubygems'
2
- require 'resque'
3
- require 'resque_scheduler/version'
4
- require 'resque/scheduler'
5
- require 'resque_scheduler/plugin'
6
-
7
- module ResqueScheduler
8
-
9
- #
10
- # Accepts a new schedule configuration of the form:
11
- #
12
- # {
13
- # "MakeTea" => {
14
- # "every" => "1m" },
15
- # "some_name" => {
16
- # "cron" => "5/* * * *",
17
- # "class" => "DoSomeWork",
18
- # "args" => "work on this string",
19
- # "description" => "this thing works it"s butter off" },
20
- # ...
21
- # }
22
- #
23
- # Hash keys can be anything and are used to describe and reference
24
- # the scheduled job. If the "class" argument is missing, the key
25
- # is used implicitly as "class" argument - in the "MakeTea" example,
26
- # "MakeTea" is used both as job name and resque worker class.
27
- #
28
- # Any jobs that were in the old schedule, but are not
29
- # present in the new schedule, will be removed.
30
- #
31
- # :cron can be any cron scheduling string
32
- #
33
- # :every can be used in lieu of :cron. see rufus-scheduler's 'every' usage
34
- # for valid syntax. If :cron is present it will take precedence over :every.
35
- #
36
- # :class must be a resque worker class. If it is missing, the job name (hash key)
37
- # will be used as :class.
38
- #
39
- # :args can be any yaml which will be converted to a ruby literal and
40
- # passed in a params. (optional)
41
- #
42
- # :rails_envs is the list of envs where the job gets loaded. Envs are
43
- # comma separated (optional)
44
- #
45
- # :description is just that, a description of the job (optional). If
46
- # params is an array, each element in the array is passed as a separate
47
- # param, otherwise params is passed in as the only parameter to perform.
48
- def schedule=(schedule_hash)
49
- # clean the schedules as it exists in redis
50
- clean_schedules
51
-
52
- schedule_hash = prepare_schedule(schedule_hash)
53
-
54
- # store all schedules in redis, so we can retrieve them back everywhere.
55
- schedule_hash.each do |name, job_spec|
56
- set_schedule(name, job_spec)
57
- end
58
-
59
- # ensure only return the successfully saved data!
60
- reload_schedule!
61
- end
62
-
63
- # Returns the schedule hash
64
- def schedule
65
- @schedule ||= get_schedules
66
- if @schedule.nil?
67
- return {}
68
- end
69
- @schedule
70
- end
71
-
72
- # reloads the schedule from redis
73
- def reload_schedule!
74
- @schedule = get_schedules
75
- end
76
-
77
- # gets the schedules as it exists in redis
78
- def get_schedules
79
- unless redis.exists(:schedules)
80
- return nil
81
- end
82
-
83
- redis.hgetall(:schedules).tap do |h|
84
- h.each do |name, config|
85
- h[name] = decode(config)
86
- end
87
- end
88
- end
89
-
90
- # clean the schedules as it exists in redis, useful for first setup?
91
- def clean_schedules
92
- if redis.exists(:schedules)
93
- redis.hkeys(:schedules).each do |key|
94
- remove_schedule(key)
95
- end
96
- end
97
- @schedule = nil
98
- true
99
- end
100
-
101
- # Create or update a schedule with the provided name and configuration.
102
- #
103
- # Note: values for class and custom_job_class need to be strings,
104
- # not constants.
105
- #
106
- # Resque.set_schedule('some_job', {:class => 'SomeJob',
107
- # :every => '15mins',
108
- # :queue => 'high',
109
- # :args => '/tmp/poop'})
110
- def set_schedule(name, config)
111
- existing_config = get_schedule(name)
112
- unless existing_config && existing_config == config
113
- redis.hset(:schedules, name, encode(config))
114
- redis.sadd(:schedules_changed, name)
115
- end
116
- config
117
- end
118
-
119
- # retrive the schedule configuration for the given name
120
- def get_schedule(name)
121
- decode(redis.hget(:schedules, name))
122
- end
123
-
124
- # remove a given schedule by name
125
- def remove_schedule(name)
126
- redis.hdel(:schedules, name)
127
- redis.sadd(:schedules_changed, name)
128
- end
129
-
130
- # This method is nearly identical to +enqueue+ only it also
131
- # takes a timestamp which will be used to schedule the job
132
- # for queueing. Until timestamp is in the past, the job will
133
- # sit in the schedule list.
134
- def enqueue_at(timestamp, klass, *args)
135
- validate_job!(klass)
136
- enqueue_at_with_queue(queue_from_class(klass), timestamp, klass, *args)
137
- end
138
-
139
- # Identical to +enqueue_at+, except you can also specify
140
- # a queue in which the job will be placed after the
141
- # timestamp has passed. It respects Resque.inline option, by
142
- # creating the job right away instead of adding to the queue.
143
- def enqueue_at_with_queue(queue, timestamp, klass, *args)
144
- return false unless Plugin.run_before_schedule_hooks(klass, *args)
145
-
146
- if Resque.inline? || timestamp.to_i < Time.now.to_i
147
- # Just create the job and let resque perform it right away with inline.
148
- # If the class is a custom job class, call self#scheduled on it. This allows you to do things like
149
- # Resque.enqueue_at(timestamp, CustomJobClass, :opt1 => val1). Otherwise, pass off to Resque.
150
- if klass.respond_to?(:scheduled)
151
- klass.scheduled(queue, klass.to_s(), *args)
152
- else
153
- Resque::Job.create(queue, klass, *args)
154
- end
155
- else
156
- delayed_push(timestamp, job_to_hash_with_queue(queue, klass, args))
157
- end
158
-
159
- Plugin.run_after_schedule_hooks(klass, *args)
160
- end
161
-
162
- # Identical to enqueue_at but takes number_of_seconds_from_now
163
- # instead of a timestamp.
164
- def enqueue_in(number_of_seconds_from_now, klass, *args)
165
- enqueue_at(Time.now + number_of_seconds_from_now, klass, *args)
166
- end
167
-
168
- # Identical to +enqueue_in+, except you can also specify
169
- # a queue in which the job will be placed after the
170
- # number of seconds has passed.
171
- def enqueue_in_with_queue(queue, number_of_seconds_from_now, klass, *args)
172
- enqueue_at_with_queue(queue, Time.now + number_of_seconds_from_now, klass, *args)
173
- end
174
-
175
- # Used internally to stuff the item into the schedule sorted list.
176
- # +timestamp+ can be either in seconds or a datetime object
177
- # Insertion if O(log(n)).
178
- # Returns true if it's the first job to be scheduled at that time, else false
179
- def delayed_push(timestamp, item)
180
- # First add this item to the list for this timestamp
181
- redis.rpush("delayed:#{timestamp.to_i}", encode(item))
182
-
183
- # Store the timestamps at with this item occurs
184
- redis.sadd("timestamps:#{encode(item)}", "delayed:#{timestamp.to_i}")
185
-
186
- # Now, add this timestamp to the zsets. The score and the value are
187
- # the same since we'll be querying by timestamp, and we don't have
188
- # anything else to store.
189
- redis.zadd :delayed_queue_schedule, timestamp.to_i, timestamp.to_i
190
- end
191
-
192
- # Returns an array of timestamps based on start and count
193
- def delayed_queue_peek(start, count)
194
- Array(redis.zrange(:delayed_queue_schedule, start, start+count-1)).collect { |x| x.to_i }
195
- end
196
-
197
- # Returns the size of the delayed queue schedule
198
- def delayed_queue_schedule_size
199
- redis.zcard :delayed_queue_schedule
200
- end
201
-
202
- # Returns the number of jobs for a given timestamp in the delayed queue schedule
203
- def delayed_timestamp_size(timestamp)
204
- redis.llen("delayed:#{timestamp.to_i}").to_i
205
- end
206
-
207
- # Returns an array of delayed items for the given timestamp
208
- def delayed_timestamp_peek(timestamp, start, count)
209
- if 1 == count
210
- r = list_range "delayed:#{timestamp.to_i}", start, count
211
- r.nil? ? [] : [r]
212
- else
213
- list_range "delayed:#{timestamp.to_i}", start, count
214
- end
215
- end
216
-
217
- # Returns the next delayed queue timestamp
218
- # (don't call directly)
219
- def next_delayed_timestamp(at_time=nil)
220
- items = redis.zrangebyscore :delayed_queue_schedule, '-inf', (at_time || Time.now).to_i, :limit => [0, 1]
221
- timestamp = items.nil? ? nil : Array(items).first
222
- timestamp.to_i unless timestamp.nil?
223
- end
224
-
225
- # Returns the next item to be processed for a given timestamp, nil if
226
- # done. (don't call directly)
227
- # +timestamp+ can either be in seconds or a datetime
228
- def next_item_for_timestamp(timestamp)
229
- key = "delayed:#{timestamp.to_i}"
230
-
231
- encoded_item = redis.lpop(key)
232
- redis.srem("timestamps:#{encoded_item}", key)
233
- item = decode(encoded_item)
234
-
235
- # If the list is empty, remove it.
236
- clean_up_timestamp(key, timestamp)
237
- item
238
- end
239
-
240
- # Clears all jobs created with enqueue_at or enqueue_in
241
- def reset_delayed_queue
242
- Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |item|
243
- key = "delayed:#{item}"
244
- items = redis.lrange(key, 0, -1)
245
- redis.pipelined do
246
- items.each { |ts_item| redis.del("timestamps:#{ts_item}") }
247
- end
248
- redis.del key
249
- end
250
-
251
- redis.del :delayed_queue_schedule
252
- end
253
-
254
- # Given an encoded item, remove it from the delayed_queue
255
- def remove_delayed(klass, *args)
256
- search = encode(job_to_hash(klass, args))
257
- timestamps = redis.smembers("timestamps:#{search}")
258
-
259
- replies = redis.pipelined do
260
- timestamps.each do |key|
261
- redis.lrem(key, 0, search)
262
- redis.srem("timestamps:#{search}", key)
263
- end
264
- end
265
-
266
- (replies.nil? || replies.empty?) ? 0 : replies.each_slice(2).collect { |slice| slice.first }.inject(:+)
267
- end
268
-
269
- # Given an encoded item, enqueue it now
270
- def enqueue_delayed(klass, *args)
271
- hash = job_to_hash(klass, args)
272
- remove_delayed(klass, *args).times { Resque::Scheduler.enqueue_from_config(hash) }
273
- end
274
-
275
- # Given a timestamp and job (klass + args) it removes all instances and
276
- # returns the count of jobs removed.
277
- #
278
- # O(N) where N is the number of jobs scheduled to fire at the given
279
- # timestamp
280
- def remove_delayed_job_from_timestamp(timestamp, klass, *args)
281
- key = "delayed:#{timestamp.to_i}"
282
- encoded_job = encode(job_to_hash(klass, args))
283
-
284
- redis.srem("timestamps:#{encoded_job}", key)
285
- count = redis.lrem(key, 0, encoded_job)
286
- clean_up_timestamp(key, timestamp)
287
-
288
- count
289
- end
290
-
291
- def count_all_scheduled_jobs
292
- total_jobs = 0
293
- Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |timestamp|
294
- total_jobs += redis.llen("delayed:#{timestamp}").to_i
295
- end
296
- total_jobs
297
- end
298
-
299
- # Returns delayed jobs schedule timestamp for +klass+, +args+.
300
- def scheduled_at(klass, *args)
301
- search = encode(job_to_hash(klass, args))
302
- redis.smembers("timestamps:#{search}").collect do |key|
303
- key.tr('delayed:', '').to_i
304
- end
305
- end
306
-
307
- private
308
-
309
- def job_to_hash(klass, args)
310
- {:class => klass.to_s, :args => args, :queue => queue_from_class(klass)}
311
- end
312
-
313
- def job_to_hash_with_queue(queue, klass, args)
314
- {:class => klass.to_s, :args => args, :queue => queue}
315
- end
316
-
317
- def clean_up_timestamp(key, timestamp)
318
- # If the list is empty, remove it.
319
-
320
- # Use a watch here to ensure nobody adds jobs to this delayed
321
- # queue while we're removing it.
322
- redis.watch key
323
- if 0 == redis.llen(key).to_i
324
- redis.multi do
325
- redis.del key
326
- redis.zrem :delayed_queue_schedule, timestamp.to_i
327
- end
328
- else
329
- redis.unwatch
330
- end
331
- end
332
-
333
- def validate_job!(klass)
334
- if klass.to_s.empty?
335
- raise Resque::NoClassError.new("Jobs must be given a class.")
336
- end
337
-
338
- unless queue_from_class(klass)
339
- raise Resque::NoQueueError.new("Jobs must be placed onto a queue.")
340
- end
341
- end
342
-
343
- def prepare_schedule(schedule_hash)
344
- prepared_hash = {}
345
- schedule_hash.each do |name, job_spec|
346
- job_spec = job_spec.dup
347
- job_spec['class'] = name unless job_spec.key?('class') || job_spec.key?(:class)
348
- prepared_hash[name] = job_spec
349
- end
350
- prepared_hash
351
- end
352
-
353
- end
354
-
355
- Resque.extend ResqueScheduler
@@ -1,14 +0,0 @@
1
- require 'redis'
2
- require 'resque'
3
-
4
- if ARGV.size != 1
5
- puts "migrate_to_timestamps_set.rb <redis-host:redis-port>"
6
- exit
7
- end
8
-
9
- Resque.redis = ARGV[0]
10
- redis = Resque.redis
11
- Array(redis.keys("delayed:*")).each do |key|
12
- jobs = redis.lrange(key, 0, -1)
13
- jobs.each {|job| redis.sadd("timestamps:#{job}", key)}
14
- end
@@ -1,2 +0,0 @@
1
- $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
- require 'resque_scheduler/tasks'