resque-admin-scheduler 1.0.2 → 1.3.0
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.
- checksums.yaml +4 -4
- data/bin/migrate_to_timestamps_set.rb +16 -0
- data/exe/resque-scheduler +2 -2
- data/lib/resque/scheduler/cli.rb +6 -6
- data/lib/resque/scheduler/delaying_extensions.rb +14 -14
- data/lib/resque/scheduler/env.rb +7 -7
- data/lib/resque/scheduler/lock.rb +1 -1
- data/lib/resque/scheduler/locking.rb +7 -7
- data/lib/resque/scheduler/logger_builder.rb +4 -4
- data/lib/resque/scheduler/scheduling_extensions.rb +4 -4
- data/lib/resque/scheduler/server/views/delayed.erb +4 -4
- data/lib/resque/scheduler/server/views/search.erb +1 -1
- data/lib/resque/scheduler/server.rb +37 -37
- data/lib/resque/scheduler/signal_handling.rb +1 -1
- data/lib/resque/scheduler/tasks.rb +6 -6
- data/lib/resque/scheduler/util.rb +4 -4
- data/lib/resque/scheduler_admin/cli.rb +147 -0
- data/lib/resque/scheduler_admin/delaying_extensions.rb +324 -0
- data/lib/resque/scheduler_admin/env.rb +89 -0
- data/lib/resque/scheduler_admin/lock.rb +4 -0
- data/lib/resque/scheduler_admin/locking.rb +104 -0
- data/lib/resque/scheduler_admin/logger_builder.rb +72 -0
- data/lib/resque/scheduler_admin/scheduling_extensions.rb +141 -0
- data/lib/resque/scheduler_admin/server/views/delayed.erb +63 -0
- data/lib/resque/scheduler_admin/server/views/search.erb +72 -0
- data/lib/resque/scheduler_admin/signal_handling.rb +40 -0
- data/lib/resque/scheduler_admin/tasks.rb +25 -0
- data/lib/resque/scheduler_admin/util.rb +39 -0
- data/lib/resque-admin-scheduler.rb +4 -0
- data/lib/resque_admin/scheduler/cli.rb +147 -0
- data/lib/{resque → resque_admin}/scheduler/configuration.rb +1 -1
- data/lib/resque_admin/scheduler/delaying_extensions.rb +324 -0
- data/lib/resque_admin/scheduler/env.rb +89 -0
- data/lib/{resque → resque_admin}/scheduler/extension.rb +1 -1
- data/lib/{resque → resque_admin}/scheduler/failure_handler.rb +2 -2
- data/lib/{resque → resque_admin}/scheduler/lock/base.rb +3 -3
- data/lib/{resque → resque_admin}/scheduler/lock/basic.rb +4 -4
- data/lib/{resque → resque_admin}/scheduler/lock/resilient.rb +4 -4
- data/lib/resque_admin/scheduler/lock.rb +4 -0
- data/lib/resque_admin/scheduler/locking.rb +104 -0
- data/lib/resque_admin/scheduler/logger_builder.rb +72 -0
- data/lib/{resque → resque_admin}/scheduler/plugin.rb +1 -1
- data/lib/resque_admin/scheduler/scheduling_extensions.rb +141 -0
- data/lib/resque_admin/scheduler/server/views/delayed.erb +63 -0
- data/lib/{resque → resque_admin}/scheduler/server/views/delayed_schedules.erb +0 -0
- data/lib/{resque → resque_admin}/scheduler/server/views/delayed_timestamp.erb +2 -2
- data/lib/{resque → resque_admin}/scheduler/server/views/requeue-params.erb +0 -0
- data/lib/{resque → resque_admin}/scheduler/server/views/scheduler.erb +7 -7
- data/lib/resque_admin/scheduler/server/views/search.erb +72 -0
- data/lib/{resque → resque_admin}/scheduler/server/views/search_form.erb +0 -0
- data/lib/resque_admin/scheduler/server.rb +268 -0
- data/lib/resque_admin/scheduler/signal_handling.rb +40 -0
- data/lib/resque_admin/scheduler/tasks.rb +25 -0
- data/lib/resque_admin/scheduler/util.rb +39 -0
- data/lib/{resque → resque_admin}/scheduler/version.rb +2 -2
- data/lib/{resque → resque_admin}/scheduler.rb +44 -44
- data/tasks/resque_admin_scheduler.rake +2 -0
- metadata +47 -85
- data/AUTHORS.md +0 -81
- data/CHANGELOG.md +0 -456
- data/CODE_OF_CONDUCT.md +0 -74
- data/CONTRIBUTING.md +0 -6
- data/Gemfile +0 -4
- data/LICENSE +0 -23
- data/README.md +0 -691
- data/Rakefile +0 -21
- data/lib/resque-scheduler.rb +0 -4
- 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
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# vim:fileencoding=utf-8
|
2
2
|
|
3
|
-
module
|
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
|
-
|
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
|
-
|
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
|
4
|
+
module ResqueAdmin
|
5
5
|
module Scheduler
|
6
6
|
module Lock
|
7
7
|
class Basic < Base
|
8
8
|
def acquire!
|
9
|
-
if
|
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
|
16
|
+
if ResqueAdmin.redis.get(key) == value
|
17
17
|
extend_lock!
|
18
18
|
|
19
|
-
return true if
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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,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
|