lockfile 1.1.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,518 @@
1
+ unless defined? $__lockfile__
2
+ require 'socket'
3
+ require 'timeout'
4
+ require 'fileutils'
5
+
6
+ class Lockfile
7
+ #--{{{
8
+ VERSION = '1.3.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
+ DEFAULT_RETRIES = nil # maximum number of attempts
55
+ DEFAULT_TIMEOUT = nil # the longest we will try
56
+ DEFAULT_MAX_AGE = 1024 # lockfiles older than this are stale
57
+ DEFAULT_SLEEP_INC = 2 # sleep cycle is this much longer each time
58
+ DEFAULT_MIN_SLEEP = 2 # shortest sleep time
59
+ DEFAULT_MAX_SLEEP = 32 # longest sleep time
60
+ DEFAULT_SUSPEND = 64 # iff we steal a lock wait this long before we go on
61
+ DEFAULT_REFRESH = 8 # how often we touch/validate the lock
62
+ DEFAULT_DONT_CLEAN = false # iff we leave lock files lying around
63
+ DEFAULT_POLL_RETRIES = 16 # this many polls makes one 'try'
64
+ DEFAULT_POLL_MAX_SLEEP = 0.08 # the longest we'll sleep between polls
65
+ DEFAULT_DONT_SWEEP = false # if we cleanup after other process on our host
66
+
67
+ DEFAULT_DEBUG = ENV['LOCKFILE_DEBUG'] || false
68
+
69
+ class << self
70
+ #--{{{
71
+ attr :retries, true
72
+ attr :max_age, true
73
+ attr :sleep_inc, true
74
+ attr :min_sleep, true
75
+ attr :max_sleep, true
76
+ attr :suspend, true
77
+ attr :timeout, true
78
+ attr :refresh, true
79
+ attr :debug, true
80
+ attr :dont_clean, true
81
+ attr :poll_retries, true
82
+ attr :poll_max_sleep, true
83
+ attr :dont_sweep, true
84
+
85
+ def init
86
+ #--{{{
87
+ @retries = DEFAULT_RETRIES
88
+ @max_age = DEFAULT_MAX_AGE
89
+ @sleep_inc = DEFAULT_SLEEP_INC
90
+ @min_sleep = DEFAULT_MIN_SLEEP
91
+ @max_sleep = DEFAULT_MAX_SLEEP
92
+ @suspend = DEFAULT_SUSPEND
93
+ @timeout = DEFAULT_TIMEOUT
94
+ @refresh = DEFAULT_REFRESH
95
+ @dont_clean = DEFAULT_DONT_CLEAN
96
+ @poll_retries = DEFAULT_POLL_RETRIES
97
+ @poll_max_sleep = DEFAULT_POLL_MAX_SLEEP
98
+ @dont_sweep = DEFAULT_DONT_SWEEP
99
+
100
+ @debug = DEFAULT_DEBUG
101
+
102
+ STDOUT.sync = true if @debug
103
+ STDERR.sync = true if @debug
104
+ #--}}}
105
+ end
106
+ #--}}}
107
+ end
108
+ self.init
109
+
110
+ attr :klass
111
+ attr :path
112
+ attr :opts
113
+ attr :locked
114
+ attr :thief
115
+ attr :dirname
116
+ attr :basename
117
+ attr :clean
118
+ attr :retries
119
+ attr :max_age
120
+ attr :sleep_inc
121
+ attr :min_sleep
122
+ attr :max_sleep
123
+ attr :suspend
124
+ attr :refresh
125
+ attr :timeout
126
+ attr :dont_clean
127
+ attr :poll_retries
128
+ attr :poll_max_sleep
129
+ attr :dont_sweep
130
+
131
+ attr :debug, true
132
+
133
+ alias thief? thief
134
+ alias locked? locked
135
+ alias debug? debug
136
+
137
+ def initialize(path, opts = {}, &block)
138
+ #--{{{
139
+ @klass = self.class
140
+ @path = path
141
+ @opts = opts
142
+
143
+ @retries = getopt('retries') || @klass.retries
144
+ @max_age = getopt('max_age') || @klass.max_age
145
+ @sleep_inc = getopt('sleep_inc') || @klass.sleep_inc
146
+ @min_sleep = getopt('min_sleep') || @klass.min_sleep
147
+ @max_sleep = getopt('max_sleep') || @klass.max_sleep
148
+ @suspend = getopt('suspend') || @klass.suspend
149
+ @timeout = getopt('timeout') || @klass.timeout
150
+ @refresh = getopt('refresh') || @klass.refresh
151
+ @dont_clean = getopt('dont_clean') || @klass.dont_clean
152
+ @poll_retries = getopt('poll_retries') || @klass.poll_retries
153
+ @poll_max_sleep = getopt('poll_max_sleep') || @klass.poll_max_sleep
154
+ @dont_sweep = getopt('dont_sweep') || @klass.dont_sweep
155
+
156
+ @debug = getopt('debug') || @klass.debug
157
+
158
+ @sleep_cycle = SleepCycle::new @min_sleep, @max_sleep, @sleep_inc
159
+
160
+ @clean = @dont_clean ? nil : lambda{ File::unlink @path rescue nil }
161
+ @dirname = File::dirname @path
162
+ @basename = File::basename @path
163
+ @thief = false
164
+ @locked = false
165
+
166
+ lock(&block) if block
167
+ #--}}}
168
+ end
169
+ def lock
170
+ #--{{{
171
+ raise StackingLockError, "<#{ @path }> is locked!" if @locked
172
+
173
+ sweep unless @dont_sweep
174
+
175
+ ret = nil
176
+
177
+ attempt do
178
+ begin
179
+ @sleep_cycle.reset
180
+ create_tmplock do |f|
181
+ begin
182
+ Timeout::timeout(@timeout) do
183
+ tmp_path = f.path
184
+ tmp_stat = f.lstat
185
+ n_retries = 0
186
+ trace{ "attempting to lock <#{ @path }>..." }
187
+ begin
188
+ i = 0
189
+ begin
190
+ trace{ "polling attempt <#{ i }>..." }
191
+ begin
192
+ File::link tmp_path, @path
193
+ rescue Errno::ENOENT
194
+ try_again!
195
+ end
196
+ lock_stat = File::lstat @path
197
+ raise StatLockError, "stat's do not agree" unless
198
+ tmp_stat.rdev == lock_stat.rdev and tmp_stat.ino == lock_stat.ino
199
+ trace{ "aquired lock <#{ @path }>" }
200
+ @locked = true
201
+ rescue => e
202
+ i += 1
203
+ unless i >= @poll_retries
204
+ t = [rand(@poll_max_sleep), @poll_max_sleep].min
205
+ trace{ "poll sleep <#{ t }>..." }
206
+ sleep t
207
+ retry
208
+ end
209
+ raise
210
+ end
211
+
212
+ rescue => e
213
+ n_retries += 1
214
+ trace{ "n_retries <#{ n_retries }>" }
215
+ case validlock?
216
+ when true
217
+ raise MaxTriesLockError, "surpased retries <#{ @retries }>" if
218
+ @retries and n_retries >= @retries
219
+ trace{ "found valid lock" }
220
+ sleeptime = @sleep_cycle.next
221
+ trace{ "sleep <#{ sleeptime }>..." }
222
+ sleep sleeptime
223
+ when false
224
+ trace{ "found invalid lock and removing" }
225
+ begin
226
+ File::unlink @path
227
+ @thief = true
228
+ warn "<#{ @path }> stolen by <#{ Process.pid }> at <#{ timestamp }>"
229
+ trace{ "i am a thief!" }
230
+ rescue Errno::ENOENT
231
+ end
232
+ trace{ "suspending <#{ @suspend }>" }
233
+ sleep @suspend
234
+ when nil
235
+ raise MaxTriesLockError, "surpased retries <#{ @retries }>" if
236
+ @retries and n_retries >= @retries
237
+ end
238
+ retry
239
+ end # begin
240
+ end # timeout
241
+ rescue Timeout::Error
242
+ raise TimeoutLockError, "surpassed timeout <#{ @timeout }>"
243
+ end # begin
244
+ end # create_tmplock
245
+
246
+ if block_given?
247
+ stolen = false
248
+ refresher = (@refresh ? new_refresher : nil)
249
+ begin
250
+ begin
251
+ ret = yield @path
252
+ rescue StolenLockError
253
+ stolen = true
254
+ raise
255
+ end
256
+ ensure
257
+ begin
258
+ refresher.kill if refresher and refresher.status
259
+ ensure
260
+ unlock unless stolen
261
+ end
262
+ end
263
+ else
264
+ ObjectSpace.define_finalizer self, @clean if @clean
265
+ ret = self
266
+ end
267
+ rescue Errno::ESTALE, Errno::EIO => e
268
+ raise(NFSLockError, errmsg(e))
269
+ end
270
+ end
271
+
272
+ return ret
273
+ #--}}}
274
+ end
275
+ def sweep
276
+ #----{{{
277
+ begin
278
+ glob = File::join(@dirname, ".*lck")
279
+ paths = Dir[glob]
280
+ paths.each do |path|
281
+ begin
282
+ basename = File::basename path
283
+ pat = %r/^\s*\.([^_]+)_([^_]+)/o
284
+ if pat.match(basename)
285
+ host, pid = $1, $2
286
+ else
287
+ next
288
+ end
289
+ host.gsub!(%r/^\.+|\.+$/,'')
290
+ quad = host.split %r/\./
291
+ host = quad.first
292
+ pat = %r/^\s*#{ host }/i
293
+ if pat.match(HOSTNAME) and %r/^\s*\d+\s*$/.match(pid)
294
+ unless alive?(pid)
295
+ trace{ "process <#{ pid }> on <#{ host }> is no longer alive" }
296
+ trace{ "sweeping <#{ path }>" }
297
+ FileUtils::rm_f path
298
+ else
299
+ trace{ "process <#{ pid }> on <#{ host }> is still alive" }
300
+ trace{ "ignoring <#{ path }>" }
301
+ end
302
+ else
303
+ trace{ "ignoring <#{ path }> generated by <#{ host }>" }
304
+ end
305
+ rescue
306
+ next
307
+ end
308
+ end
309
+ rescue => e
310
+ warn(errmsg(e))
311
+ end
312
+ #----}}}
313
+ end
314
+ def alive? pid
315
+ #----{{{
316
+ pid = Integer("#{ pid }")
317
+ begin
318
+ Process::kill 0, pid
319
+ true
320
+ rescue Errno::ESRCH
321
+ false
322
+ end
323
+ #----}}}
324
+ end
325
+ def unlock
326
+ #--{{{
327
+ raise UnLockError, "<#{ @path }> is not locked!" unless @locked
328
+ begin
329
+ File::unlink @path
330
+ rescue Errno::ENOENT
331
+ raise StolenLockError, @path
332
+ ensure
333
+ @thief = false
334
+ @locked = false
335
+ ObjectSpace.undefine_finalizer self if @clean
336
+ end
337
+ #--}}}
338
+ end
339
+ def new_refresher
340
+ #--{{{
341
+ Thread::new(Thread::current, @path, @refresh) do |thread, path, refresh|
342
+ loop do
343
+ touch path
344
+ trace{"touched <#{ path }> @ <#{ Time.now.to_f }>"}
345
+ begin
346
+ loaded = load_lock_id(IO.read(path))
347
+ trace{"loaded <\n#{ loaded.inspect }\n>"}
348
+ raise unless loaded == @lock_id
349
+ rescue => e
350
+ trace{errmsg e}
351
+ thread.raise StolenLockError
352
+ Thread::exit
353
+ end
354
+ sleep refresh
355
+ end
356
+ end
357
+ #--}}}
358
+ end
359
+ def validlock?
360
+ #--{{{
361
+ if @max_age
362
+ uncache @path rescue nil
363
+ begin
364
+ return((Time.now - File::stat(@path).mtime) < @max_age)
365
+ rescue Errno::ENOENT
366
+ return nil
367
+ end
368
+ else
369
+ exist = File::exist?(@path)
370
+ return(exist ? true : nil)
371
+ end
372
+ #--}}}
373
+ end
374
+ def uncache file
375
+ #--{{{
376
+ refresh = nil
377
+ begin
378
+ is_a_file = File === file
379
+ path = (is_a_file ? file.path : file.to_s)
380
+ stat = (is_a_file ? file.stat : File::stat(file.to_s))
381
+ refresh = tmpnam(File::dirname(path))
382
+ File::link path, refresh
383
+ File::chmod stat.mode, path
384
+ File::utime stat.atime, stat.mtime, path
385
+ ensure
386
+ begin
387
+ File::unlink refresh if refresh
388
+ rescue Errno::ENOENT
389
+ end
390
+ end
391
+ #--}}}
392
+ end
393
+ def create_tmplock
394
+ #--{{{
395
+ tmplock = tmpnam @dirname
396
+ begin
397
+ create(tmplock) do |f|
398
+ @lock_id = gen_lock_id
399
+ dumped = dump_lock_id
400
+ trace{"lock_id <\n#{ @lock_id.inspect }\n>"}
401
+ f.write dumped
402
+ f.flush
403
+ yield f
404
+ end
405
+ ensure
406
+ begin; File::unlink tmplock; rescue Errno::ENOENT; end if tmplock
407
+ end
408
+ #--}}}
409
+ end
410
+ def gen_lock_id
411
+ #--{{{
412
+ Hash[
413
+ 'host' => "#{ HOSTNAME }",
414
+ 'pid' => "#{ Process.pid }",
415
+ 'ppid' => "#{ Process.ppid }",
416
+ 'time' => timestamp,
417
+ ]
418
+ #--}}}
419
+ end
420
+ def timestamp
421
+ #--{{{
422
+ time = Time.now
423
+ usec = time.usec.to_s
424
+ usec << '0' while usec.size < 6
425
+ "#{ time.strftime('%Y-%m-%d %H:%M:%S') }.#{ usec }"
426
+ #--}}}
427
+ end
428
+ def dump_lock_id lock_id = @lock_id
429
+ #--{{{
430
+ "host: %s\npid: %s\nppid: %s\ntime: %s\n" %
431
+ lock_id.values_at('host','pid','ppid','time')
432
+ #--}}}
433
+ end
434
+ def load_lock_id buf
435
+ #--{{{
436
+ lock_id = {}
437
+ kv = %r/([^:]+):(.*)/o
438
+ buf.each do |line|
439
+ m = kv.match line
440
+ k, v = m[1], m[2]
441
+ next unless m and k and v
442
+ lock_id[k.strip] = v.strip
443
+ end
444
+ lock_id
445
+ #--}}}
446
+ end
447
+ def tmpnam dir, seed = File::basename($0)
448
+ #--{{{
449
+ pid = Process.pid
450
+ time = Time.now
451
+ sec = time.to_i
452
+ usec = time.usec
453
+ "%s%s.%s_%d_%s_%d_%d_%d.lck" %
454
+ [dir, File::SEPARATOR, HOSTNAME, pid, seed, sec, usec, rand(sec)]
455
+ #--}}}
456
+ end
457
+ def create path
458
+ #--{{{
459
+ umask = nil
460
+ f = nil
461
+ begin
462
+ umask = File::umask 022
463
+ f = open path, File::WRONLY|File::CREAT|File::EXCL, 0644
464
+ ensure
465
+ File::umask umask if umask
466
+ end
467
+ return(block_given? ? begin; yield f; ensure; f.close; end : f)
468
+ #--}}}
469
+ end
470
+ def touch path
471
+ #--{{{
472
+ FileUtils.touch path
473
+ #--}}}
474
+ end
475
+ def getopt key
476
+ #--{{{
477
+ @opts[key] || @opts[key.to_s] || @opts[key.to_s.intern]
478
+ #--}}}
479
+ end
480
+ def to_str
481
+ #--{{{
482
+ @path
483
+ #--}}}
484
+ end
485
+ alias to_s to_str
486
+ def trace s = nil
487
+ #--{{{
488
+ STDERR.puts((s ? s : yield)) if @debug
489
+ #--}}}
490
+ end
491
+ def errmsg e
492
+ #--{{{
493
+ "%s (%s)\n%s\n" % [e.class, e.message, e.backtrace.join("\n")]
494
+ #--}}}
495
+ end
496
+ def attempt
497
+ #----{{{
498
+ ret = nil
499
+ loop{ break unless catch('attempt'){ ret = yield } == 'try_again' }
500
+ ret
501
+ #----}}}
502
+ end
503
+ def try_again!
504
+ #----{{{
505
+ throw 'attempt', 'try_again'
506
+ #----}}}
507
+ end
508
+ alias again! try_again!
509
+ def give_up!
510
+ #----{{{
511
+ throw 'attempt', 'give_up'
512
+ #----}}}
513
+ end
514
+ #--}}}
515
+ end
516
+
517
+ $__lockfile__ == __FILE__
518
+ end