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.
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