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
data/Rakefile CHANGED
@@ -1,41 +1,18 @@
1
+ # vim:fileencoding=utf-8
1
2
  require 'bundler/gem_tasks'
3
+ require 'rake/testtask'
4
+ require 'yard'
2
5
 
3
- $LOAD_PATH.unshift 'lib'
6
+ task default: :test
4
7
 
5
- task :default => :test
6
-
7
- desc 'Run tests'
8
- task :test do
9
- if RUBY_VERSION =~ /^1\.8/
10
- unless ENV['SEED']
11
- srand
12
- ENV['SEED'] = (srand % 0xFFFF).to_s
13
- end
14
-
15
- $stdout.puts "Running with SEED=#{ENV['SEED']}"
16
- srand Integer(ENV['SEED'])
17
- elsif ENV['SEED']
18
- ARGV += %W(--seed #{ENV['SEED']})
19
- end
20
- Dir['test/*_test.rb'].each do |f|
21
- require File.expand_path(f)
8
+ Rake::TestTask.new do |t|
9
+ t.libs << 'test'
10
+ t.pattern = ENV['PATTERN'] || 'test/*_test.rb'
11
+ t.options = ''.tap do |o|
12
+ o << "--seed #{ENV['SEED']} " if ENV['SEED']
13
+ o << '--verbose ' if ENV['VERBOSE']
22
14
  end
15
+ t.warning = false
23
16
  end
24
17
 
25
- desc 'Run rubocop'
26
- task :rubocop do
27
- unless RUBY_VERSION < '1.9'
28
- sh('rubocop --config .rubocop.yml --format simple') { |r, _| r || abort }
29
- end
30
- end
31
-
32
- begin
33
- require 'rdoc/task'
34
-
35
- Rake::RDocTask.new do |rd|
36
- rd.main = 'README.md'
37
- rd.rdoc_files.include('README.md', 'lib/**/*.rb')
38
- rd.rdoc_dir = 'doc'
39
- end
40
- rescue LoadError
41
- end
18
+ YARD::Rake::YardocTask.new
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # vim:fileencoding=utf-8
3
+
4
+ require 'resque-scheduler'
5
+ Resque::Scheduler::Cli.run!
@@ -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,102 @@
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
+ attr_writer :environment
12
+ def environment
13
+ @environment ||= ENV
14
+ end
15
+
16
+ # Used in `#load_schedule_job`
17
+ attr_writer :env
18
+
19
+ def env
20
+ return @env if @env
21
+ @env ||= Rails.env if defined?(Rails) && Rails.respond_to?(:env)
22
+ @env ||= environment['RAILS_ENV']
23
+ @env
24
+ end
25
+
26
+ # If true, logs more stuff...
27
+ attr_writer :verbose
28
+
29
+ def verbose
30
+ @verbose ||= to_bool(environment['VERBOSE'])
31
+ end
32
+
33
+ # If set, produces no output
34
+ attr_writer :quiet
35
+
36
+ def quiet
37
+ @quiet ||= to_bool(environment['QUIET'])
38
+ end
39
+
40
+ # If set, will write messages to the file
41
+ attr_writer :logfile
42
+
43
+ def logfile
44
+ @logfile ||= environment['LOGFILE']
45
+ end
46
+
47
+ # Sets whether to log in 'text', 'json' or 'logfmt'
48
+ attr_writer :logformat
49
+
50
+ def logformat
51
+ @logformat ||= environment['LOGFORMAT']
52
+ end
53
+
54
+ # If set, will try to update the schedule in the loop
55
+ attr_writer :dynamic
56
+
57
+ def dynamic
58
+ @dynamic ||= to_bool(environment['DYNAMIC_SCHEDULE'])
59
+ end
60
+
61
+ # If set, will append the app name to procline
62
+ attr_writer :app_name
63
+
64
+ def app_name
65
+ @app_name ||= environment['APP_NAME']
66
+ end
67
+
68
+ def delayed_requeue_batch_size
69
+ @delayed_requeue_batch_size ||= \
70
+ ENV['DELAYED_REQUEUE_BATCH_SIZE'].to_i if environment['DELAYED_REQUEUE_BATCH_SIZE']
71
+ @delayed_requeue_batch_size ||= 100
72
+ end
73
+
74
+ # Amount of time in seconds to sleep between polls of the delayed
75
+ # queue. Defaults to 5
76
+ attr_writer :poll_sleep_amount
77
+
78
+ def poll_sleep_amount
79
+ @poll_sleep_amount ||=
80
+ Float(environment.fetch('RESQUE_SCHEDULER_INTERVAL', '5'))
81
+ end
82
+
83
+ private
84
+
85
+ # Copied from https://github.com/rails/rails/blob/main/activemodel/lib/active_model/type/boolean.rb#L17
86
+ TRUE_VALUES = [
87
+ true, 1,
88
+ '1', :'1',
89
+ 't', :t,
90
+ 'T', :T,
91
+ 'true', :true,
92
+ 'TRUE', :TRUE,
93
+ 'on', :on,
94
+ 'ON', :ON
95
+ ].to_set.freeze
96
+
97
+ def to_bool(value)
98
+ TRUE_VALUES.include?(value)
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,371 @@
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.enqueue_to(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
+ unless number_of_seconds_from_now.is_a?(Numeric)
49
+ raise ArgumentError, 'Please supply a numeric number of seconds'
50
+ end
51
+ enqueue_at(Time.now + number_of_seconds_from_now, klass, *args)
52
+ end
53
+
54
+ # Identical to +enqueue_in+, except you can also specify
55
+ # a queue in which the job will be placed after the
56
+ # number of seconds has passed.
57
+ def enqueue_in_with_queue(queue, number_of_seconds_from_now,
58
+ klass, *args)
59
+ unless number_of_seconds_from_now.is_a?(Numeric)
60
+ raise ArgumentError, 'Please supply a numeric number of seconds'
61
+ end
62
+ enqueue_at_with_queue(queue, Time.now + number_of_seconds_from_now,
63
+ klass, *args)
64
+ end
65
+
66
+ # Update the delayed timestamp of any matching delayed jobs or enqueue a
67
+ # new job if no matching jobs are found. Returns the number of delayed or
68
+ # enqueued jobs.
69
+ def delay_or_enqueue_at(timestamp, klass, *args)
70
+ count = remove_delayed(klass, *args)
71
+ count = 1 if count == 0
72
+
73
+ count.times do
74
+ enqueue_at(timestamp, klass, *args)
75
+ end
76
+ end
77
+
78
+ # Identical to +delay_or_enqueue_at+, except it takes
79
+ # number_of_seconds_from_now instead of a timestamp
80
+ def delay_or_enqueue_in(number_of_seconds_from_now, klass, *args)
81
+ delay_or_enqueue_at(Time.now + number_of_seconds_from_now, klass, *args)
82
+ end
83
+
84
+ # Used internally to stuff the item into the schedule sorted list.
85
+ # +timestamp+ can be either in seconds or a datetime object. The
86
+ # insertion time complexity is O(log(n)). Returns true if it's
87
+ # the first job to be scheduled at that time, else false.
88
+ def delayed_push(timestamp, item)
89
+ # First add this item to the list for this timestamp
90
+ redis.rpush("delayed:#{timestamp.to_i}", encode(item))
91
+
92
+ # Store the timestamps at with this item occurs
93
+ redis.sadd("timestamps:#{encode(item)}", ["delayed:#{timestamp.to_i}"])
94
+
95
+ # Now, add this timestamp to the zsets. The score and the value are
96
+ # the same since we'll be querying by timestamp, and we don't have
97
+ # anything else to store.
98
+ redis.zadd :delayed_queue_schedule, timestamp.to_i, timestamp.to_i
99
+ end
100
+
101
+ # Returns an array of timestamps based on start and count
102
+ def delayed_queue_peek(start, count)
103
+ result = redis.zrange(:delayed_queue_schedule, start,
104
+ start + count - 1)
105
+ Array(result).map(&:to_i)
106
+ end
107
+
108
+ # Returns the size of the delayed queue schedule
109
+ # this does not represent the number of items in the queue to be scheduled
110
+ def delayed_queue_schedule_size
111
+ redis.zcard :delayed_queue_schedule
112
+ end
113
+
114
+ # Returns the number of jobs for a given timestamp in the delayed queue
115
+ # schedule
116
+ def delayed_timestamp_size(timestamp)
117
+ redis.llen("delayed:#{timestamp.to_i}").to_i
118
+ end
119
+
120
+ # Returns an array of delayed items for the given timestamp
121
+ def delayed_timestamp_peek(timestamp, start, count)
122
+ if 1 == count
123
+ r = list_range "delayed:#{timestamp.to_i}", start, count
124
+ r.nil? ? [] : [r]
125
+ else
126
+ list_range "delayed:#{timestamp.to_i}", start, count
127
+ end
128
+ end
129
+
130
+ # Returns the next delayed queue timestamp
131
+ # (don't call directly)
132
+ def next_delayed_timestamp(at_time = nil)
133
+ search_first_delayed_timestamp_in_range(nil, at_time || Time.now)
134
+ end
135
+
136
+ # Returns the next item to be processed for a given timestamp, nil if
137
+ # done. (don't call directly)
138
+ # +timestamp+ can either be in seconds or a datetime
139
+ def next_item_for_timestamp(timestamp)
140
+ key = "delayed:#{timestamp.to_i}"
141
+
142
+ encoded_item = redis.lpop(key)
143
+ redis.srem("timestamps:#{encoded_item}", [key])
144
+ item = decode(encoded_item)
145
+
146
+ # If the list is empty, remove it.
147
+ clean_up_timestamp(key, timestamp)
148
+ item
149
+ end
150
+
151
+ # Clears all jobs created with enqueue_at or enqueue_in
152
+ def reset_delayed_queue
153
+ Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |item|
154
+ key = "delayed:#{item}"
155
+ items = redis.lrange(key, 0, -1)
156
+ redis.del(key, items.map { |ts_item| "timestamps:#{ts_item}" })
157
+ end
158
+
159
+ redis.del :delayed_queue_schedule
160
+ end
161
+
162
+ # Given an encoded item, remove it from the delayed_queue
163
+ def remove_delayed(klass, *args)
164
+ search = encode(job_to_hash(klass, args))
165
+ remove_delayed_job(search)
166
+ end
167
+
168
+ def remove_delayed_in_queue(klass, queue, *args)
169
+ search = encode(job_to_hash_with_queue(queue, klass, args))
170
+ remove_delayed_job(search)
171
+ end
172
+
173
+ # Given an encoded item, enqueue it now
174
+ def enqueue_delayed(klass, *args)
175
+ hash = job_to_hash(klass, args)
176
+ remove_delayed(klass, *args).times do
177
+ Resque::Scheduler.enqueue_from_config(hash)
178
+ end
179
+ end
180
+
181
+ def enqueue_delayed_with_queue(klass, queue, *args)
182
+ hash = job_to_hash_with_queue(queue, klass, args)
183
+ remove_delayed_in_queue(klass, queue, *args).times do
184
+ Resque::Scheduler.enqueue_from_config(hash)
185
+ end
186
+ end
187
+
188
+ # Given a block, remove jobs that return true from a block
189
+ #
190
+ # This allows for removal of delayed jobs that have arguments matching
191
+ # certain criteria
192
+ def remove_delayed_selection(klass = nil)
193
+ raise ArgumentError, 'Please supply a block' unless block_given?
194
+
195
+ found_jobs = find_delayed_selection(klass) { |args| yield(args) }
196
+ found_jobs.reduce(0) do |sum, encoded_job|
197
+ sum + remove_delayed_job(encoded_job)
198
+ end
199
+ end
200
+
201
+ # Given a block, enqueue jobs now that return true from a block
202
+ #
203
+ # This allows for enqueuing of delayed jobs that have arguments matching
204
+ # certain criteria
205
+ def enqueue_delayed_selection(klass = nil)
206
+ raise ArgumentError, 'Please supply a block' unless block_given?
207
+
208
+ found_jobs = find_delayed_selection(klass) { |args| yield(args) }
209
+ found_jobs.reduce(0) do |sum, encoded_job|
210
+ decoded_job = decode(encoded_job)
211
+ klass = Util.constantize(decoded_job['class'])
212
+ queue = decoded_job['queue']
213
+
214
+ if queue
215
+ jobs_queued = enqueue_delayed_with_queue(klass, queue, *decoded_job['args'])
216
+ else
217
+ jobs_queued = enqueue_delayed(klass, *decoded_job['args'])
218
+ end
219
+
220
+ jobs_queued + sum
221
+ end
222
+ end
223
+
224
+ # Given a block, find jobs that return true from a block
225
+ #
226
+ # This allows for finding of delayed jobs that have arguments matching
227
+ # certain criteria
228
+ def find_delayed_selection(klass = nil, &block)
229
+ raise ArgumentError, 'Please supply a block' unless block_given?
230
+
231
+ timestamps = redis.zrange(:delayed_queue_schedule, 0, -1)
232
+
233
+ # Beyond 100 there's almost no improvement in speed
234
+ found = timestamps.each_slice(100).map do |ts_group|
235
+ jobs = redis.pipelined do |pipeline|
236
+ ts_group.each do |ts|
237
+ pipeline.lrange("delayed:#{ts}", 0, -1)
238
+ end
239
+ end
240
+
241
+ jobs.flatten.select do |payload|
242
+ payload_matches_selection?(decode(payload), klass, &block)
243
+ end
244
+ end
245
+
246
+ found.flatten
247
+ end
248
+
249
+ # Given a timestamp and job (klass + args) it removes all instances and
250
+ # returns the count of jobs removed.
251
+ #
252
+ # O(N) where N is the number of jobs scheduled to fire at the given
253
+ # timestamp
254
+ def remove_delayed_job_from_timestamp(timestamp, klass, *args)
255
+ return 0 if Resque.inline?
256
+
257
+ key = "delayed:#{timestamp.to_i}"
258
+ encoded_job = encode(job_to_hash(klass, args))
259
+
260
+ redis.srem("timestamps:#{encoded_job}", [key])
261
+ count = redis.lrem(key, 0, encoded_job)
262
+ clean_up_timestamp(key, timestamp)
263
+
264
+ count
265
+ end
266
+
267
+ def count_all_scheduled_jobs
268
+ total_jobs = 0
269
+ Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |ts|
270
+ total_jobs += redis.llen("delayed:#{ts}").to_i
271
+ end
272
+ total_jobs
273
+ end
274
+
275
+ # Discover if a job has been delayed.
276
+ # Examples
277
+ # Resque.delayed?(MyJob)
278
+ # Resque.delayed?(MyJob, id: 1)
279
+ # Returns true if the job has been delayed
280
+ def delayed?(klass, *args)
281
+ !scheduled_at(klass, *args).empty?
282
+ end
283
+
284
+ # Returns delayed jobs schedule timestamp for +klass+, +args+.
285
+ def scheduled_at(klass, *args)
286
+ search = encode(job_to_hash(klass, args))
287
+ redis.smembers("timestamps:#{search}").map do |key|
288
+ key.tr('delayed:', '').to_i
289
+ end
290
+ end
291
+
292
+ def last_enqueued_at(job_name, date)
293
+ redis.hset('delayed:last_enqueued_at', job_name, date)
294
+ end
295
+
296
+ def get_last_enqueued_at(job_name)
297
+ redis.hget('delayed:last_enqueued_at', job_name)
298
+ end
299
+
300
+ def clean_up_timestamp(key, timestamp)
301
+ # Use a watch here to ensure nobody adds jobs to this delayed
302
+ # queue while we're removing it.
303
+ redis.watch(key) do
304
+ if redis.llen(key).to_i == 0
305
+ # If the list is empty, remove it.
306
+ redis.multi do |transaction|
307
+ transaction.del(key)
308
+ transaction.zrem(:delayed_queue_schedule, timestamp.to_i)
309
+ end
310
+ else
311
+ redis.redis.unwatch
312
+ end
313
+ end
314
+ end
315
+
316
+ private
317
+
318
+ def job_to_hash(klass, args)
319
+ { class: klass.to_s, args: args, queue: queue_from_class(klass) }
320
+ end
321
+
322
+ def job_to_hash_with_queue(queue, klass, args)
323
+ { class: klass.to_s, args: args, queue: queue }
324
+ end
325
+
326
+ # Removes a job from the queue, but not modify the timestamp schedule. This method
327
+ # will not effect the output of `delayed_queue_schedule_size`
328
+ def remove_delayed_job(encoded_job)
329
+ return 0 if Resque.inline?
330
+
331
+ timestamps = redis.smembers("timestamps:#{encoded_job}")
332
+
333
+ replies = redis.pipelined do |pipeline|
334
+ timestamps.each do |key|
335
+ pipeline.lrem(key, 0, encoded_job)
336
+ pipeline.srem("timestamps:#{encoded_job}", [key])
337
+ end
338
+ end
339
+
340
+ # timestamp key is not removed from the schedule, this is done later
341
+ # by the scheduler loop
342
+
343
+ return 0 if replies.nil? || replies.empty?
344
+ replies.each_slice(2).map(&:first).inject(:+)
345
+ end
346
+
347
+ def search_first_delayed_timestamp_in_range(start_at, stop_at)
348
+ start_at = start_at.nil? ? '-inf' : start_at.to_i
349
+ stop_at = stop_at.nil? ? '+inf' : stop_at.to_i
350
+
351
+ items = redis.zrangebyscore(
352
+ :delayed_queue_schedule, start_at, stop_at,
353
+ limit: [0, 1]
354
+ )
355
+ timestamp = items.nil? ? nil : Array(items).first
356
+ timestamp.to_i unless timestamp.nil?
357
+ end
358
+
359
+ def payload_matches_selection?(decoded_payload, klass)
360
+ return false if decoded_payload.nil?
361
+ job_class = decoded_payload['class']
362
+ relevant_class = (klass.nil? || klass.to_s == job_class)
363
+ relevant_class && yield(decoded_payload['args'])
364
+ end
365
+
366
+ def plugin
367
+ Resque::Scheduler::Plugin
368
+ end
369
+ end
370
+ end
371
+ end