crazy_ivan 1.2.3 → 1.2.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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