resque-admin-scheduler 1.0.2 → 1.3.0

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 +4 -4
  2. data/bin/migrate_to_timestamps_set.rb +16 -0
  3. data/exe/resque-scheduler +2 -2
  4. data/lib/resque/scheduler/cli.rb +6 -6
  5. data/lib/resque/scheduler/delaying_extensions.rb +14 -14
  6. data/lib/resque/scheduler/env.rb +7 -7
  7. data/lib/resque/scheduler/lock.rb +1 -1
  8. data/lib/resque/scheduler/locking.rb +7 -7
  9. data/lib/resque/scheduler/logger_builder.rb +4 -4
  10. data/lib/resque/scheduler/scheduling_extensions.rb +4 -4
  11. data/lib/resque/scheduler/server/views/delayed.erb +4 -4
  12. data/lib/resque/scheduler/server/views/search.erb +1 -1
  13. data/lib/resque/scheduler/server.rb +37 -37
  14. data/lib/resque/scheduler/signal_handling.rb +1 -1
  15. data/lib/resque/scheduler/tasks.rb +6 -6
  16. data/lib/resque/scheduler/util.rb +4 -4
  17. data/lib/resque/scheduler_admin/cli.rb +147 -0
  18. data/lib/resque/scheduler_admin/delaying_extensions.rb +324 -0
  19. data/lib/resque/scheduler_admin/env.rb +89 -0
  20. data/lib/resque/scheduler_admin/lock.rb +4 -0
  21. data/lib/resque/scheduler_admin/locking.rb +104 -0
  22. data/lib/resque/scheduler_admin/logger_builder.rb +72 -0
  23. data/lib/resque/scheduler_admin/scheduling_extensions.rb +141 -0
  24. data/lib/resque/scheduler_admin/server/views/delayed.erb +63 -0
  25. data/lib/resque/scheduler_admin/server/views/search.erb +72 -0
  26. data/lib/resque/scheduler_admin/signal_handling.rb +40 -0
  27. data/lib/resque/scheduler_admin/tasks.rb +25 -0
  28. data/lib/resque/scheduler_admin/util.rb +39 -0
  29. data/lib/resque-admin-scheduler.rb +4 -0
  30. data/lib/resque_admin/scheduler/cli.rb +147 -0
  31. data/lib/{resque → resque_admin}/scheduler/configuration.rb +1 -1
  32. data/lib/resque_admin/scheduler/delaying_extensions.rb +324 -0
  33. data/lib/resque_admin/scheduler/env.rb +89 -0
  34. data/lib/{resque → resque_admin}/scheduler/extension.rb +1 -1
  35. data/lib/{resque → resque_admin}/scheduler/failure_handler.rb +2 -2
  36. data/lib/{resque → resque_admin}/scheduler/lock/base.rb +3 -3
  37. data/lib/{resque → resque_admin}/scheduler/lock/basic.rb +4 -4
  38. data/lib/{resque → resque_admin}/scheduler/lock/resilient.rb +4 -4
  39. data/lib/resque_admin/scheduler/lock.rb +4 -0
  40. data/lib/resque_admin/scheduler/locking.rb +104 -0
  41. data/lib/resque_admin/scheduler/logger_builder.rb +72 -0
  42. data/lib/{resque → resque_admin}/scheduler/plugin.rb +1 -1
  43. data/lib/resque_admin/scheduler/scheduling_extensions.rb +141 -0
  44. data/lib/resque_admin/scheduler/server/views/delayed.erb +63 -0
  45. data/lib/{resque → resque_admin}/scheduler/server/views/delayed_schedules.erb +0 -0
  46. data/lib/{resque → resque_admin}/scheduler/server/views/delayed_timestamp.erb +2 -2
  47. data/lib/{resque → resque_admin}/scheduler/server/views/requeue-params.erb +0 -0
  48. data/lib/{resque → resque_admin}/scheduler/server/views/scheduler.erb +7 -7
  49. data/lib/resque_admin/scheduler/server/views/search.erb +72 -0
  50. data/lib/{resque → resque_admin}/scheduler/server/views/search_form.erb +0 -0
  51. data/lib/resque_admin/scheduler/server.rb +268 -0
  52. data/lib/resque_admin/scheduler/signal_handling.rb +40 -0
  53. data/lib/resque_admin/scheduler/tasks.rb +25 -0
  54. data/lib/resque_admin/scheduler/util.rb +39 -0
  55. data/lib/{resque → resque_admin}/scheduler/version.rb +2 -2
  56. data/lib/{resque → resque_admin}/scheduler.rb +44 -44
  57. data/tasks/resque_admin_scheduler.rake +2 -0
  58. metadata +47 -85
  59. data/AUTHORS.md +0 -81
  60. data/CHANGELOG.md +0 -456
  61. data/CODE_OF_CONDUCT.md +0 -74
  62. data/CONTRIBUTING.md +0 -6
  63. data/Gemfile +0 -4
  64. data/LICENSE +0 -23
  65. data/README.md +0 -691
  66. data/Rakefile +0 -21
  67. data/lib/resque-scheduler.rb +0 -4
  68. data/resque-scheduler.gemspec +0 -60
