rescheduler 0.2.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.
- data/lib/rescheduler.rb +302 -0
- metadata +83 -0
data/lib/rescheduler.rb
ADDED
@@ -0,0 +1,302 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'redis'
|
3
|
+
|
4
|
+
module Rescheduler
|
5
|
+
extend self
|
6
|
+
|
7
|
+
# Setup configuration
|
8
|
+
attr_accessor :config
|
9
|
+
self.config = {}
|
10
|
+
#==========================
|
11
|
+
# Management routines
|
12
|
+
def prefix
|
13
|
+
return @config[:prefix] || ''
|
14
|
+
end
|
15
|
+
|
16
|
+
def reinitialize # Very slow reinitialize
|
17
|
+
keys = %w{TMCOUNTER TMMAINT TMDEFERRED}
|
18
|
+
%w{TMTUBE:* TMARGS:* TMRUNNING:*}.each do |p|
|
19
|
+
keys += redis.keys(prefix + p)
|
20
|
+
end
|
21
|
+
redis.del(keys)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Return a hash of statistics
|
25
|
+
def stats
|
26
|
+
qnids = redis.keys(rk_args('*'))
|
27
|
+
stats = {}
|
28
|
+
qnids.each do |k|
|
29
|
+
qnid = k.split('TMARGS:')[1]
|
30
|
+
queue = qnid_to_queue(qnid)
|
31
|
+
stats[queue] ||= 0
|
32
|
+
stats[queue] += 1
|
33
|
+
end
|
34
|
+
return {:jobs=>stats}
|
35
|
+
end
|
36
|
+
|
37
|
+
#==========================
|
38
|
+
# Task producer routines
|
39
|
+
# Add an immediate task to the queue
|
40
|
+
def enqueue(options=nil)
|
41
|
+
options ||= {}
|
42
|
+
now = Time.now
|
43
|
+
|
44
|
+
# Error check
|
45
|
+
validate_queue_name(options[:queue]) if options.include?(:queue)
|
46
|
+
|
47
|
+
# Convert due_in to due_at
|
48
|
+
if options.include?(:due_in)
|
49
|
+
raise ArgumentError, ':due_it and :due_at can not be both specified' if options.include?(:due_at)
|
50
|
+
options[:due_at] = now + options[:due_in]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Get an ID if not already have one
|
54
|
+
user_id = options.include?(:id)
|
55
|
+
unless user_id
|
56
|
+
options[:id] = redis.incr(rk_counter)
|
57
|
+
end
|
58
|
+
|
59
|
+
ts = options[:due_at].to_i || 0
|
60
|
+
qnid = get_qnid(options)
|
61
|
+
|
62
|
+
# Encode and save args
|
63
|
+
redis.multi do # Transaction to enqueue the job and save args together
|
64
|
+
if user_id # Delete possible existing job if user set id
|
65
|
+
redis.zrem(rk_deferred, qnid)
|
66
|
+
redis.lrem(rk_queue(options[:queue]), 0, qnid)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Save options
|
70
|
+
redis.set(rk_args(qnid), options.to_json)
|
71
|
+
|
72
|
+
# Determine the due time
|
73
|
+
if ts > now.to_i # Future job
|
74
|
+
redis.zadd(rk_deferred, ts, qnid)
|
75
|
+
else
|
76
|
+
redis.lpush(rk_queue(options[:queue]), qnid)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Now decide if we need to wait up the workers (outside of the transaction)
|
81
|
+
if (ts > now.to_i)
|
82
|
+
dt = redis.zrange(rk_deferred, 0, 0)[0]
|
83
|
+
# Wake up workers if our job is the first one in deferred queue, so they can reset timeout
|
84
|
+
if dt && dt == qnid
|
85
|
+
redis.lpush(rk_maintenace, '-') # Kick start the maintenance token ring
|
86
|
+
end
|
87
|
+
end
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
|
91
|
+
def exists?(options)
|
92
|
+
raise ArgumentError, 'Can not test existence without :id' unless options.include?(:id)
|
93
|
+
qnid = get_qnid(options)
|
94
|
+
return redis.exists(rk_args(qnid))
|
95
|
+
end
|
96
|
+
|
97
|
+
def enqueue_unless_exists(options)
|
98
|
+
enqueue(options) unless exists?(options)
|
99
|
+
end
|
100
|
+
|
101
|
+
def delete(options)
|
102
|
+
qnid = get_qnid(options)
|
103
|
+
|
104
|
+
redis.multi do
|
105
|
+
redis.del(rk_args(qnid))
|
106
|
+
redis.zrem(rk_deferred, qnid)
|
107
|
+
redis.lrem(rk_queue(options[:queue]), 0, qnid)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
#=================
|
112
|
+
# Job definition
|
113
|
+
# Task consumer routines
|
114
|
+
def job(tube, &block)
|
115
|
+
@runners ||= {}
|
116
|
+
@runners[tube] = block
|
117
|
+
return nil
|
118
|
+
end
|
119
|
+
|
120
|
+
#=================
|
121
|
+
# Runner/Maintenance routines
|
122
|
+
def start(*tubes)
|
123
|
+
# Check arguments
|
124
|
+
if !@runners || @runners.size == 0
|
125
|
+
raise Exception, 'Can not start worker without defining job handlers.'
|
126
|
+
end
|
127
|
+
|
128
|
+
tubes.each do |t|
|
129
|
+
next if @runners.include?(t)
|
130
|
+
raise Exception, "Handler for queue #{t} is undefined."
|
131
|
+
end
|
132
|
+
|
133
|
+
tubes = @runners.keys if !tubes || tubes.size == 0
|
134
|
+
|
135
|
+
log_debug "[[ Starting: #{tubes.join(',')} ]]"
|
136
|
+
|
137
|
+
# Generate a random clientkey for maintenance token ring maintenance.
|
138
|
+
client_key = [[rand(0xFFFFFFFF)].pack('L')].pack('m0')[0...6]
|
139
|
+
|
140
|
+
keys = tubes.map {|t| rk_queue(t)}
|
141
|
+
keys << rk_maintenace
|
142
|
+
|
143
|
+
dopush = nil
|
144
|
+
|
145
|
+
loop do
|
146
|
+
# Run maintenance and determine timeout
|
147
|
+
next_job_time = determine_next_deferred_job_time.to_i
|
148
|
+
|
149
|
+
if dopush # Only pass-on the token after we are done with maintenance. Avoid contention
|
150
|
+
redis.lpush(rk_maintenace, dopush)
|
151
|
+
dopush = nil
|
152
|
+
end
|
153
|
+
|
154
|
+
# Blocking wait
|
155
|
+
timeout = next_job_time - Time.now.to_i
|
156
|
+
timeout = 1 if timeout < 1
|
157
|
+
result = redis.brpop(keys, :timeout=>timeout)
|
158
|
+
|
159
|
+
# Handle task
|
160
|
+
if result # Got a task
|
161
|
+
tube = result[0]
|
162
|
+
qnid = result[1]
|
163
|
+
if tube == rk_maintenace
|
164
|
+
# Circulate maintenance task until it comes a full circle. This depends on redis
|
165
|
+
# first come first serve policy in brpop.
|
166
|
+
dopush = qnid + client_key unless qnid.include?(client_key) # Push if we have not pushed yet.
|
167
|
+
else
|
168
|
+
run_job(qnid)
|
169
|
+
end
|
170
|
+
else
|
171
|
+
# Do nothing when got timeout, the run_maintenance will take care of deferred jobs
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
# Runner routines
|
179
|
+
def run_job(qnid)
|
180
|
+
# First load job parameters for running
|
181
|
+
rk = rk_args(qnid)
|
182
|
+
rkr = rk_running(qnid)
|
183
|
+
optstr = nil
|
184
|
+
begin
|
185
|
+
res = nil
|
186
|
+
redis.watch(rk) do # Transaction to ensure read/delete is atomic
|
187
|
+
optstr = redis.get(rk)
|
188
|
+
if optstr.nil?
|
189
|
+
redis.unwatch
|
190
|
+
log_debug("Job is deleted mysteriously")
|
191
|
+
return # Job is deleted somewhere
|
192
|
+
end
|
193
|
+
res = redis.multi do
|
194
|
+
redis.del(rk)
|
195
|
+
redis.set(rkr, optstr)
|
196
|
+
end
|
197
|
+
if !res
|
198
|
+
# Contention, try read again
|
199
|
+
log_debug("Job read contention")
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end until res
|
203
|
+
|
204
|
+
# Parse and run
|
205
|
+
opt = JSON.parse(optstr)
|
206
|
+
#opt.symbolize_keys # Be mindful of non-rails people, explicitly here
|
207
|
+
sopt = {}
|
208
|
+
opt.each { |key, val| sopt[key.to_sym] = val }
|
209
|
+
|
210
|
+
# 2. Find runner and invoke it
|
211
|
+
begin
|
212
|
+
log_debug(">>---- Starting #{qnid}")
|
213
|
+
runner = @runners[qnid_to_queue(qnid)]
|
214
|
+
runner.call(sopt)
|
215
|
+
log_debug("----<< Finished #{qnid}")
|
216
|
+
rescue
|
217
|
+
log_debug("----<< Failed #{qnid}: #{$!}")
|
218
|
+
end
|
219
|
+
|
220
|
+
# 3. Remove job from running list (Done)
|
221
|
+
redis.del(rkr)
|
222
|
+
end
|
223
|
+
|
224
|
+
# Helper routines
|
225
|
+
|
226
|
+
# Find all the "due" deferred jobs and move them into respective queues
|
227
|
+
def service_deferred_jobs
|
228
|
+
dtn = rk_deferred
|
229
|
+
ntry = 0
|
230
|
+
loop do
|
231
|
+
curtime = Time.now.to_i
|
232
|
+
redis.watch(dtn) do
|
233
|
+
tasks = redis.zrangebyscore(dtn, 0, curtime)
|
234
|
+
if tasks.empty?
|
235
|
+
redis.unwatch
|
236
|
+
return # Nothing to transfer, moving on.
|
237
|
+
end
|
238
|
+
|
239
|
+
redis.multi
|
240
|
+
redis.zremrangebyscore(dtn, 0, curtime)
|
241
|
+
tasks.each do |qnid|
|
242
|
+
q = qnid_to_queue(qnid)
|
243
|
+
redis.lpush(rk_queue(q), qnid)
|
244
|
+
end
|
245
|
+
if !redis.exec
|
246
|
+
# Contention happens, retrying
|
247
|
+
# Sleep a random amount of time after first try
|
248
|
+
log_debug("service_deferred_jobs contention")
|
249
|
+
Kernel.sleep (rand(ntry * 1000) / 1000.0) if ntry > 0
|
250
|
+
else
|
251
|
+
return # Done transfering
|
252
|
+
end
|
253
|
+
end
|
254
|
+
ntry += 1
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def determine_next_deferred_job_time(skip_service = nil)
|
259
|
+
tsnow = Time.now.to_f
|
260
|
+
maxtime = tsnow + 3600
|
261
|
+
|
262
|
+
dt = redis.zrange(rk_deferred, 0, 0, :with_scores=>true)[0]
|
263
|
+
nt = (dt && dt[1] && dt[1] < maxtime) ? dt[1] : maxtime
|
264
|
+
if !skip_service && nt <= tsnow
|
265
|
+
service_deferred_jobs
|
266
|
+
# Get the deferred jobs again.
|
267
|
+
dt = redis.zrange(rk_deferred, 0, 0, :with_scores=>true)[0]
|
268
|
+
nt = (dt && dt[1] && dt[1] < maxtime) ? dt[1] : maxtime
|
269
|
+
end
|
270
|
+
return nt
|
271
|
+
end
|
272
|
+
|
273
|
+
def rk_queue(tube)
|
274
|
+
return "#{prefix}TMTUBE:#{tube}"
|
275
|
+
end
|
276
|
+
|
277
|
+
def rk_deferred; prefix + 'TMDEFERRED'; end
|
278
|
+
def rk_maintenace; prefix + 'TMMAINT'; end
|
279
|
+
def rk_args(qnid); "#{prefix}TMARGS:#{qnid}"; end
|
280
|
+
def rk_running(qnid); "#{prefix}TMRUNNING:#{qnid}"; end
|
281
|
+
def rk_counter; prefix + 'TMCOUNTER'; end
|
282
|
+
|
283
|
+
def get_qnid(options)
|
284
|
+
return "#{options[:queue]}:#{options[:id]}"
|
285
|
+
end
|
286
|
+
|
287
|
+
def qnid_to_queue(qnid); qnid[0...qnid.index(':')]; end
|
288
|
+
|
289
|
+
def redis
|
290
|
+
@redis ||= Redis.new(@config[:redis_connection] || {})
|
291
|
+
end
|
292
|
+
|
293
|
+
def validate_queue_name(queue)
|
294
|
+
raise ArgumentError, 'Queue name can not contain special characters' if queue.include?(':')
|
295
|
+
end
|
296
|
+
|
297
|
+
# Logging facility
|
298
|
+
def log_debug(msg)
|
299
|
+
print("#{Time.now.to_i} #{msg}\n")
|
300
|
+
end
|
301
|
+
|
302
|
+
end
|
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rescheduler
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Dongyi Liao
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-10-01 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: redis
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: json
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: ! " Rescheduler is a library that uses Redis to maintain a task queue
|
47
|
+
of immediate and delayed jobs without any polling.\n\n Goals:\n 1. Light weight.
|
48
|
+
Leave as much control to user as possible.\n 2. Fast response: no polling, pipe
|
49
|
+
architecture for immediate job response time.\n 3. No Setup: Can not require extra
|
50
|
+
\"maintenance thread\" in background. Auto maintained by each and every worker thread.\n\n"
|
51
|
+
email: liaody@gmail.com
|
52
|
+
executables: []
|
53
|
+
extensions: []
|
54
|
+
extra_rdoc_files: []
|
55
|
+
files:
|
56
|
+
- lib/rescheduler.rb
|
57
|
+
homepage: https://github.com/liaody/rescheduler
|
58
|
+
licenses:
|
59
|
+
- BSD
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
requirements: []
|
77
|
+
rubyforge_project:
|
78
|
+
rubygems_version: 1.8.24
|
79
|
+
signing_key:
|
80
|
+
specification_version: 3
|
81
|
+
summary: A job queue for immediate and delayed jobs using Redis
|
82
|
+
test_files: []
|
83
|
+
has_rdoc:
|