rq-ruby1.8 3.4.3
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/Gemfile +22 -0
- data/Gemfile.lock +22 -0
- data/INSTALL +166 -0
- data/LICENSE +10 -0
- data/Makefile +6 -0
- data/README +1183 -0
- data/Rakefile +37 -0
- data/TODO +24 -0
- data/TUTORIAL +230 -0
- data/VERSION +1 -0
- data/bin/rq +902 -0
- data/bin/rqmailer +865 -0
- data/example/a.rb +7 -0
- data/extconf.rb +198 -0
- data/gemspec.rb +40 -0
- data/install.rb +210 -0
- data/lib/rq.rb +155 -0
- data/lib/rq/arrayfields.rb +371 -0
- data/lib/rq/backer.rb +31 -0
- data/lib/rq/configfile.rb +82 -0
- data/lib/rq/configurator.rb +40 -0
- data/lib/rq/creator.rb +54 -0
- data/lib/rq/cron.rb +144 -0
- data/lib/rq/defaultconfig.txt +5 -0
- data/lib/rq/deleter.rb +51 -0
- data/lib/rq/executor.rb +40 -0
- data/lib/rq/feeder.rb +527 -0
- data/lib/rq/ioviewer.rb +48 -0
- data/lib/rq/job.rb +51 -0
- data/lib/rq/jobqueue.rb +947 -0
- data/lib/rq/jobrunner.rb +110 -0
- data/lib/rq/jobrunnerdaemon.rb +193 -0
- data/lib/rq/lister.rb +47 -0
- data/lib/rq/locker.rb +43 -0
- data/lib/rq/lockfile.rb +564 -0
- data/lib/rq/logging.rb +124 -0
- data/lib/rq/mainhelper.rb +189 -0
- data/lib/rq/orderedautohash.rb +39 -0
- data/lib/rq/orderedhash.rb +240 -0
- data/lib/rq/qdb.rb +733 -0
- data/lib/rq/querier.rb +98 -0
- data/lib/rq/rails.rb +80 -0
- data/lib/rq/recoverer.rb +28 -0
- data/lib/rq/refresher.rb +80 -0
- data/lib/rq/relayer.rb +283 -0
- data/lib/rq/resource.rb +22 -0
- data/lib/rq/resourcemanager.rb +40 -0
- data/lib/rq/resubmitter.rb +100 -0
- data/lib/rq/rotater.rb +98 -0
- data/lib/rq/sleepcycle.rb +46 -0
- data/lib/rq/snapshotter.rb +40 -0
- data/lib/rq/sqlite.rb +286 -0
- data/lib/rq/statuslister.rb +48 -0
- data/lib/rq/submitter.rb +113 -0
- data/lib/rq/toucher.rb +182 -0
- data/lib/rq/updater.rb +94 -0
- data/lib/rq/usage.rb +1222 -0
- data/lib/rq/util.rb +304 -0
- data/rdoc.sh +17 -0
- data/rq-ruby1.8.gemspec +120 -0
- data/test/.gitignore +1 -0
- data/test/test_rq.rb +145 -0
- data/white_box/crontab +2 -0
- data/white_box/joblist +8 -0
- data/white_box/killrq +18 -0
- data/white_box/rq_killer +27 -0
- metadata +208 -0
data/lib/rq/jobrunner.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
unless defined? $__rq_jobrunner__
|
2
|
+
module RQ
|
3
|
+
#--{{{
|
4
|
+
LIBDIR = File::dirname(File::expand_path(__FILE__)) + File::SEPARATOR unless
|
5
|
+
defined? LIBDIR
|
6
|
+
|
7
|
+
require 'drb/drb'
|
8
|
+
require 'yaml'
|
9
|
+
|
10
|
+
require LIBDIR + 'util'
|
11
|
+
|
12
|
+
#
|
13
|
+
# the JobRunner class is responsible for pre-forking a process/shell in
|
14
|
+
# which to run a job. this class is utilized by the JobRunnerDaemon so
|
15
|
+
# processes can be forked via a drb proxy to avoid actual forking during an
|
16
|
+
# sqlite transaction - which has undefined behaviour
|
17
|
+
#
|
18
|
+
class JobRunner
|
19
|
+
#--{{{
|
20
|
+
$VERBOSE = nil
|
21
|
+
include DRbUndumped
|
22
|
+
attr :q
|
23
|
+
attr :job
|
24
|
+
attr :jid
|
25
|
+
attr :cid
|
26
|
+
attr :shell
|
27
|
+
attr :command
|
28
|
+
attr :stdin
|
29
|
+
attr :stdout
|
30
|
+
attr :stderr
|
31
|
+
attr :data
|
32
|
+
alias pid cid
|
33
|
+
def initialize q, job
|
34
|
+
#--{{{
|
35
|
+
@q = q
|
36
|
+
@job = job
|
37
|
+
@jid = job['jid']
|
38
|
+
@command = job['command']
|
39
|
+
@shell = job['shell'] || 'bash'
|
40
|
+
@sh_like = File::basename(@shell) == 'bash' || File::basename(@shell) == 'sh'
|
41
|
+
@r,@w = IO::pipe
|
42
|
+
|
43
|
+
@env = {}
|
44
|
+
@env["PATH"] = [@q.bin, ENV["PATH"]].join(":")
|
45
|
+
@job.fields.each do |field|
|
46
|
+
key = "RQ_#{ field }".upcase.gsub(%r/\s+/,'_')
|
47
|
+
val = @job[field]
|
48
|
+
val = File.expand_path(File.join(@q.path,val)) if %w( stdin stdout stderr data).include?(field.to_s)
|
49
|
+
@env[key] = "#{ val }"
|
50
|
+
end
|
51
|
+
@env['RQ'] = File.expand_path @q.path
|
52
|
+
@env['RQ_JOB'] = @job.to_hash.to_yaml
|
53
|
+
|
54
|
+
@stdin = @job['stdin']
|
55
|
+
@stdout = @job['stdout']
|
56
|
+
@stderr = @job['stderr']
|
57
|
+
@data = @job['data']
|
58
|
+
|
59
|
+
@stdin &&= File::join @q.path, @stdin # assume path relative to queue
|
60
|
+
@stdout &&= File::join @q.path, @stdout # assume path relative to queue
|
61
|
+
@stderr &&= File::join @q.path, @stderr # assume path relative to queue
|
62
|
+
@data &&= File::join @q.path, @data # assume path relative to queue
|
63
|
+
|
64
|
+
@cid =
|
65
|
+
Util::fork do
|
66
|
+
@env.each{|k,v| ENV[k] = v}
|
67
|
+
ENV['RQ_PID'] = "#{ $$ }"
|
68
|
+
@w.close
|
69
|
+
STDIN.reopen @r
|
70
|
+
argv =
|
71
|
+
if @sh_like
|
72
|
+
[ [@shell, "__rq_job__#{ @jid }__#{ File::basename(@shell) }__"], '--login' ]
|
73
|
+
else
|
74
|
+
[ [@shell, "__rq_job__#{ @jid }__#{ File::basename(@shell) }__"], '-l' ]
|
75
|
+
end
|
76
|
+
exec *argv
|
77
|
+
end
|
78
|
+
@r.close
|
79
|
+
#--}}}
|
80
|
+
end
|
81
|
+
def run
|
82
|
+
#--{{{
|
83
|
+
command = @command.gsub %r/#.*/o, '' # kill comments
|
84
|
+
path = @q.bin
|
85
|
+
|
86
|
+
command =
|
87
|
+
if @sh_like
|
88
|
+
sin = "0<#{ @stdin }" if @stdin and File.exist?(@stdin)
|
89
|
+
sout = "1>#{ @stdout }" if @stdout
|
90
|
+
serr = "2>#{ @stderr }" if @stderr
|
91
|
+
"( PATH=#{ path }:$PATH #{ command } ;) #{ sin } #{ sout } #{ serr }"
|
92
|
+
else
|
93
|
+
sin = "<#{ @stdin }" if @stdin
|
94
|
+
sout = ">#{ @stdout }" if @stdout
|
95
|
+
serr = ">&#{ @stderr }" if @stderr
|
96
|
+
"( ( #{ command } ;) #{ sin } #{ sout } ) #{ serr }"
|
97
|
+
end
|
98
|
+
|
99
|
+
FileUtils::touch(@stdin) unless File.exist?(@stdin)
|
100
|
+
|
101
|
+
@w.puts command
|
102
|
+
@w.close
|
103
|
+
#--}}}
|
104
|
+
end
|
105
|
+
#--}}}
|
106
|
+
end # class JobRunner
|
107
|
+
#--}}}
|
108
|
+
end # module RQ
|
109
|
+
$__rq_jobrunner__ = __FILE__
|
110
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
unless defined? $__rq_jobrunnerdaemon__
|
2
|
+
module RQ
|
3
|
+
#--{{{
|
4
|
+
LIBDIR = File::dirname(File::expand_path(__FILE__)) + File::SEPARATOR unless
|
5
|
+
defined? LIBDIR
|
6
|
+
|
7
|
+
require 'drb/drb'
|
8
|
+
require 'fileutils'
|
9
|
+
require 'tmpdir'
|
10
|
+
require 'tempfile'
|
11
|
+
|
12
|
+
require LIBDIR + 'job'
|
13
|
+
require LIBDIR + 'jobrunner'
|
14
|
+
|
15
|
+
#
|
16
|
+
# as stated in the description of the JobRunner class, the JobRunnerDaemon
|
17
|
+
# is a helper daemon that runs as a drb object. it's primary responsibilty
|
18
|
+
# is simply for enable forks to occur in a a different address space that
|
19
|
+
# the one doing the sqlite transaction. in addition to forking to create
|
20
|
+
# child processes in which to run jobs, the JobRunnerDaemon daemon also
|
21
|
+
# provides facilities to wait for these children
|
22
|
+
#
|
23
|
+
class JobRunnerDaemon
|
24
|
+
#--{{{
|
25
|
+
include Logging
|
26
|
+
|
27
|
+
class << self
|
28
|
+
#--{{{
|
29
|
+
def daemon(*a,&b)
|
30
|
+
#--{{{
|
31
|
+
jrd = new(*a, &b)
|
32
|
+
|
33
|
+
r, w = IO::pipe
|
34
|
+
|
35
|
+
unless((pid = fork)) # child
|
36
|
+
$0 = "#{ self }".gsub(%r/[^a-zA-Z]+/,'_').downcase
|
37
|
+
begin
|
38
|
+
r.close
|
39
|
+
n = 0
|
40
|
+
uri = nil
|
41
|
+
socket = nil
|
42
|
+
|
43
|
+
42.times do
|
44
|
+
begin
|
45
|
+
s = "%s/%s_%s_%s_%s" %
|
46
|
+
[Dir::tmpdir, File::basename($0), Process::ppid, n, rand(42)]
|
47
|
+
u = "drbunix://#{ s }"
|
48
|
+
DRb::start_service u, jrd
|
49
|
+
socket = s
|
50
|
+
uri = u
|
51
|
+
break
|
52
|
+
rescue Errno::EADDRINUSE
|
53
|
+
n += 1
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
if socket and uri
|
58
|
+
w.write socket
|
59
|
+
w.close
|
60
|
+
pid = Process::pid
|
61
|
+
ppid = Process::ppid
|
62
|
+
cur = Thread::current
|
63
|
+
Thread::new(pid, ppid, cur) do |pid, ppid, cur|
|
64
|
+
loop do
|
65
|
+
begin
|
66
|
+
Process::kill 0, ppid
|
67
|
+
sleep 42
|
68
|
+
rescue
|
69
|
+
cur.raise "parent <#{ ppid }> died unexpectedly"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
DRb::thread.join
|
74
|
+
else
|
75
|
+
w.close
|
76
|
+
end
|
77
|
+
ensure
|
78
|
+
exit!
|
79
|
+
end
|
80
|
+
else # parent
|
81
|
+
w.close
|
82
|
+
socket = r.read
|
83
|
+
r.close
|
84
|
+
|
85
|
+
if socket and File::exist?(socket)
|
86
|
+
at_exit{ FileUtils::rm_f socket }
|
87
|
+
uri = "drbunix://#{ socket }"
|
88
|
+
#
|
89
|
+
# starting this on localhost avoids dns lookups!
|
90
|
+
#
|
91
|
+
DRb::start_service 'druby://localhost:0', nil
|
92
|
+
jrd = DRbObject::new nil, uri
|
93
|
+
jrd.pid = pid
|
94
|
+
jrd.uri = uri
|
95
|
+
else
|
96
|
+
raise "failed to start job runner daemon"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
return jrd
|
101
|
+
#--}}}
|
102
|
+
end
|
103
|
+
#--}}}
|
104
|
+
end
|
105
|
+
attr :q
|
106
|
+
attr :runners
|
107
|
+
attr :pid, true
|
108
|
+
attr :uri, true
|
109
|
+
def initialize q
|
110
|
+
#--{{{
|
111
|
+
@q = q
|
112
|
+
@runners = {}
|
113
|
+
@uri = nil
|
114
|
+
@pid = Process::pid
|
115
|
+
#--}}}
|
116
|
+
end
|
117
|
+
def runner job
|
118
|
+
#--{{{
|
119
|
+
r = nil
|
120
|
+
retried = false
|
121
|
+
begin
|
122
|
+
r = JobRunner::new @q, job
|
123
|
+
rescue Errno::ENOMEM, Errno::EAGAIN
|
124
|
+
GC::start
|
125
|
+
unless retried
|
126
|
+
retried = true
|
127
|
+
retry
|
128
|
+
else
|
129
|
+
raise
|
130
|
+
end
|
131
|
+
end
|
132
|
+
@runners[r.pid] = r
|
133
|
+
r
|
134
|
+
#--}}}
|
135
|
+
end
|
136
|
+
def wait
|
137
|
+
#--{{{
|
138
|
+
pid = Process::wait
|
139
|
+
@runners.delete pid
|
140
|
+
pid
|
141
|
+
#--}}}
|
142
|
+
end
|
143
|
+
def wait2
|
144
|
+
#--{{{
|
145
|
+
pid, status = Process::wait2
|
146
|
+
@runners.delete pid
|
147
|
+
[pid, status]
|
148
|
+
#--}}}
|
149
|
+
end
|
150
|
+
def waitpid pid = -1, flags = 0
|
151
|
+
#--{{{
|
152
|
+
pid = pid.pid if pid.respond_to? 'pid'
|
153
|
+
pid = Process::waitpid pid, flags
|
154
|
+
@runners.delete pid
|
155
|
+
pid
|
156
|
+
#--}}}
|
157
|
+
end
|
158
|
+
def waitpid2 pid = -1, flags = 0
|
159
|
+
#--{{{
|
160
|
+
pid = pid.pid if pid.respond_to? 'pid'
|
161
|
+
pid, status = Process::waitpid2 pid, flags
|
162
|
+
@runners.delete pid
|
163
|
+
[pid, status]
|
164
|
+
#--}}}
|
165
|
+
end
|
166
|
+
def shutdown
|
167
|
+
#--{{{
|
168
|
+
@death =
|
169
|
+
Thread::new do
|
170
|
+
begin
|
171
|
+
while not @runners.empty?
|
172
|
+
pid = Process::wait
|
173
|
+
@runners.delete pid
|
174
|
+
end
|
175
|
+
ensure
|
176
|
+
#sleep 4.2
|
177
|
+
DRb::thread.kill
|
178
|
+
Thread::main exit!
|
179
|
+
end
|
180
|
+
end
|
181
|
+
#--}}}
|
182
|
+
end
|
183
|
+
def install_signal_handlers
|
184
|
+
#--{{{
|
185
|
+
%w(TERM INT HUP).each{|sig| trap sig, 'SIG_IGN'}
|
186
|
+
#--}}}
|
187
|
+
end
|
188
|
+
#--}}}
|
189
|
+
end # class JobRunnerDaemon
|
190
|
+
#--}}}
|
191
|
+
end # module RQ
|
192
|
+
$__rq_jobrunnerdaemon__ = __FILE__
|
193
|
+
end
|
data/lib/rq/lister.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
unless defined? $__rq_lister__
|
2
|
+
module RQ
|
3
|
+
#--{{{
|
4
|
+
LIBDIR = File::dirname(File::expand_path(__FILE__)) + File::SEPARATOR unless
|
5
|
+
defined? LIBDIR
|
6
|
+
|
7
|
+
require LIBDIR + 'mainhelper'
|
8
|
+
|
9
|
+
#
|
10
|
+
# the Lister class simply dumps the contents of the queue in valid yaml
|
11
|
+
#
|
12
|
+
class Lister < MainHelper
|
13
|
+
#--{{{
|
14
|
+
def list
|
15
|
+
#--{{{
|
16
|
+
set_q
|
17
|
+
|
18
|
+
@infile = @options['infile']
|
19
|
+
debug{ "infile <#{ @infile }>" }
|
20
|
+
|
21
|
+
jobs = []
|
22
|
+
if @infile
|
23
|
+
open(@infile) do |f|
|
24
|
+
debug{ "reading jobs from <#{ @infile }>" }
|
25
|
+
loadio f, @infile, jobs
|
26
|
+
end
|
27
|
+
end
|
28
|
+
if stdin?
|
29
|
+
debug{ "reading jobs from <stdin>" }
|
30
|
+
loadio stdin, 'stdin', jobs
|
31
|
+
end
|
32
|
+
jobs.each{|job| @argv << Integer(job['jid'])}
|
33
|
+
|
34
|
+
@q.qdb.transaction_retries = 1
|
35
|
+
|
36
|
+
@q.list(*@argv, &dumping_yaml_tuples)
|
37
|
+
|
38
|
+
jobs = nil
|
39
|
+
self
|
40
|
+
#--}}}
|
41
|
+
end
|
42
|
+
#--}}}
|
43
|
+
end # class Lister
|
44
|
+
#--}}}
|
45
|
+
end # module RQ
|
46
|
+
$__rq_lister__ = __FILE__
|
47
|
+
end
|
data/lib/rq/locker.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
unless defined? $__rq_locker__
|
2
|
+
module RQ
|
3
|
+
#--{{{
|
4
|
+
LIBDIR = File::dirname(File::expand_path(__FILE__)) + File::SEPARATOR unless
|
5
|
+
defined? LIBDIR
|
6
|
+
|
7
|
+
require LIBDIR + 'util'
|
8
|
+
require LIBDIR + 'locker'
|
9
|
+
|
10
|
+
#
|
11
|
+
# a Locker simply obtains an exclusive lock on a queue's lock and then runs
|
12
|
+
# and arbitrary command which is taken from the command line only. it's use
|
13
|
+
# is simply to allow unforseen applications to coordinate access to the
|
14
|
+
# queue
|
15
|
+
#
|
16
|
+
class Locker < MainHelper
|
17
|
+
#--{{{
|
18
|
+
def lock
|
19
|
+
#--{{{
|
20
|
+
set_q
|
21
|
+
ltype = @argv.shift
|
22
|
+
debug{ "ltype <#{ ltype }>" }
|
23
|
+
read_only =
|
24
|
+
case ltype
|
25
|
+
when /^\s*r(?:ead)?|^\s*sh(?:ared)?/io
|
26
|
+
true
|
27
|
+
when /^\s*w(?:rite)?|^\s*ex(?:clusive)?/io
|
28
|
+
false
|
29
|
+
else
|
30
|
+
raise "lock type must be one of (r)ead|(sh)ared|(w)rite|(ex)clusive, not <#{ ltype }>"
|
31
|
+
end
|
32
|
+
cmd = @argv.join(' ').strip
|
33
|
+
raise "no command given for lock type <#{ ltype }>" if cmd.empty?
|
34
|
+
debug{ "cmd <#{ cmd }>" }
|
35
|
+
@q.lock(:read_only => read_only){ Util::system cmd }
|
36
|
+
#--}}}
|
37
|
+
end
|
38
|
+
#--}}}
|
39
|
+
end # class Locker
|
40
|
+
#--}}}
|
41
|
+
end # module RQ
|
42
|
+
$__rq_locker__ = __FILE__
|
43
|
+
end
|
data/lib/rq/lockfile.rb
ADDED
@@ -0,0 +1,564 @@
|
|
1
|
+
unless(defined?($__lockfile__) or defined?(Lockfile))
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'timeout'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
class Lockfile
|
8
|
+
#--{{{
|
9
|
+
VERSION = '1.4.3'
|
10
|
+
def version() VERSION end
|
11
|
+
|
12
|
+
class LockError < StandardError; end
|
13
|
+
class StolenLockError < LockError; end
|
14
|
+
class StackingLockError < LockError; end
|
15
|
+
class StatLockError < LockError; end
|
16
|
+
class MaxTriesLockError < LockError; end
|
17
|
+
class TimeoutLockError < LockError; end
|
18
|
+
class NFSLockError < LockError; end
|
19
|
+
class UnLockError < LockError; end
|
20
|
+
|
21
|
+
class SleepCycle < Array
|
22
|
+
#--{{{
|
23
|
+
attr :min
|
24
|
+
attr :max
|
25
|
+
attr :range
|
26
|
+
attr :inc
|
27
|
+
def initialize min, max, inc
|
28
|
+
#--{{{
|
29
|
+
@min, @max, @inc = Float(min), Float(max), Float(inc)
|
30
|
+
@range = @max - @min
|
31
|
+
raise RangeError, "max(#{ @max }) <= min(#{ @min })" if @max <= @min
|
32
|
+
raise RangeError, "inc(#{ @inc }) > range(#{ @range })" if @inc > @range
|
33
|
+
raise RangeError, "inc(#{ @inc }) <= 0" if @inc <= 0
|
34
|
+
raise RangeError, "range(#{ @range }) <= 0" if @range <= 0
|
35
|
+
s = @min
|
36
|
+
push(s) and s += @inc while(s <= @max)
|
37
|
+
self[-1] = @max if self[-1] < @max
|
38
|
+
reset
|
39
|
+
#--}}}
|
40
|
+
end
|
41
|
+
def next
|
42
|
+
#--{{{
|
43
|
+
ret = self[@idx]
|
44
|
+
@idx = ((@idx + 1) % self.size)
|
45
|
+
ret
|
46
|
+
#--}}}
|
47
|
+
end
|
48
|
+
def reset
|
49
|
+
#--{{{
|
50
|
+
@idx = 0
|
51
|
+
#--}}}
|
52
|
+
end
|
53
|
+
#--}}}
|
54
|
+
end
|
55
|
+
|
56
|
+
HOSTNAME = Socket::gethostname
|
57
|
+
|
58
|
+
DEFAULT_RETRIES = nil # maximum number of attempts
|
59
|
+
DEFAULT_TIMEOUT = nil # the longest we will try
|
60
|
+
DEFAULT_MAX_AGE = 3600 # lockfiles older than this are stale
|
61
|
+
DEFAULT_SLEEP_INC = 2 # sleep cycle is this much longer each time
|
62
|
+
DEFAULT_MIN_SLEEP = 2 # shortest sleep time
|
63
|
+
DEFAULT_MAX_SLEEP = 32 # longest sleep time
|
64
|
+
DEFAULT_SUSPEND = 1800 # iff we steal a lock wait this long before we go on
|
65
|
+
DEFAULT_REFRESH = 8 # how often we touch/validate the lock
|
66
|
+
DEFAULT_DONT_CLEAN = false # iff we leave lock files lying around
|
67
|
+
DEFAULT_POLL_RETRIES = 16 # this many polls makes one 'try'
|
68
|
+
DEFAULT_POLL_MAX_SLEEP = 0.08 # the longest we'll sleep between polls
|
69
|
+
DEFAULT_DONT_SWEEP = false # if we cleanup after other process on our host
|
70
|
+
DEFAULT_DONT_USE_LOCK_ID = false # if we dump lock info into lockfile
|
71
|
+
|
72
|
+
DEFAULT_DEBUG = ENV['LOCKFILE_DEBUG'] || false
|
73
|
+
|
74
|
+
class << self
|
75
|
+
#--{{{
|
76
|
+
attr :retries, true
|
77
|
+
attr :max_age, true
|
78
|
+
attr :sleep_inc, true
|
79
|
+
attr :min_sleep, true
|
80
|
+
attr :max_sleep, true
|
81
|
+
attr :suspend, true
|
82
|
+
attr :timeout, true
|
83
|
+
attr :refresh, true
|
84
|
+
attr :debug, true
|
85
|
+
attr :dont_clean, true
|
86
|
+
attr :poll_retries, true
|
87
|
+
attr :poll_max_sleep, true
|
88
|
+
attr :dont_sweep, true
|
89
|
+
attr :dont_use_lock_id, true
|
90
|
+
|
91
|
+
def init
|
92
|
+
#--{{{
|
93
|
+
@retries = DEFAULT_RETRIES
|
94
|
+
@max_age = DEFAULT_MAX_AGE
|
95
|
+
@sleep_inc = DEFAULT_SLEEP_INC
|
96
|
+
@min_sleep = DEFAULT_MIN_SLEEP
|
97
|
+
@max_sleep = DEFAULT_MAX_SLEEP
|
98
|
+
@suspend = DEFAULT_SUSPEND
|
99
|
+
@timeout = DEFAULT_TIMEOUT
|
100
|
+
@refresh = DEFAULT_REFRESH
|
101
|
+
@dont_clean = DEFAULT_DONT_CLEAN
|
102
|
+
@poll_retries = DEFAULT_POLL_RETRIES
|
103
|
+
@poll_max_sleep = DEFAULT_POLL_MAX_SLEEP
|
104
|
+
@dont_sweep = DEFAULT_DONT_SWEEP
|
105
|
+
@dont_use_lock_id = DEFAULT_DONT_USE_LOCK_ID
|
106
|
+
|
107
|
+
@debug = DEFAULT_DEBUG
|
108
|
+
|
109
|
+
STDOUT.sync = true if @debug
|
110
|
+
STDERR.sync = true if @debug
|
111
|
+
#--}}}
|
112
|
+
end
|
113
|
+
#--}}}
|
114
|
+
end
|
115
|
+
self.init
|
116
|
+
|
117
|
+
attr :klass
|
118
|
+
attr :path
|
119
|
+
attr :opts
|
120
|
+
attr :locked
|
121
|
+
attr :thief
|
122
|
+
attr :dirname
|
123
|
+
attr :basename
|
124
|
+
attr :clean
|
125
|
+
attr :retries
|
126
|
+
attr :max_age
|
127
|
+
attr :sleep_inc
|
128
|
+
attr :min_sleep
|
129
|
+
attr :max_sleep
|
130
|
+
attr :suspend
|
131
|
+
attr :refresh
|
132
|
+
attr :timeout
|
133
|
+
attr :dont_clean
|
134
|
+
attr :poll_retries
|
135
|
+
attr :poll_max_sleep
|
136
|
+
attr :dont_sweep
|
137
|
+
attr :dont_use_lock_id
|
138
|
+
|
139
|
+
attr :debug, true
|
140
|
+
|
141
|
+
alias thief? thief
|
142
|
+
alias locked? locked
|
143
|
+
alias debug? debug
|
144
|
+
|
145
|
+
def self::create(path, *a, &b)
|
146
|
+
#--{{{
|
147
|
+
opts = {
|
148
|
+
'retries' => 0,
|
149
|
+
'min_sleep' => 0,
|
150
|
+
'max_sleep' => 1,
|
151
|
+
'sleep_inc' => 1,
|
152
|
+
'max_age' => nil,
|
153
|
+
'suspend' => 0,
|
154
|
+
'refresh' => nil,
|
155
|
+
'timeout' => nil,
|
156
|
+
'poll_retries' => 0,
|
157
|
+
'dont_clean' => true,
|
158
|
+
'dont_sweep' => false,
|
159
|
+
'dont_use_lock_id' => true,
|
160
|
+
}
|
161
|
+
begin
|
162
|
+
new(path, opts).lock
|
163
|
+
rescue LockError
|
164
|
+
raise Errno::EEXIST, path
|
165
|
+
end
|
166
|
+
open(path, *a, &b)
|
167
|
+
#--}}}
|
168
|
+
end
|
169
|
+
|
170
|
+
def initialize(path, opts = {}, &block)
|
171
|
+
#--{{{
|
172
|
+
@klass = self.class
|
173
|
+
@path = path
|
174
|
+
@opts = opts
|
175
|
+
|
176
|
+
@retries = getopt 'retries' , @klass.retries
|
177
|
+
@max_age = getopt 'max_age' , @klass.max_age
|
178
|
+
@sleep_inc = getopt 'sleep_inc' , @klass.sleep_inc
|
179
|
+
@min_sleep = getopt 'min_sleep' , @klass.min_sleep
|
180
|
+
@max_sleep = getopt 'max_sleep' , @klass.max_sleep
|
181
|
+
@suspend = getopt 'suspend' , @klass.suspend
|
182
|
+
@timeout = getopt 'timeout' , @klass.timeout
|
183
|
+
@refresh = getopt 'refresh' , @klass.refresh
|
184
|
+
@dont_clean = getopt 'dont_clean' , @klass.dont_clean
|
185
|
+
@poll_retries = getopt 'poll_retries' , @klass.poll_retries
|
186
|
+
@poll_max_sleep = getopt 'poll_max_sleep' , @klass.poll_max_sleep
|
187
|
+
@dont_sweep = getopt 'dont_sweep' , @klass.dont_sweep
|
188
|
+
@dont_use_lock_id = getopt 'dont_use_lock_id' , @klass.dont_use_lock_id
|
189
|
+
@debug = getopt 'debug' , @klass.debug
|
190
|
+
|
191
|
+
@sleep_cycle = SleepCycle::new @min_sleep, @max_sleep, @sleep_inc
|
192
|
+
|
193
|
+
@clean = @dont_clean ? nil : lambda{ File::unlink @path rescue nil }
|
194
|
+
@dirname = File::dirname @path
|
195
|
+
@basename = File::basename @path
|
196
|
+
@thief = false
|
197
|
+
@locked = false
|
198
|
+
|
199
|
+
lock(&block) if block
|
200
|
+
#--}}}
|
201
|
+
end
|
202
|
+
def lock
|
203
|
+
#--{{{
|
204
|
+
raise StackingLockError, "<#{ @path }> is locked!" if @locked
|
205
|
+
|
206
|
+
sweep unless @dont_sweep
|
207
|
+
|
208
|
+
ret = nil
|
209
|
+
|
210
|
+
attempt do
|
211
|
+
begin
|
212
|
+
@sleep_cycle.reset
|
213
|
+
create_tmplock do |f|
|
214
|
+
begin
|
215
|
+
Timeout::timeout(@timeout) do
|
216
|
+
tmp_path = f.path
|
217
|
+
tmp_stat = f.lstat
|
218
|
+
n_retries = 0
|
219
|
+
trace{ "attempting to lock <#{ @path }>..." }
|
220
|
+
begin
|
221
|
+
i = 0
|
222
|
+
begin
|
223
|
+
trace{ "polling attempt <#{ i }>..." }
|
224
|
+
begin
|
225
|
+
File::link tmp_path, @path
|
226
|
+
rescue Errno::ENOENT
|
227
|
+
try_again!
|
228
|
+
end
|
229
|
+
lock_stat = File::lstat @path
|
230
|
+
raise StatLockError, "stat's do not agree" unless
|
231
|
+
tmp_stat.rdev == lock_stat.rdev and tmp_stat.ino == lock_stat.ino
|
232
|
+
trace{ "aquired lock <#{ @path }>" }
|
233
|
+
@locked = true
|
234
|
+
rescue => e
|
235
|
+
i += 1
|
236
|
+
unless i >= @poll_retries
|
237
|
+
t = [rand(@poll_max_sleep), @poll_max_sleep].min
|
238
|
+
trace{ "poll sleep <#{ t }>..." }
|
239
|
+
sleep t
|
240
|
+
retry
|
241
|
+
end
|
242
|
+
raise
|
243
|
+
end
|
244
|
+
|
245
|
+
rescue => e
|
246
|
+
n_retries += 1
|
247
|
+
trace{ "n_retries <#{ n_retries }>" }
|
248
|
+
case validlock?
|
249
|
+
when true
|
250
|
+
raise MaxTriesLockError, "surpased retries <#{ @retries }>" if
|
251
|
+
@retries and n_retries >= @retries
|
252
|
+
trace{ "found valid lock" }
|
253
|
+
sleeptime = @sleep_cycle.next
|
254
|
+
trace{ "sleep <#{ sleeptime }>..." }
|
255
|
+
sleep sleeptime
|
256
|
+
when false
|
257
|
+
trace{ "found invalid lock and removing" }
|
258
|
+
begin
|
259
|
+
File::unlink @path
|
260
|
+
@thief = true
|
261
|
+
warn "<#{ @path }> stolen by <#{ Process.pid }> at <#{ timestamp }>"
|
262
|
+
trace{ "i am a thief!" }
|
263
|
+
rescue Errno::ENOENT
|
264
|
+
end
|
265
|
+
trace{ "suspending <#{ @suspend }>" }
|
266
|
+
sleep @suspend
|
267
|
+
when nil
|
268
|
+
raise MaxTriesLockError, "surpased retries <#{ @retries }>" if
|
269
|
+
@retries and n_retries >= @retries
|
270
|
+
end
|
271
|
+
retry
|
272
|
+
end # begin
|
273
|
+
end # timeout
|
274
|
+
rescue Timeout::Error
|
275
|
+
raise TimeoutLockError, "surpassed timeout <#{ @timeout }>"
|
276
|
+
end # begin
|
277
|
+
end # create_tmplock
|
278
|
+
|
279
|
+
if block_given?
|
280
|
+
stolen = false
|
281
|
+
refresher = (@refresh ? new_refresher : nil)
|
282
|
+
begin
|
283
|
+
begin
|
284
|
+
ret = yield @path
|
285
|
+
rescue StolenLockError
|
286
|
+
stolen = true
|
287
|
+
raise
|
288
|
+
end
|
289
|
+
ensure
|
290
|
+
begin
|
291
|
+
refresher.kill if refresher and refresher.status
|
292
|
+
ensure
|
293
|
+
unlock unless stolen
|
294
|
+
end
|
295
|
+
end
|
296
|
+
else
|
297
|
+
ObjectSpace.define_finalizer self, @clean if @clean
|
298
|
+
ret = self
|
299
|
+
end
|
300
|
+
rescue Errno::ESTALE, Errno::EIO => e
|
301
|
+
raise(NFSLockError, errmsg(e))
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
return ret
|
306
|
+
#--}}}
|
307
|
+
end
|
308
|
+
def sweep
|
309
|
+
#----{{{
|
310
|
+
begin
|
311
|
+
glob = File::join(@dirname, ".*lck")
|
312
|
+
paths = Dir[glob]
|
313
|
+
paths.each do |path|
|
314
|
+
begin
|
315
|
+
basename = File::basename path
|
316
|
+
pat = %r/^\s*\.([^_]+)_([^_]+)/o
|
317
|
+
if pat.match(basename)
|
318
|
+
host, pid = $1, $2
|
319
|
+
else
|
320
|
+
next
|
321
|
+
end
|
322
|
+
host.gsub!(%r/^\.+|\.+$/,'')
|
323
|
+
quad = host.split %r/\./
|
324
|
+
host = quad.first
|
325
|
+
pat = %r/^\s*#{ host }/i
|
326
|
+
if pat.match(HOSTNAME) and %r/^\s*\d+\s*$/.match(pid)
|
327
|
+
unless alive?(pid)
|
328
|
+
trace{ "process <#{ pid }> on <#{ host }> is no longer alive" }
|
329
|
+
trace{ "sweeping <#{ path }>" }
|
330
|
+
FileUtils::rm_f path
|
331
|
+
else
|
332
|
+
trace{ "process <#{ pid }> on <#{ host }> is still alive" }
|
333
|
+
trace{ "ignoring <#{ path }>" }
|
334
|
+
end
|
335
|
+
else
|
336
|
+
trace{ "ignoring <#{ path }> generated by <#{ host }>" }
|
337
|
+
end
|
338
|
+
rescue
|
339
|
+
next
|
340
|
+
end
|
341
|
+
end
|
342
|
+
rescue => e
|
343
|
+
warn(errmsg(e))
|
344
|
+
end
|
345
|
+
#----}}}
|
346
|
+
end
|
347
|
+
def alive? pid
|
348
|
+
#----{{{
|
349
|
+
pid = Integer("#{ pid }")
|
350
|
+
begin
|
351
|
+
Process::kill 0, pid
|
352
|
+
true
|
353
|
+
rescue Errno::ESRCH
|
354
|
+
false
|
355
|
+
end
|
356
|
+
#----}}}
|
357
|
+
end
|
358
|
+
def unlock
|
359
|
+
#--{{{
|
360
|
+
raise UnLockError, "<#{ @path }> is not locked!" unless @locked
|
361
|
+
begin
|
362
|
+
File::unlink @path
|
363
|
+
rescue Errno::ENOENT
|
364
|
+
raise StolenLockError, @path
|
365
|
+
ensure
|
366
|
+
@thief = false
|
367
|
+
@locked = false
|
368
|
+
ObjectSpace.undefine_finalizer self if @clean
|
369
|
+
end
|
370
|
+
#--}}}
|
371
|
+
end
|
372
|
+
def new_refresher
|
373
|
+
#--{{{
|
374
|
+
Thread::new(Thread::current, @path, @refresh, @dont_use_lock_id) do |thread, path, refresh, dont_use_lock_id|
|
375
|
+
loop do
|
376
|
+
begin
|
377
|
+
touch path
|
378
|
+
trace{"touched <#{ path }> @ <#{ Time.now.to_f }>"}
|
379
|
+
unless dont_use_lock_id
|
380
|
+
loaded = load_lock_id(IO.read(path))
|
381
|
+
trace{"loaded <\n#{ loaded.inspect }\n>"}
|
382
|
+
raise unless loaded == @lock_id
|
383
|
+
end
|
384
|
+
sleep refresh
|
385
|
+
rescue Exception => e
|
386
|
+
trace{errmsg e}
|
387
|
+
thread.raise StolenLockError
|
388
|
+
Thread::exit
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
#--}}}
|
393
|
+
end
|
394
|
+
def validlock?
|
395
|
+
#--{{{
|
396
|
+
if @max_age
|
397
|
+
uncache @path rescue nil
|
398
|
+
begin
|
399
|
+
return((Time.now - File::stat(@path).mtime) < @max_age)
|
400
|
+
rescue Errno::ENOENT
|
401
|
+
return nil
|
402
|
+
end
|
403
|
+
else
|
404
|
+
exist = File::exist?(@path)
|
405
|
+
return(exist ? true : nil)
|
406
|
+
end
|
407
|
+
#--}}}
|
408
|
+
end
|
409
|
+
def uncache file
|
410
|
+
#--{{{
|
411
|
+
refresh = nil
|
412
|
+
begin
|
413
|
+
is_a_file = File === file
|
414
|
+
path = (is_a_file ? file.path : file.to_s)
|
415
|
+
stat = (is_a_file ? file.stat : File::stat(file.to_s))
|
416
|
+
refresh = tmpnam(File::dirname(path))
|
417
|
+
File::link path, refresh
|
418
|
+
File::chmod stat.mode, path
|
419
|
+
File::utime stat.atime, stat.mtime, path
|
420
|
+
ensure
|
421
|
+
begin
|
422
|
+
File::unlink refresh if refresh
|
423
|
+
rescue Errno::ENOENT
|
424
|
+
end
|
425
|
+
end
|
426
|
+
#--}}}
|
427
|
+
end
|
428
|
+
def create_tmplock
|
429
|
+
#--{{{
|
430
|
+
tmplock = tmpnam @dirname
|
431
|
+
begin
|
432
|
+
create(tmplock) do |f|
|
433
|
+
unless dont_use_lock_id
|
434
|
+
@lock_id = gen_lock_id
|
435
|
+
dumped = dump_lock_id
|
436
|
+
trace{"lock_id <\n#{ @lock_id.inspect }\n>"}
|
437
|
+
f.write dumped
|
438
|
+
f.flush
|
439
|
+
end
|
440
|
+
yield f
|
441
|
+
end
|
442
|
+
ensure
|
443
|
+
begin; File::unlink tmplock; rescue Errno::ENOENT; end if tmplock
|
444
|
+
end
|
445
|
+
#--}}}
|
446
|
+
end
|
447
|
+
def gen_lock_id
|
448
|
+
#--{{{
|
449
|
+
Hash[
|
450
|
+
'host' => "#{ HOSTNAME }",
|
451
|
+
'pid' => "#{ Process.pid }",
|
452
|
+
'ppid' => "#{ Process.ppid }",
|
453
|
+
'time' => timestamp,
|
454
|
+
]
|
455
|
+
#--}}}
|
456
|
+
end
|
457
|
+
def timestamp
|
458
|
+
#--{{{
|
459
|
+
time = Time.now
|
460
|
+
usec = time.usec.to_s
|
461
|
+
usec << '0' while usec.size < 6
|
462
|
+
"#{ time.strftime('%Y-%m-%d %H:%M:%S') }.#{ usec }"
|
463
|
+
#--}}}
|
464
|
+
end
|
465
|
+
def dump_lock_id lock_id = @lock_id
|
466
|
+
#--{{{
|
467
|
+
"host: %s\npid: %s\nppid: %s\ntime: %s\n" %
|
468
|
+
lock_id.values_at('host','pid','ppid','time')
|
469
|
+
#--}}}
|
470
|
+
end
|
471
|
+
def load_lock_id buf
|
472
|
+
#--{{{
|
473
|
+
lock_id = {}
|
474
|
+
kv = %r/([^:]+):(.*)/o
|
475
|
+
buf.each do |line|
|
476
|
+
m = kv.match line
|
477
|
+
k, v = m[1], m[2]
|
478
|
+
next unless m and k and v
|
479
|
+
lock_id[k.strip] = v.strip
|
480
|
+
end
|
481
|
+
lock_id
|
482
|
+
#--}}}
|
483
|
+
end
|
484
|
+
def tmpnam dir, seed = File::basename($0)
|
485
|
+
#--{{{
|
486
|
+
pid = Process.pid
|
487
|
+
time = Time.now
|
488
|
+
sec = time.to_i
|
489
|
+
usec = time.usec
|
490
|
+
"%s%s.%s_%d_%s_%d_%d_%d.lck" %
|
491
|
+
[dir, File::SEPARATOR, HOSTNAME, pid, seed, sec, usec, rand(sec)]
|
492
|
+
#--}}}
|
493
|
+
end
|
494
|
+
def create path
|
495
|
+
#--{{{
|
496
|
+
umask = nil
|
497
|
+
f = nil
|
498
|
+
begin
|
499
|
+
umask = File::umask 022
|
500
|
+
f = open path, File::WRONLY|File::CREAT|File::EXCL, 0644
|
501
|
+
ensure
|
502
|
+
File::umask umask if umask
|
503
|
+
end
|
504
|
+
return(block_given? ? begin; yield f; ensure; f.close; end : f)
|
505
|
+
#--}}}
|
506
|
+
end
|
507
|
+
def touch path
|
508
|
+
#--{{{
|
509
|
+
FileUtils.touch path
|
510
|
+
#--}}}
|
511
|
+
end
|
512
|
+
def getopt key, default = nil
|
513
|
+
#--{{{
|
514
|
+
[ key, key.to_s, key.to_s.intern ].each do |k|
|
515
|
+
return @opts[k] if @opts.has_key?(k)
|
516
|
+
end
|
517
|
+
return default
|
518
|
+
#--}}}
|
519
|
+
end
|
520
|
+
def to_str
|
521
|
+
#--{{{
|
522
|
+
@path
|
523
|
+
#--}}}
|
524
|
+
end
|
525
|
+
alias to_s to_str
|
526
|
+
def trace s = nil
|
527
|
+
#--{{{
|
528
|
+
STDERR.puts((s ? s : yield)) if @debug
|
529
|
+
#--}}}
|
530
|
+
end
|
531
|
+
def errmsg e
|
532
|
+
#--{{{
|
533
|
+
"%s (%s)\n%s\n" % [e.class, e.message, e.backtrace.join("\n")]
|
534
|
+
#--}}}
|
535
|
+
end
|
536
|
+
def attempt
|
537
|
+
#----{{{
|
538
|
+
ret = nil
|
539
|
+
loop{ break unless catch('attempt'){ ret = yield } == 'try_again' }
|
540
|
+
ret
|
541
|
+
#----}}}
|
542
|
+
end
|
543
|
+
def try_again!
|
544
|
+
#----{{{
|
545
|
+
throw 'attempt', 'try_again'
|
546
|
+
#----}}}
|
547
|
+
end
|
548
|
+
alias again! try_again!
|
549
|
+
def give_up!
|
550
|
+
#----{{{
|
551
|
+
throw 'attempt', 'give_up'
|
552
|
+
#----}}}
|
553
|
+
end
|
554
|
+
#--}}}
|
555
|
+
end
|
556
|
+
|
557
|
+
def Lockfile path, *a, &b
|
558
|
+
#--{{{
|
559
|
+
Lockfile.new(path, *a, &b)
|
560
|
+
#--}}}
|
561
|
+
end
|
562
|
+
|
563
|
+
$__lockfile__ = __FILE__
|
564
|
+
end
|