@@ -0,0 +1,324 @@
1
+ # vim:fileencoding=utf-8
2
+ require 'resque_admin'
3
+ require_relative 'plugin'
4
+ require_relative '../scheduler'
5
+
6
+ module ResqueAdmin
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 ResqueAdmin.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 ResqueAdmin.inline? || timestamp.to_i < Time.now.to_i
28
+ # Just create the job and let resque_admin 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
+ # ResqueAdmin.enqueue_at(timestamp, CustomJobClass, :opt1 => val1).
32
+ # Otherwise, pass off to ResqueAdmin.
33
+ if klass.respond_to?(:scheduled)
34
+ klass.scheduled(queue, klass.to_s, *args)
35
+ else
36
+ ResqueAdmin::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
+ ResqueAdmin::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 ResqueAdmin.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
+ # ResqueAdmin.delayed?(MyJob)
236
+ # ResqueAdmin.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 ResqueAdmin.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
+ ResqueAdmin::Scheduler::Plugin
321
+ end
322
+ end
323
+ end
324
+ end
@@ -0,0 +1,89 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ require 'English' # $PROCESS_ID
4
+
5
+ module ResqueAdmin
6
+ module Scheduler
7
+ class Env
8
+ def initialize(options)
9
+ @options = options
10
+ @pidfile_path = nil
11
+ end
12
+
13
+ def setup
14
+ require 'resque_admin'
15
+ require 'resque_admin/scheduler'
16
+
17
+ setup_backgrounding
18
+ setup_pid_file
19
+ setup_scheduler_configuration
20
+ end
21
+
22
+ def cleanup
23
+ cleanup_pid_file
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :options, :pidfile_path
29
+
30
+ def setup_backgrounding
31
+ return unless options[:background]
32
+
33
+ # Need to set this here for conditional Process.daemon redirect of
34
+ # stderr/stdout to /dev/null
35
+ ResqueAdmin::Scheduler.quiet = if options.key?(:quiet)
36
+ !!options[:quiet]
37
+ else
38
+ true
39
+ end
40
+
41
+ unless Process.respond_to?('daemon')
42
+ abort 'background option is set, which requires ruby >= 1.9'
43
+ end
44
+
45
+ Process.daemon(true, !ResqueAdmin::Scheduler.quiet)
46
+ ResqueAdmin.redis.client.reconnect
47
+ end
48
+
49
+ def setup_pid_file
50
+ return unless options[:pidfile]
51
+
52
+ @pidfile_path = File.expand_path(options[:pidfile])
53
+
54
+ File.open(pidfile_path, 'w') do |f|
55
+ f.puts $PROCESS_ID
56
+ end
57
+
58
+ at_exit { cleanup_pid_file }
59
+ end
60
+
61
+ def setup_scheduler_configuration
62
+ ResqueAdmin::Scheduler.configure do |c|
63
+ c.app_name = options[:app_name] if options.key?(:app_name)
64
+
65
+ c.dynamic = !!options[:dynamic] if options.key?(:dynamic)
66
+
67
+ c.env = options[:env] if options.key(:env)
68
+
69
+ c.logfile = options[:logfile] if options.key?(:logfile)
70
+
71
+ c.logformat = options[:logformat] if options.key?(:logformat)
72
+
73
+ if psleep = options[:poll_sleep_amount] && !psleep.nil?
74
+ c.poll_sleep_amount = Float(psleep)
75
+ end
76
+
77
+ c.verbose = !!options[:verbose] if options.key?(:verbose)
78
+ end
79
+ end
80
+
81
+ def cleanup_pid_file
82
+ return unless pidfile_path
83
+
84
+ File.delete(pidfile_path) if File.exist?(pidfile_path)
85
+ @pidfile_path = nil
86
+ end
87
+ end
88
+ end
89
+ end
@@ -3,7 +3,7 @@
3
3
  require_relative 'scheduling_extensions'
4
4
  require_relative 'delaying_extensions'
5
5
 
6
- module Resque
6
+ module ResqueAdmin
7
7
  module Scheduler
8
8
  module Extension
