resque_admin-scheduler 1.0.2

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.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/bin/migrate_to_timestamps_set.rb +16 -0
  3. data/exe/resque-scheduler +5 -0
  4. data/lib/resque-scheduler.rb +4 -0
  5. data/lib/resque/scheduler.rb +447 -0
  6. data/lib/resque/scheduler/cli.rb +147 -0
  7. data/lib/resque/scheduler/configuration.rb +73 -0
  8. data/lib/resque/scheduler/delaying_extensions.rb +324 -0
  9. data/lib/resque/scheduler/env.rb +89 -0
  10. data/lib/resque/scheduler/extension.rb +13 -0
  11. data/lib/resque/scheduler/failure_handler.rb +11 -0
  12. data/lib/resque/scheduler/lock.rb +4 -0
  13. data/lib/resque/scheduler/lock/base.rb +61 -0
  14. data/lib/resque/scheduler/lock/basic.rb +27 -0
  15. data/lib/resque/scheduler/lock/resilient.rb +78 -0
  16. data/lib/resque/scheduler/locking.rb +104 -0
  17. data/lib/resque/scheduler/logger_builder.rb +72 -0
  18. data/lib/resque/scheduler/plugin.rb +31 -0
  19. data/lib/resque/scheduler/scheduling_extensions.rb +141 -0
  20. data/lib/resque/scheduler/server.rb +268 -0
  21. data/lib/resque/scheduler/server/views/delayed.erb +63 -0
  22. data/lib/resque/scheduler/server/views/delayed_schedules.erb +20 -0
  23. data/lib/resque/scheduler/server/views/delayed_timestamp.erb +26 -0
  24. data/lib/resque/scheduler/server/views/requeue-params.erb +23 -0
  25. data/lib/resque/scheduler/server/views/scheduler.erb +58 -0
  26. data/lib/resque/scheduler/server/views/search.erb +72 -0
  27. data/lib/resque/scheduler/server/views/search_form.erb +8 -0
  28. data/lib/resque/scheduler/signal_handling.rb +40 -0
  29. data/lib/resque/scheduler/tasks.rb +25 -0
  30. data/lib/resque/scheduler/util.rb +39 -0
  31. data/lib/resque/scheduler/version.rb +7 -0
  32. data/tasks/resque_scheduler.rake +2 -0
  33. metadata +267 -0
