lockfile 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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