9
9
  include SchedulingExtensions
@@ -1,8 +1,8 @@
1
- module Resque
1
+ module ResqueAdmin
2
2
  module Scheduler
3
3
  class FailureHandler
4
4
  def self.on_enqueue_failure(_, e)
5
- Resque::Scheduler.log_error(
5
+ ResqueAdmin::Scheduler.log_error(
6
6
  "#{e.class.name}: #{e.message} #{e.backtrace.inspect}"
7
7
  )
8
8
  end
@@ -1,6 +1,6 @@
1
1
  # vim:fileencoding=utf-8
2
2
 
3
- module Resque
3
+ module ResqueAdmin
4
4
  module Scheduler
5
5
  module Lock
6
6
  class Base
@@ -30,7 +30,7 @@ module Resque
30
30
 
31
31
  # Releases the lock.
32
32
  def release!
33
- Resque.redis.del(key) == 1
33
+ ResqueAdmin.redis.del(key) == 1
34
34
  end
35
35
 
36
36
  # Releases the lock iff we own it
@@ -42,7 +42,7 @@ module Resque
42
42
 
43
43
  # Extends the lock by `timeout` seconds.
44
44
  def extend_lock!
45
- Resque.redis.expire(key, timeout)
45
+ ResqueAdmin.redis.expire(key, timeout)
46
46
  end
47
47
 
48
48
  def hostname
@@ -1,22 +1,22 @@
1
1
  # vim:fileencoding=utf-8
2
2
  require_relative 'base'
3
3
 
4
- module Resque
4
+ module ResqueAdmin
5
5
  module Scheduler
6
6
  module Lock
7
7
  class Basic < Base
8
8
  def acquire!
9
- if Resque.redis.setnx(key, value)
9
+ if ResqueAdmin.redis.setnx(key, value)
10
10
  extend_lock!
11
11
  true
12
12
  end
13
13
  end
14
14
 
15
15
  def locked?
16
- if Resque.redis.get(key) == value
16
+ if ResqueAdmin.redis.get(key) == value
17
17
  extend_lock!
18
18
 
19
- return true if Resque.redis.get(key) == value
19
+ return true if ResqueAdmin.redis.get(key) == value
20
20
  end
21
21
 
22
22
  false
@@ -1,7 +1,7 @@
1
1
  # vim:fileencoding=utf-8
2
2
  require_relative 'base'
3
3
 
4
- module Resque
4
+ module ResqueAdmin
5
5
  module Scheduler
6
6
  module Lock
7
7
  class Resilient < Base
@@ -26,7 +26,7 @@ module Resque
26
26
 
27
27
  def evalsha(script, keys, argv, refresh: false)
28
28
  sha_method_name = "#{script}_sha"
29
- Resque.redis.evalsha(
29
+ ResqueAdmin.redis.evalsha(
30
30
  send(sha_method_name, refresh),
31
31
  keys: keys,
32
32
  argv: argv
@@ -43,7 +43,7 @@ module Resque
43
43
  @locked_sha = nil if refresh
44
44
 
45
45
  @locked_sha ||=
46
- Resque.redis.script(:load, <<-EOF.gsub(/^ {14}/, ''))
46
+ ResqueAdmin.redis.script(:load, <<-EOF.gsub(/^ {14}/, ''))
47
47
  if redis.call('GET', KEYS[1]) == ARGV[1]
48
48
  then
49
49
  redis.call('EXPIRE', KEYS[1], #{timeout})
@@ -62,7 +62,7 @@ module Resque
62
62
  @acquire_sha = nil if refresh
63
63
 
64
64
  @acquire_sha ||=
65
- Resque.redis.script(:load, <<-EOF.gsub(/^ {14}/, ''))
65
+ ResqueAdmin.redis.script(:load, <<-EOF.gsub(/^ {14}/, ''))
66
66
  if redis.call('SETNX', KEYS[1], ARGV[1]) == 1
67
67
  then
68
68
  redis.call('EXPIRE', KEYS[1], #{timeout})
@@ -0,0 +1,4 @@
1
+ # vim:fileencoding=utf-8
2
+ %w(base basic resilient).each do |file|
3
+ require "resque_admin/scheduler/lock/#{file}"
4
+ end
@@ -0,0 +1,104 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ # ### Locking the scheduler process
4
+ #
5
+ # There are two places in resque_admin-scheduler that need to be synchonized in order
6
+ # to be able to run redundant scheduler processes while ensuring jobs don't get
7
+ # queued multiple times when the master process changes.
8
+ #
9
+ # 1) Processing the delayed queues (jobs that are created from
10
+ # enqueue_at/enqueue_in, etc) 2) Processing the scheduled (cron-like) jobs from
11
+ # rufus-scheduler
12
+ #
13
+ # Protecting the delayed queues (#1) is relatively easy. A simple SETNX in
14
+ # redis would suffice. However, protecting the scheduled jobs is trickier
15
+ # because the clocks on machines could be slightly off or actual firing times
16
+ # could vary slightly due to load. If scheduler A's clock is slightly ahead of
17
+ # scheduler B's clock (since they are on different machines), when scheduler A
18
+ # dies, we need to ensure that scheduler B doesn't queue jobs that A already
19
+ # queued before it's death. (This all assumes that it is better to miss a few
20
+ # scheduled jobs than it is to run them multiple times for the same iteration.)
21
+ #
22
+ # To avoid queuing multiple jobs in the case of master fail-over, the master
23
+ # should remain the master as long as it can rather than a simple SETNX which
24
+ # would result in the master roll being passed around frequently.
25
+ #
26
+ # Locking Scheme: Each resque_admin-scheduler process attempts to get the master lock
27
+ # via SETNX. Once obtained, it sets the expiration for 3 minutes
28
+ # (configurable). The master process continually updates the timeout on the
29
+ # lock key to be 3 minutes in the future in it's loop(s) (see `run`) and when
30
+ # jobs come out of rufus-scheduler (see `load_schedule_job`). That ensures
31
+ # that a minimum of 3 minutes must pass since the last queuing operation before
32
+ # a new master is chosen. If, for whatever reason, the master fails to update
33
+ # the expiration for 3 minutes, the key expires and the lock is up for grabs.
34
+ # If miraculously the original master comes back to life, it will realize it is
35
+ # no longer the master and stop processing jobs.
36
+ #
37
+ # The clocks on the scheduler machines can then be up to 3 minutes off from
38
+ # each other without the risk of queueing the same scheduled job twice during a
39
+ # master change. The catch is, in the event of a master change, no scheduled
40
+ # jobs will be queued during those 3 minutes. So, there is a trade off: the
41
+ # higher the timeout, the less likely scheduled jobs will be fired twice but
42
+ # greater chances of missing scheduled jobs. The lower the timeout, less
43
+ # likely jobs will be missed, greater the chances of jobs firing twice. If you
44
+ # don't care about jobs firing twice or are certain your machines' clocks are
45
+ # well in sync, a lower timeout is preferable. One thing to keep in mind: this
46
+ # only effects *scheduled* jobs - delayed jobs will never be lost or skipped
47
+ # since eventually a master will come online and it will process everything
48
+ # that is ready (no matter how old it is). Scheduled jobs work like cron - if
49
+ # you stop cron, no jobs fire while it's stopped and it doesn't fire jobs that
50
+ # were missed when it starts up again.
51
+
52
+ require_relative 'lock'
53
+
54
+ module ResqueAdmin
55
+ module Scheduler
56
+ module Locking
57
+ def master_lock
58
+ @master_lock ||= build_master_lock
59
+ end
60
+
61
+ def supports_lua?
62
+ redis_master_version >= 2.5
63
+ end
64
+
65
+ def master?
66
+ master_lock.acquire! || master_lock.locked?
67
+ end
68
+
69
+ def release_master_lock!
70
+ warn "#{self}\#release_master_lock! is deprecated because it does " \
71
+ "not respect lock ownership. Use #{self}\#release_master_lock " \
72
+ "instead (at #{caller.first}"
73
+
74
+ master_lock.release!
75
+ end
76
+
77
+ def release_master_lock
78
+ master_lock.release
79
+ rescue Errno::EAGAIN, Errno::ECONNRESET, Redis::CannotConnectError
80
+ @master_lock = nil
81
+ end
82
+
83
+ private
84
+
85
+ def build_master_lock
86
+ if supports_lua?
87
+ ResqueAdmin::Scheduler::Lock::Resilient.new(master_lock_key)
88
+ else
89
+ ResqueAdmin::Scheduler::Lock::Basic.new(master_lock_key)
90
+ end
91
+ end
92
+
93
+ def master_lock_key
94
+ lock_prefix = ENV['RESQUE_SCHEDULER_MASTER_LOCK_PREFIX'] || ''
95
+ lock_prefix += ':' if lock_prefix != ''
96
+ "#{ResqueAdmin.redis.namespace}:#{lock_prefix}resque_scheduler_master_lock"
97
+ end
98
+
99
+ def redis_master_version
100
+ ResqueAdmin.redis.info['redis_version'].to_f
101
+ end
102
+ end
103
+ end
104
+ end