resque-scheduler 2.2.0 → 4.10.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 (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