rq-ruby1.8 3.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/Gemfile +22 -0
  2. data/Gemfile.lock +22 -0
  3. data/INSTALL +166 -0
  4. data/LICENSE +10 -0
  5. data/Makefile +6 -0
  6. data/README +1183 -0
  7. data/Rakefile +37 -0
  8. data/TODO +24 -0
  9. data/TUTORIAL +230 -0
  10. data/VERSION +1 -0
  11. data/bin/rq +902 -0
  12. data/bin/rqmailer +865 -0
  13. data/example/a.rb +7 -0
  14. data/extconf.rb +198 -0
  15. data/gemspec.rb +40 -0
  16. data/install.rb +210 -0
  17. data/lib/rq.rb +155 -0
  18. data/lib/rq/arrayfields.rb +371 -0
  19. data/lib/rq/backer.rb +31 -0
  20. data/lib/rq/configfile.rb +82 -0
  21. data/lib/rq/configurator.rb +40 -0
  22. data/lib/rq/creator.rb +54 -0
  23. data/lib/rq/cron.rb +144 -0
  24. data/lib/rq/defaultconfig.txt +5 -0
  25. data/lib/rq/deleter.rb +51 -0
  26. data/lib/rq/executor.rb +40 -0
  27. data/lib/rq/feeder.rb +527 -0
  28. data/lib/rq/ioviewer.rb +48 -0
  29. data/lib/rq/job.rb +51 -0
  30. data/lib/rq/jobqueue.rb +947 -0
  31. data/lib/rq/jobrunner.rb +110 -0
  32. data/lib/rq/jobrunnerdaemon.rb +193 -0
  33. data/lib/rq/lister.rb +47 -0
  34. data/lib/rq/locker.rb +43 -0
  35. data/lib/rq/lockfile.rb +564 -0
  36. data/lib/rq/logging.rb +124 -0
  37. data/lib/rq/mainhelper.rb +189 -0
  38. data/lib/rq/orderedautohash.rb +39 -0
  39. data/lib/rq/orderedhash.rb +240 -0
  40. data/lib/rq/qdb.rb +733 -0
  41. data/lib/rq/querier.rb +98 -0
  42. data/lib/rq/rails.rb +80 -0
  43. data/lib/rq/recoverer.rb +28 -0
  44. data/lib/rq/refresher.rb +80 -0
  45. data/lib/rq/relayer.rb +283 -0
  46. data/lib/rq/resource.rb +22 -0
  47. data/lib/rq/resourcemanager.rb +40 -0
  48. data/lib/rq/resubmitter.rb +100 -0
  49. data/lib/rq/rotater.rb +98 -0
  50. data/lib/rq/sleepcycle.rb +46 -0
  51. data/lib/rq/snapshotter.rb +40 -0
  52. data/lib/rq/sqlite.rb +286 -0
  53. data/lib/rq/statuslister.rb +48 -0
  54. data/lib/rq/submitter.rb +113 -0
  55. data/lib/rq/toucher.rb +182 -0
  56. data/lib/rq/updater.rb +94 -0
  57. data/lib/rq/usage.rb +1222 -0
  58. data/lib/rq/util.rb +304 -0
  59. data/rdoc.sh +17 -0
  60. data/rq-ruby1.8.gemspec +120 -0
  61. data/test/.gitignore +1 -0
  62. data/test/test_rq.rb +145 -0
  63. data/white_box/crontab +2 -0
  64. data/white_box/joblist +8 -0
  65. data/white_box/killrq +18 -0
  66. data/white_box/rq_killer +27 -0
  67. metadata +208 -0
@@ -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
@@ -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
@@ -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
@@ -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