lockfile 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/doc/rlock.help ADDED
@@ -0,0 +1,88 @@
1
+ = NAME
2
+ rlock v0.0.0
3
+
4
+ = SYNOPSIS
5
+ rlock [options]+ file.lock [program [-- [options]+] [args]+]
6
+
7
+ = DESCRIPTTION
8
+ rlock creates NFS resistent lockfiles
9
+
10
+ = ENVIRONMENT
11
+ LOCKFILE_DEBUG=1 will show internal actions of the library
12
+
13
+ = DIAGNOSTICS
14
+ success => $? == 0
15
+ failure => $? != 0
16
+
17
+ = AUTHOR
18
+ ara.t.howard@noaa.gov
19
+
20
+ = BUGS
21
+ > 1
22
+
23
+ = OPTIONS
24
+
25
+ -r, --retries=n
26
+ -a, --max_age=n
27
+ -s, --sleep_inc=n
28
+ -p, --max_sleep=n
29
+ -u, --suspend=n
30
+ -t, --timeout=n
31
+ -f, --refresh=n
32
+ -d, --debug
33
+ -v=0-4|debug|info|warn|error|fatal
34
+ --verbosity
35
+ -l, --log=path
36
+ --log_age=log_age
37
+ --log_size=log_size
38
+ -h, --help
39
+
40
+ = EXAMPLES
41
+
42
+ 0) simple usage - just create a file.lock in an atomic fashion
43
+
44
+ ~ > rlock file.lock
45
+
46
+ 1) safe usage - create a file.lock, execute a command, and remove file.lock
47
+
48
+ ~ > rlock file.lock ls file.lock
49
+
50
+ 2) same as above, but logging verbose messages
51
+
52
+ ~ > rlock -v4 file.lock ls file.lock
53
+
54
+ 3) same as above, but logging verbose messages and showing actions internal to
55
+ lockfile library
56
+
57
+ ~ > rlock -v4 -d file.lock ls file.lock
58
+
59
+ 4) same as above
60
+
61
+ ~ > LOCKFILE_DEBUG=1 rlock -v4 file.lock ls file.lock
62
+
63
+ 5) same as above
64
+
65
+ ~ > export LOCKFILE_DEBUG=1
66
+ ~ > rlock -v4 file.lock ls file.lock
67
+
68
+ 6) note that you need to tell the option parser to stop parsing rlock options if
69
+ you intend to pass options to 'program'
70
+
71
+ ~ > rlock -v4 -d file.lock -- ls -ltar file.lock
72
+
73
+ without the '--' rlock would consume the '-ltar' options, parsing it
74
+ as the logfile name 'tar'
75
+
76
+ 7) lock file.lock and exec 'program' - remove the file.lock if it is older than 4242 seconds
77
+
78
+ ~ > rlock --max_age=4242 file.lock program
79
+
80
+ 8) lock file.lock and exec 'program' - remove the file.lock if it is older
81
+ than 4242 seconds, also spawn a background thread which will refresh
82
+ file.lock every 8 seonds will 'program' is executing
83
+
84
+ ~ > rlock --max_age=4242 --refresh=8 file.lock program
85
+
86
+ 9) same as above, but fail if file.lock cannot be obtained within 1 minute
87
+
88
+ ~ > rlock --max_age=4242 --refresh=8 --timeout=60 file.lock program
data/install.rb ADDED
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rbconfig'
3
+ require 'find'
4
+ require 'ftools'
5
+ require 'tempfile'
6
+ include Config
7
+
8
+ LIBDIR = "lib"
9
+ LIBDIR_MODE = 0644
10
+
11
+ BINDIR = "bin"
12
+ BINDIR_MODE = 0755
13
+
14
+
15
+ $srcdir = CONFIG["srcdir"]
16
+ $version = CONFIG["MAJOR"]+"."+CONFIG["MINOR"]
17
+ $libdir = File.join(CONFIG["libdir"], "ruby", $version)
18
+ $archdir = File.join($libdir, CONFIG["arch"])
19
+ $site_libdir = $:.find {|x| x =~ /site_ruby$/}
20
+ $bindir = CONFIG["bindir"]
21
+ $ruby_install_name = CONFIG['ruby_install_name'] || CONFIG['RUBY_INSTALL_NAME']
22
+ $ruby = File.join($bindir, $ruby_install_name || 'ruby')
23
+
24
+ if !$site_libdir
25
+ $site_libdir = File.join($libdir, "site_ruby")
26
+ elsif $site_libdir !~ %r/#{Regexp.quote($version)}/
27
+ $site_libdir = File.join($site_libdir, $version)
28
+ end
29
+
30
+ def install_rb(srcdir=nil, destdir=nil, mode=nil, bin=nil)
31
+ #{{{
32
+ path = []
33
+ dir = []
34
+ Find.find(srcdir) do |f|
35
+ next unless FileTest.file?(f)
36
+ next if (f = f[srcdir.length+1..-1]) == nil
37
+ next if (/CVS$/ =~ File.dirname(f))
38
+ path.push f
39
+ dir |= [File.dirname(f)]
40
+ end
41
+ for f in dir
42
+ next if f == "."
43
+ next if f == "CVS"
44
+ File::makedirs(File.join(destdir, f))
45
+ end
46
+ for f in path
47
+ next if (/\~$/ =~ f)
48
+ next if (/^\./ =~ File.basename(f))
49
+ unless bin
50
+ File::install(File.join(srcdir, f), File.join(destdir, f), mode, true)
51
+ else
52
+ from = File.join(srcdir, f)
53
+ to = File.join(destdir, f)
54
+ shebangify(from) do |sf|
55
+ $deferr.print from, " -> ", File::catname(from, to), "\n"
56
+ $deferr.printf "chmod %04o %s\n", mode, to
57
+ File::install(sf, to, mode, false)
58
+ end
59
+ end
60
+ end
61
+ #}}}
62
+ end
63
+ def shebangify f
64
+ #{{{
65
+ open(f) do |fd|
66
+ buf = fd.read 42
67
+ if buf =~ %r/^\s*#\s*!.*ruby/o
68
+ ftmp = Tempfile::new("#{ $$ }_#{ File::basename(f) }")
69
+ begin
70
+ fd.rewind
71
+ ftmp.puts "#!#{ $ruby }"
72
+ while((buf = fd.read(8192)))
73
+ ftmp.write buf
74
+ end
75
+ ftmp.close
76
+ yield ftmp.path
77
+ ensure
78
+ ftmp.close!
79
+ end
80
+ else
81
+ yield f
82
+ end
83
+ end
84
+ #}}}
85
+ end
86
+ def ARGV.switch
87
+ #{{{
88
+ return nil if self.empty?
89
+ arg = self.shift
90
+ return nil if arg == '--'
91
+ if arg =~ /^-(.)(.*)/
92
+ return arg if $1 == '-'
93
+ raise 'unknown switch "-"' if $2.index('-')
94
+ self.unshift "-#{$2}" if $2.size > 0
95
+ "-#{$1}"
96
+ else
97
+ self.unshift arg
98
+ nil
99
+ end
100
+ #}}}
101
+ end
102
+ def ARGV.req_arg
103
+ #{{{
104
+ self.shift || raise('missing argument')
105
+ #}}}
106
+ end
107
+
108
+
109
+ #
110
+ # main program
111
+ #
112
+
113
+ libdir = $site_libdir
114
+ bindir = $bindir
115
+
116
+ begin
117
+ while switch = ARGV.switch
118
+ case switch
119
+ when '-d', '--destdir'
120
+ libdir = ARGV.req_arg
121
+ when '-l', '--libdir'
122
+ libdir = ARGV.req_arg
123
+ when '-b', '--bindir'
124
+ bindir = ARGV.req_arg
125
+ when '-r', '--ruby'
126
+ $ruby = ARGV.req_arg
127
+ else
128
+ raise "unknown switch #{switch.dump}"
129
+ end
130
+ end
131
+ rescue
132
+ STDERR.puts $!.to_s
133
+ STDERR.puts File.basename($0) +
134
+ " -d <destdir>" +
135
+ " -l <libdir>" +
136
+ " -b <bindir>"
137
+ " -r <ruby>"
138
+ exit 1
139
+ end
140
+
141
+ install_rb(LIBDIR, libdir, LIBDIR_MODE)
142
+ install_rb(BINDIR, bindir, BINDIR_MODE, bin=true)
143
+
@@ -0,0 +1,437 @@
1
+ unless defined? $__lockfile__
2
+ require 'socket'
3
+ require 'timeout'
4
+ require 'fileutils'
5
+
6
+ class Lockfile
7
+ #{{{
8
+ VERSION = '1.1.0'
9
+
10
+ class LockError < StandardError; end
11
+ class StolenLockError < LockError; end
12
+ class StackingLockError < LockError; end
13
+ class StatLockError < LockError; end
14
+ class MaxTriesLockError < LockError; end
15
+ class TimeoutLockError < LockError; end
16
+ class NFSLockError < LockError; end
17
+ class UnLockError < LockError; end
18
+
19
+ class SleepCycle < Array
20
+ #{{{
21
+ attr :min
22
+ attr :max
23
+ attr :range
24
+ attr :inc
25
+ def initialize min, max, inc
26
+ #{{{
27
+ @min, @max, @inc = Float(min), Float(max), Float(inc)
28
+ @range = @max - @min
29
+ raise RangeError, "max < min" if @max < @min
30
+ raise RangeError, "inc > range" if @inc > @range
31
+ s = @min
32
+ push(s) and s += @inc while(s <= @max)
33
+ self[-1] = @max if self[-1] < @max
34
+ reset
35
+ #}}}
36
+ end
37
+ def next
38
+ #{{{
39
+ ret = self[@idx]
40
+ @idx = ((@idx + 1) % self.size)
41
+ ret
42
+ #}}}
43
+ end
44
+ def reset
45
+ #{{{
46
+ @idx = 0
47
+ #}}}
48
+ end
49
+ #}}}
50
+ end
51
+
52
+ HOSTNAME = Socket::gethostname
53
+
54
+ RETRIES = nil # maximum number of attempts
55
+ TIMEOUT = nil # the longest we will try
56
+ MAX_AGE = 1024 # lockfiles older than this are stale
57
+ SLEEP_INC = 2 # sleep cycle is this much longer each time
58
+ MIN_SLEEP = 2 # shortest sleep time
59
+ MAX_SLEEP = 32 # longest sleep time
60
+ SUSPEND = 64 # iff we steal a lock wait this long before we go on
61
+ REFRESH = 8 # how often we touch/validate the lock
62
+ DONT_CLEAN = false # iff we leave lock files lying around
63
+ POLL_RETRIES = 16 # this many polls makes one 'try'
64
+ POLL_MAX_SLEEP = 0.08 # the longest we'll sleep between polls
65
+
66
+ DEBUG = ENV['LOCKFILE_DEBUG'] || false
67
+
68
+ class << self
69
+ #{{{
70
+ attr :retries, true
71
+ attr :max_age, true
72
+ attr :sleep_inc, true
73
+ attr :min_sleep, true
74
+ attr :max_sleep, true
75
+ attr :suspend, true
76
+ attr :timeout, true
77
+ attr :refresh, true
78
+ attr :debug, true
79
+ attr :dont_clean, true
80
+ attr :poll_retries, true
81
+ attr :poll_max_sleep, true
82
+ def init
83
+ #{{{
84
+ @retries = RETRIES
85
+ @max_age = MAX_AGE
86
+ @sleep_inc = SLEEP_INC
87
+ @min_sleep = MIN_SLEEP
88
+ @max_sleep = MAX_SLEEP
89
+ @suspend = SUSPEND
90
+ @timeout = TIMEOUT
91
+ @refresh = REFRESH
92
+ @debug = DEBUG
93
+ @dont_clean = DONT_CLEAN
94
+ @poll_retries = POLL_RETRIES
95
+ @poll_max_sleep = POLL_MAX_SLEEP
96
+
97
+ STDOUT.sync = true if @debug
98
+ STDERR.sync = true if @debug
99
+ #}}}
100
+ end
101
+ #}}}
102
+ end
103
+ self.init
104
+
105
+ attr :klass
106
+ attr :path
107
+ attr :opts
108
+ attr :locked
109
+ attr :thief
110
+ attr :dirname
111
+ attr :basename
112
+ attr :clean
113
+ attr :retries, true
114
+ attr :max_age, true
115
+ attr :sleep_inc, true
116
+ attr :min_sleep, true
117
+ attr :max_sleep, true
118
+ attr :suspend, true
119
+ attr :refresh, true
120
+ attr :timeout, true
121
+ attr :debug, true
122
+ attr :dont_clean, true
123
+ attr :poll_retries, true
124
+ attr :poll_max_sleep, true
125
+
126
+ alias thief? thief
127
+ alias locked? locked
128
+ alias debug? debug
129
+
130
+ def initialize(path, opts = {}, &block)
131
+ #{{{
132
+ @klass = self.class
133
+ @path = path
134
+ @opts = opts
135
+
136
+ @retries = getopt('retries') || @klass.retries
137
+ @max_age = getopt('max_age') || @klass.max_age
138
+ @sleep_inc = getopt('sleep_inc') || @klass.sleep_inc
139
+ @min_sleep = getopt('min_sleep') || @klass.min_sleep
140
+ @max_sleep = getopt('max_sleep') || @klass.max_sleep
141
+ @suspend = getopt('suspend') || @klass.suspend
142
+ @timeout = getopt('timeout') || @klass.timeout
143
+ @refresh = getopt('refresh') || @klass.refresh
144
+ @debug = getopt('debug') || @klass.debug
145
+ @dont_clean = getopt('dont_clean') || @klass.dont_clean
146
+ @poll_retries = getopt('poll_retries') || @klass.poll_retries
147
+ @poll_max_sleep = getopt('poll_max_sleep') || @klass.poll_max_sleep
148
+
149
+ @sleep_cycle = SleepCycle.new @min_sleep, @max_sleep, @sleep_inc
150
+
151
+ @clean = @dont_clean ? nil : lambda{File.unlink @path rescue nil}
152
+ @dirname = File.dirname @path
153
+ @basename = File.basename @path
154
+ @thief = false
155
+ @locked = false
156
+
157
+ lock(&block) if block
158
+ #}}}
159
+ end
160
+ def lock
161
+ #{{{
162
+ raise StackingLockError, "<#{ @path }> is locked!" if @locked
163
+
164
+ ret = nil
165
+
166
+ begin
167
+ @sleep_cycle.reset
168
+ create_tmplock do |f|
169
+ begin
170
+ Timeout::timeout(@timeout) do
171
+ tmp_path = f.path
172
+ tmp_stat = f.lstat
173
+ n_retries = 0
174
+ #sleeptime = @sleep_inc
175
+
176
+ trace{ "attempting to lock <#{ @path }>..." }
177
+ begin
178
+ i = 0
179
+ begin
180
+ trace{ "polling attempt <#{ i }>..." }
181
+ File.link tmp_path, @path
182
+ lock_stat = File.lstat @path
183
+ raise StatLockError, "stat's do not agree" unless
184
+ tmp_stat.rdev == lock_stat.rdev and tmp_stat.ino == lock_stat.ino
185
+ trace{ "aquired lock <#{ @path }>" }
186
+ @locked = true
187
+ rescue => e
188
+ i += 1
189
+ unless i >= @poll_retries
190
+ t = [rand(@poll_max_sleep), @poll_max_sleep].min
191
+ trace{ "poll sleep <#{ t }>..." }
192
+ sleep t
193
+ retry
194
+ end
195
+ raise
196
+ end
197
+
198
+ rescue => e
199
+ n_retries += 1
200
+ trace{ "n_retries <#{ n_retries }>" }
201
+ raise MaxTriesLockError, "surpased retries <#{ @retries }>" if
202
+ @retries and n_retries >= @retries
203
+
204
+ valid = validlock?
205
+
206
+ case valid
207
+ when true
208
+ trace{ "found valid lock" }
209
+ sleeptime = @sleep_cycle.next
210
+ trace{ "sleep <#{ sleeptime }>..." }
211
+ sleep sleeptime
212
+ when false
213
+ trace{ "found invalid lock and removing" }
214
+ begin
215
+ File.unlink @path
216
+ @thief = true
217
+ warn "<#{ @path }> stolen by <#{ Process.pid }> at <#{ timestamp }>"
218
+ trace{ "i am a thief!" }
219
+ trace{ "suspending <#{ @suspend }>" }
220
+ sleep @suspend
221
+ rescue Errno::ENOENT
222
+ end
223
+ when nil
224
+ # nothing
225
+ end
226
+
227
+ retry
228
+ end # begin
229
+ end # timeout
230
+ rescue Timeout::Error
231
+ raise TimeoutLockError, "surpassed timeout <#{ @timeout }>"
232
+ end # begin
233
+ end # create_tmplock
234
+
235
+ if block_given?
236
+ stolen = false
237
+ refresher = (@refresh ? new_refresher : nil)
238
+ begin
239
+ begin
240
+ ret = yield @path
241
+ rescue StolenLockError
242
+ stolen = true
243
+ raise
244
+ end
245
+ ensure
246
+ begin
247
+ refresher.kill if refresher and refresher.status
248
+ ensure
249
+ unlock unless stolen
250
+ end
251
+ end
252
+ else
253
+ ObjectSpace.define_finalizer self, @clean if @clean
254
+ ret = self
255
+ end
256
+ rescue Errno::ESTALE, Errno::EIO => e
257
+ raise(NFSLockError, errmsg(e))
258
+ end
259
+
260
+ return ret
261
+ #}}}
262
+ end
263
+ def unlock
264
+ #{{{
265
+ raise UnLockError, "<#{ @path }> is not locked!" unless @locked
266
+ begin
267
+ File.unlink @path
268
+ @locked = false
269
+ ObjectSpace.undefine_finalizer self if @clean
270
+ rescue Errno::ENOENT
271
+ @locked = false
272
+ ObjectSpace.undefine_finalizer self if @clean
273
+ raise StolenLockError, @path
274
+ end
275
+ #}}}
276
+ end
277
+ def new_refresher
278
+ #{{{
279
+ Thread.new(Thread.current, @path, @refresh) do |thread, path, refresh|
280
+ loop do
281
+ touch path
282
+ trace{"touched <#{ path }> @ <#{ Time.now.to_f }>"}
283
+ begin
284
+ loaded = load_lock_id(IO.read(path))
285
+ trace{"loaded <\n#{ loaded.inspect }\n>"}
286
+ raise unless loaded == @lock_id
287
+ rescue => e
288
+ trace{errmsg e}
289
+ thread.raise StolenLockError
290
+ Thread.exit
291
+ end
292
+ sleep refresh
293
+ end
294
+ end
295
+ #}}}
296
+ end
297
+ def validlock?
298
+ #{{{
299
+ if @max_age
300
+ uncache @path rescue nil
301
+ begin
302
+ return((Time.now - File.stat(@path).mtime) < @max_age)
303
+ rescue Errno::ENOENT
304
+ return nil
305
+ end
306
+ else
307
+ exist = File.exist?(@path)
308
+ return(exist ? true : nil)
309
+ end
310
+ #}}}
311
+ end
312
+ def uncache file
313
+ #{{{
314
+ refresh = nil
315
+ begin
316
+ is_a_file = File === file
317
+ path = (is_a_file ? file.path : file.to_s)
318
+ stat = (is_a_file ? file.stat : File.stat(file.to_s))
319
+ refresh = tmpnam(File.dirname(path))
320
+ File.link path, refresh
321
+ File.chmod stat.mode, path
322
+ File.utime stat.atime, stat.mtime, path
323
+ ensure
324
+ begin
325
+ File.unlink refresh if refresh
326
+ rescue Errno::ENOENT
327
+ end
328
+ end
329
+ #}}}
330
+ end
331
+ def create_tmplock
332
+ #{{{
333
+ tmplock = tmpnam @dirname
334
+ begin
335
+ create(tmplock) do |f|
336
+ @lock_id = gen_lock_id
337
+ dumped = dump_lock_id
338
+ trace{"lock_id <\n#{ @lock_id.inspect }\n>"}
339
+ f.write dumped
340
+ f.flush
341
+ yield f
342
+ end
343
+ ensure
344
+ begin; File.unlink tmplock; rescue Errno::ENOENT; end if tmplock
345
+ end
346
+ #}}}
347
+ end
348
+ def gen_lock_id
349
+ #{{{
350
+ Hash[
351
+ 'host' => "#{ HOSTNAME }",
352
+ 'pid' => "#{ Process.pid }",
353
+ 'ppid' => "#{ Process.ppid }",
354
+ 'time' => timestamp,
355
+ ]
356
+ #}}}
357
+ end
358
+ def timestamp
359
+ #{{{
360
+ time = Time.now
361
+ usec = time.usec.to_s
362
+ usec << '0' while usec.size < 6
363
+ "#{ time.strftime('%Y-%m-%d %H:%M:%S') }.#{ usec }"
364
+ #}}}
365
+ end
366
+ def dump_lock_id lock_id = @lock_id
367
+ #{{{
368
+ "host: %s\npid: %s\nppid: %s\ntime: %s\n" %
369
+ lock_id.values_at('host','pid','ppid','time')
370
+ #}}}
371
+ end
372
+ def load_lock_id buf
373
+ #{{{
374
+ lock_id = {}
375
+ kv = %r/([^:]+):(.*)/o
376
+ buf.each do |line|
377
+ m = kv.match line
378
+ k, v = m[1], m[2]
379
+ next unless m and k and v
380
+ lock_id[k.strip] = v.strip
381
+ end
382
+ lock_id
383
+ #}}}
384
+ end
385
+ def tmpnam dir, seed = File.basename($0)
386
+ #{{{
387
+ pid = Process.pid
388
+ time = Time.now
389
+ sec = time.to_i
390
+ usec = time.usec
391
+ "%s%s.%s_%d_%s_%d_%d_%d" %
392
+ [dir, File::SEPARATOR, HOSTNAME, pid, seed, sec, usec, rand(sec)]
393
+ #}}}
394
+ end
395
+ def create path
396
+ #{{{
397
+ umask = nil
398
+ f = nil
399
+ begin
400
+ umask = File.umask 022
401
+ f = open path, File::WRONLY|File::CREAT|File::EXCL, 0644
402
+ ensure
403
+ File.umask umask if umask
404
+ end
405
+ return(block_given? ? begin; yield f; ensure; f.close; end : f)
406
+ #}}}
407
+ end
408
+ def touch path
409
+ #{{{
410
+ FileUtils.touch path
411
+ #}}}
412
+ end
413
+ def getopt key
414
+ #{{{
415
+ @opts[key] || @opts[key.to_s] || @opts[key.to_s.intern]
416
+ #}}}
417
+ end
418
+ def to_str
419
+ #{{{
420
+ @path
421
+ #}}}
422
+ end
423
+ alias to_s to_str
424
+ def trace s = nil
425
+ #{{{
426
+ STDERR.puts((s ? s : yield)) if @debug
427
+ #}}}
428
+ end
429
+ def errmsg e
430
+ #{{{
431
+ "%s: %s\n%s\n" % [e.class, e.message, e.backtrace.join("\n")]
432
+ #}}}
433
+ end
434
+ #}}}
435
+ end
436
+ $__lockfile__ == __FILE__
437
+ end