@@ -0,0 +1,147 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ require 'optparse'
4
+
5
+ module Resque
6
+ module Scheduler
7
+ CLI_OPTIONS_ENV_MAPPING = {
8
+ app_name: 'APP_NAME',
9
+ background: 'BACKGROUND',
10
+ dynamic: 'DYNAMIC_SCHEDULE',
11
+ env: 'RAILS_ENV',
12
+ initializer_path: 'INITIALIZER_PATH',
13
+ logfile: 'LOGFILE',
14
+ logformat: 'LOGFORMAT',
15
+ quiet: 'QUIET',
16
+ pidfile: 'PIDFILE',
17
+ poll_sleep_amount: 'RESQUE_SCHEDULER_INTERVAL',
18
+ verbose: 'VERBOSE'
19
+ }.freeze
20
+
21
+ class Cli
22
+ BANNER = <<-EOF.gsub(/ {6}/, '')
23
+ Usage: resque-scheduler [options]
24
+
25
+ Runs a resque scheduler process directly (rather than via rake).
26
+
27
+ EOF
28
+ OPTIONS = [
29
+ {
30
+ args: ['-n', '--app-name [APP_NAME]',
31
+ 'Application name for procline'],
32
+ callback: ->(options) { ->(n) { options[:app_name] = n } }
33
+ },
34
+ {
35
+ args: ['-B', '--background', 'Run in the background [BACKGROUND]'],
36
+ callback: ->(options) { ->(b) { options[:background] = b } }
37
+ },
38
+ {
39
+ args: ['-D', '--dynamic-schedule',
40
+ 'Enable dynamic scheduling [DYNAMIC_SCHEDULE]'],
41
+ callback: ->(options) { ->(d) { options[:dynamic] = d } }
42
+ },
43
+ {
44
+ args: ['-E', '--environment [RAILS_ENV]', 'Environment name'],
45
+ callback: ->(options) { ->(e) { options[:env] = e } }
46
+ },
47
+ {
48
+ args: ['-I', '--initializer-path [INITIALIZER_PATH]',
49
+ 'Path to optional initializer ruby file'],
50
+ callback: ->(options) { ->(i) { options[:initializer_path] = i } }
51
+ },
52
+ {
53
+ args: ['-i', '--interval [RESQUE_SCHEDULER_INTERVAL]',
54
+ 'Interval for checking if a scheduled job must run'],
55
+ callback: ->(options) { ->(i) { options[:poll_sleep_amount] = i } }
56
+ },
57
+ {
58
+ args: ['-l', '--logfile [LOGFILE]', 'Log file name'],
59
+ callback: ->(options) { ->(l) { options[:logfile] = l } }
60
+ },
61
+ {
62
+ args: ['-F', '--logformat [LOGFORMAT]', 'Log output format'],
63
+ callback: ->(options) { ->(f) { options[:logformat] = f } }
64
+ },
65
+ {
66
+ args: ['-P', '--pidfile [PIDFILE]', 'PID file name'],
67
+ callback: ->(options) { ->(p) { options[:pidfile] = p } }
68
+ },
69
+ {
70
+ args: ['-q', '--quiet', 'Run with minimal output [QUIET]'],
71
+ callback: ->(options) { ->(q) { options[:quiet] = q } }
72
+ },
73
+ {
74
+ args: ['-v', '--verbose', 'Run with verbose output [VERBOSE]'],
75
+ callback: ->(options) { ->(v) { options[:verbose] = v } }
76
+ }
77
+ ].freeze
78
+
79
+ def self.run!(argv = ARGV, env = ENV)
80
+ new(argv, env).run!
81
+ end
82
+
83
+ def initialize(argv = ARGV, env = ENV)
84
+ @argv = argv
85
+ @env = env
86
+ end
87
+
88
+ def run!
89
+ pre_run
90
+ run_forever
91
+ end
92
+
93
+ def pre_run
94
+ parse_options
95
+ pre_setup
96
+ setup_env
97
+ end
98
+
99
+ def parse_options
100
+ option_parser.parse!(argv.dup)
101
+ end
102
+
103
+ def pre_setup
104
+ if options[:initializer_path]
105
+ load options[:initializer_path].to_s.strip
106
+ else
107
+ false
108
+ end
109
+ end
110
+
111
+ def setup_env
112
+ require_relative 'env'
113
+ runtime_env.setup
114
+ end
115
+
116
+ def run_forever
117
+ Resque::Scheduler.run
118
+ end
119
+
120
+ private
121
+
122
+ attr_reader :argv, :env
123
+
124
+ def runtime_env
125
+ @runtime_env ||= Resque::Scheduler::Env.new(options)
126
+ end
127
+
128
+ def option_parser
129
+ OptionParser.new do |opts|
130
+ opts.banner = BANNER
131
+ opts.version = Resque::Scheduler::VERSION
132
+ OPTIONS.each do |opt|
133
+ opts.on(*opt[:args], &opt[:callback].call(options))
134
+ end
135
+ end
136
+ end
137
+
138
+ def options
139
+ @options ||= {}.tap do |o|
140
+ CLI_OPTIONS_ENV_MAPPING.each do |key, envvar|
141
+ o[key] = env[envvar] if env.include?(envvar)
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,73 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ module Resque
4
+ module Scheduler
5
+ module Configuration
6
+ # Allows for block-style configuration
7
+ def configure
8
+ yield self
9
+ end
10
+
11
+ # Used in `#load_schedule_job`
12
+ attr_writer :env
13
+
14
+ def env
15
+ return @env if @env
16
+ @env ||= Rails.env if defined?(Rails) && Rails.respond_to?(:env)
17
+ @env ||= ENV['RAILS_ENV']
18
+ @env
19
+ end
20
+
21
+ # If true, logs more stuff...
22
+ attr_writer :verbose
23
+
24
+ def verbose
25
+ @verbose ||= !!ENV['VERBOSE']
26
+ end
27
+
28
+ # If set, produces no output
29
+ attr_writer :quiet
30
+
31
+ def quiet
32
+ @quiet ||= !!ENV['QUIET']
33
+ end
34
+
35
+ # If set, will write messages to the file
36
+ attr_writer :logfile
37
+
38
+ def logfile
39
+ @logfile ||= ENV['LOGFILE']
40
+ end
41
+
42
+ # Sets whether to log in 'text' or 'json'
43
+ attr_writer :logformat
44
+
45
+ def logformat
46
+ @logformat ||= ENV['LOGFORMAT']
47
+ end
48
+
49
+ # If set, will try to update the schedule in the loop
50
+ attr_writer :dynamic
51
+
52
+ def dynamic
53
+ @dynamic ||= !!ENV['DYNAMIC_SCHEDULE']
54
+ end
55
+
56
+ # If set, will append the app name to procline
57
+ attr_writer :app_name
58
+
59
+ def app_name
60
+ @app_name ||= ENV['APP_NAME']
61
+ end
62
+
63
+ # Amount of time in seconds to sleep between polls of the delayed
64
+ # queue. Defaults to 5
65
+ attr_writer :poll_sleep_amount
66
+
67
+ def poll_sleep_amount
68
+ @poll_sleep_amount ||=
69
+ Float(ENV.fetch('RESQUE_SCHEDULER_INTERVAL', '5'))
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,324 @@
1
+ # vim:fileencoding=utf-8
2
+ require 'resque'
3
+ require_relative 'plugin'
4
+ require_relative '../scheduler'
5
+
6
+ module Resque
7
+ module Scheduler
8
+ module DelayingExtensions
9
+ # This method is nearly identical to +enqueue+ only it also
10
+ # takes a timestamp which will be used to schedule the job
11
+ # for queueing. Until timestamp is in the past, the job will
12
+ # sit in the schedule list.
13
+ def enqueue_at(timestamp, klass, *args)
14
+ validate(klass)
15
+ enqueue_at_with_queue(
16
+ queue_from_class(klass), timestamp, klass, *args
17
+ )
18
+ end
19
+
20
+ # Identical to +enqueue_at+, except you can also specify
21
+ # a queue in which the job will be placed after the
22
+ # timestamp has passed. It respects Resque.inline option, by
23
+ # creating the job right away instead of adding to the queue.
24
+ def enqueue_at_with_queue(queue, timestamp, klass, *args)
25
+ return false unless plugin.run_before_schedule_hooks(klass, *args)
26
+
27
+ if Resque.inline? || timestamp.to_i < Time.now.to_i
28
+ # Just create the job and let resque perform it right away with
29
+ # inline. If the class is a custom job class, call self#scheduled
30
+ # on it. This allows you to do things like
31
+ # Resque.enqueue_at(timestamp, CustomJobClass, :opt1 => val1).
32
+ # Otherwise, pass off to Resque.
33
+ if klass.respond_to?(:scheduled)
34
+ klass.scheduled(queue, klass.to_s, *args)
35
+ else
36
+ Resque::Job.create(queue, klass, *args)
37
+ end
38
+ else
39
+ delayed_push(timestamp, job_to_hash_with_queue(queue, klass, args))
40
+ end
41
+
42
+ plugin.run_after_schedule_hooks(klass, *args)
43
+ end
44
+
45
+ # Identical to enqueue_at but takes number_of_seconds_from_now
46
+ # instead of a timestamp.
47
+ def enqueue_in(number_of_seconds_from_now, klass, *args)
48
+ enqueue_at(Time.now + number_of_seconds_from_now, klass, *args)
49
+ end
50
+
51
+ # Identical to +enqueue_in+, except you can also specify
52
+ # a queue in which the job will be placed after the
53
+ # number of seconds has passed.
54
+ def enqueue_in_with_queue(queue, number_of_seconds_from_now,
55
+ klass, *args)
56
+ enqueue_at_with_queue(queue, Time.now + number_of_seconds_from_now,
57
+ klass, *args)
58
+ end
59
+
60
+ # Used internally to stuff the item into the schedule sorted list.
61
+ # +timestamp+ can be either in seconds or a datetime object Insertion
62
+ # if O(log(n)). Returns true if it's the first job to be scheduled at
63
+ # that time, else false
64
+ def delayed_push(timestamp, item)
65
+ # First add this item to the list for this timestamp
66
+ redis.rpush("delayed:#{timestamp.to_i}", encode(item))
67
+
68
+ # Store the timestamps at with this item occurs
69
+ redis.sadd("timestamps:#{encode(item)}", "delayed:#{timestamp.to_i}")
70
+
71
+ # Now, add this timestamp to the zsets. The score and the value are
72
+ # the same since we'll be querying by timestamp, and we don't have
73
+ # anything else to store.
74
+ redis.zadd :delayed_queue_schedule, timestamp.to_i, timestamp.to_i
75
+ end
76
+
77
+ # Returns an array of timestamps based on start and count
78
+ def delayed_queue_peek(start, count)
79
+ result = redis.zrange(:delayed_queue_schedule, start,
80
+ start + count - 1)
81
+ Array(result).map(&:to_i)
82
+ end
83
+
84
+ # Returns the size of the delayed queue schedule
85
+ def delayed_queue_schedule_size
86
+ redis.zcard :delayed_queue_schedule
87
+ end
88
+
89
+ # Returns the number of jobs for a given timestamp in the delayed queue
90
+ # schedule
91
+ def delayed_timestamp_size(timestamp)
92
+ redis.llen("delayed:#{timestamp.to_i}").to_i
93
+ end
94
+
95
+ # Returns an array of delayed items for the given timestamp
96
+ def delayed_timestamp_peek(timestamp, start, count)
97
+ if 1 == count
98
+ r = list_range "delayed:#{timestamp.to_i}", start, count
99
+ r.nil? ? [] : [r]
100
+ else
101
+ list_range "delayed:#{timestamp.to_i}", start, count
102
+ end
103
+ end
104
+
105
+ # Returns the next delayed queue timestamp
106
+ # (don't call directly)
107
+ def next_delayed_timestamp(at_time = nil)
108
+ search_first_delayed_timestamp_in_range(nil, at_time || Time.now)
109
+ end
110
+
111
+ # Returns the next item to be processed for a given timestamp, nil if
112
+ # done. (don't call directly)
113
+ # +timestamp+ can either be in seconds or a datetime
114
+ def next_item_for_timestamp(timestamp)
115
+ key = "delayed:#{timestamp.to_i}"
116
+
117
+ encoded_item = redis.lpop(key)
118
+ redis.srem("timestamps:#{encoded_item}", key)
119
+ item = decode(encoded_item)
120
+
121
+ # If the list is empty, remove it.
122
+ clean_up_timestamp(key, timestamp)
123
+ item
124
+ end
125
+
126
+ # Clears all jobs created with enqueue_at or enqueue_in
127
+ def reset_delayed_queue
128
+ Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |item|
129
+ key = "delayed:#{item}"
130
+ items = redis.lrange(key, 0, -1)
131
+ redis.pipelined do
132
+ items.each { |ts_item| redis.del("timestamps:#{ts_item}") }
133
+ end
134
+ redis.del key
135
+ end
136
+
137
+ redis.del :delayed_queue_schedule
138
+ end
139
+
140
+ # Given an encoded item, remove it from the delayed_queue
141
+ def remove_delayed(klass, *args)
142
+ search = encode(job_to_hash(klass, args))
143
+ remove_delayed_job(search)
144
+ end
145
+
146
+ # Given an encoded item, enqueue it now
147
+ def enqueue_delayed(klass, *args)
148
+ hash = job_to_hash(klass, args)
149
+ remove_delayed(klass, *args).times do
150
+ Resque::Scheduler.enqueue_from_config(hash)
151
+ end
152
+ end
153
+
154
+ # Given a block, remove jobs that return true from a block
155
+ #
156
+ # This allows for removal of delayed jobs that have arguments matching
157
+ # certain criteria
158
+ def remove_delayed_selection(klass = nil)
159
+ raise ArgumentError, 'Please supply a block' unless block_given?
160
+
161
+ found_jobs = find_delayed_selection(klass) { |args| yield(args) }
162
+ found_jobs.reduce(0) do |sum, encoded_job|
163
+ sum + remove_delayed_job(encoded_job)
164
+ end
165
+ end
166
+
167
+ # Given a block, enqueue jobs now that return true from a block
168
+ #
169
+ # This allows for enqueuing of delayed jobs that have arguments matching
170
+ # certain criteria
171
+ def enqueue_delayed_selection(klass = nil)
172
+ raise ArgumentError, 'Please supply a block' unless block_given?
173
+
174
+ found_jobs = find_delayed_selection(klass) { |args| yield(args) }
175
+ found_jobs.reduce(0) do |sum, encoded_job|
176
+ decoded_job = decode(encoded_job)
177
+ klass = Util.constantize(decoded_job['class'])
178
+ sum + enqueue_delayed(klass, *decoded_job['args'])
179
+ end
180
+ end
181
+
182
+ # Given a block, find jobs that return true from a block
183
+ #
184
+ # This allows for finding of delayed jobs that have arguments matching
185
+ # certain criteria
186
+ def find_delayed_selection(klass = nil, &block)
187
+ raise ArgumentError, 'Please supply a block' unless block_given?
188
+
189
+ timestamps = redis.zrange(:delayed_queue_schedule, 0, -1)
190
+
191
+ # Beyond 100 there's almost no improvement in speed
192
+ found = timestamps.each_slice(100).map do |ts_group|
193
+ jobs = redis.pipelined do |r|
194
+ ts_group.each do |ts|
195
+ r.lrange("delayed:#{ts}", 0, -1)
196
+ end
197
+ end
198
+
199
+ jobs.flatten.select do |payload|
200
+ payload_matches_selection?(decode(payload), klass, &block)
201
+ end
202
+ end
203
+
204
+ found.flatten
205
+ end
206
+
207
+ # Given a timestamp and job (klass + args) it removes all instances and
208
+ # returns the count of jobs removed.
209
+ #
210
+ # O(N) where N is the number of jobs scheduled to fire at the given
211
+ # timestamp
212
+ def remove_delayed_job_from_timestamp(timestamp, klass, *args)
213
+ return 0 if Resque.inline?
214
+
215
+ key = "delayed:#{timestamp.to_i}"
216
+ encoded_job = encode(job_to_hash(klass, args))
217
+
218
+ redis.srem("timestamps:#{encoded_job}", key)
219
+ count = redis.lrem(key, 0, encoded_job)
220
+ clean_up_timestamp(key, timestamp)
221
+
222
+ count
223
+ end
224
+
225
+ def count_all_scheduled_jobs
226
+ total_jobs = 0
227
+ Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |ts|
228
+ total_jobs += redis.llen("delayed:#{ts}").to_i
229
+ end
230
+ total_jobs
231
+ end
232
+
233
+ # Discover if a job has been delayed.
234
+ # Examples
235
+ # Resque.delayed?(MyJob)
236
+ # Resque.delayed?(MyJob, id: 1)
237
+ # Returns true if the job has been delayed
238
+ def delayed?(klass, *args)
239
+ !scheduled_at(klass, *args).empty?
240
+ end
241
+
242
+ # Returns delayed jobs schedule timestamp for +klass+, +args+.
243
+ def scheduled_at(klass, *args)
244
+ search = encode(job_to_hash(klass, args))
245
+ redis.smembers("timestamps:#{search}").map do |key|
246
+ key.tr('delayed:', '').to_i
247
+ end
248
+ end
249
+
250
+ def last_enqueued_at(job_name, date)
251
+ redis.hset('delayed:last_enqueued_at', job_name, date)
252
+ end
253
+
254
+ def get_last_enqueued_at(job_name)
255
+ redis.hget('delayed:last_enqueued_at', job_name)
256
+ end
257
+
258
+ private
259
+
260
+ def job_to_hash(klass, args)
261
+ { class: klass.to_s, args: args, queue: queue_from_class(klass) }
262
+ end
263
+
264
+ def job_to_hash_with_queue(queue, klass, args)
265
+ { class: klass.to_s, args: args, queue: queue }
266
+ end
267
+
268
+ def remove_delayed_job(encoded_job)
269
+ return 0 if Resque.inline?
270
+
271
+ timestamps = redis.smembers("timestamps:#{encoded_job}")
272
+
273
+ replies = redis.pipelined do
274
+ timestamps.each do |key|
275
+ redis.lrem(key, 0, encoded_job)
276
+ redis.srem("timestamps:#{encoded_job}", key)
277
+ end
278
+ end
279
+
280
+ return 0 if replies.nil? || replies.empty?
281
+ replies.each_slice(2).map(&:first).inject(:+)
282
+ end
283
+
284
+ def clean_up_timestamp(key, timestamp)
285
+ # Use a watch here to ensure nobody adds jobs to this delayed
286
+ # queue while we're removing it.
287
+ redis.watch(key) do
288
+ if redis.llen(key).to_i == 0
289
+ # If the list is empty, remove it.
290
+ redis.multi do
291
+ redis.del(key)
292
+ redis.zrem(:delayed_queue_schedule, timestamp.to_i)
293
+ end
294
+ else
295
+ redis.redis.unwatch
296
+ end
297
+ end
298
+ end
299
+
300
+ def search_first_delayed_timestamp_in_range(start_at, stop_at)
301
+ start_at = start_at.nil? ? '-inf' : start_at.to_i
302
+ stop_at = stop_at.nil? ? '+inf' : stop_at.to_i
303
+
304
+ items = redis.zrangebyscore(
305
+ :delayed_queue_schedule, start_at, stop_at,
306
+ limit: [0, 1]
307
+ )
308
+ timestamp = items.nil? ? nil : Array(items).first
309
+ timestamp.to_i unless timestamp.nil?
310
+ end
311
+
312
+ def payload_matches_selection?(decoded_payload, klass)
313
+ return false if decoded_payload.nil?
314
+ job_class = decoded_payload['class']
315
+ relevant_class = (klass.nil? || klass.to_s == job_class)
316
+ relevant_class && yield(decoded_payload['args'])
317
+ end
318
+
319
+ def plugin
320
+ Resque::Scheduler::Plugin
321
+ end
322
+ end
323
+ end
324
+ end