lockfile 1.1.0 → 1.3.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/lib/lockfile.rb CHANGED
@@ -4,8 +4,8 @@ unless defined? $__lockfile__
4
4
  require 'fileutils'
5
5
 
6
6
  class Lockfile
7
- #{{{
8
- VERSION = '1.1.0'
7
+ #--{{{
8
+ VERSION = '1.3.0'
9
9
 
10
10
  class LockError < StandardError; end
11
11
  class StolenLockError < LockError; end
@@ -17,13 +17,13 @@ unless defined? $__lockfile__
17
17
  class UnLockError < LockError; end
18
18
 
19
19
  class SleepCycle < Array
20
- #{{{
20
+ #--{{{
21
21
  attr :min
22
22
  attr :max
23
23
  attr :range
24
24
  attr :inc
25
25
  def initialize min, max, inc
26
- #{{{
26
+ #--{{{
27
27
  @min, @max, @inc = Float(min), Float(max), Float(inc)
28
28
  @range = @max - @min
29
29
  raise RangeError, "max < min" if @max < @min
@@ -32,41 +32,42 @@ unless defined? $__lockfile__
32
32
  push(s) and s += @inc while(s <= @max)
33
33
  self[-1] = @max if self[-1] < @max
34
34
  reset
35
- #}}}
35
+ #--}}}
36
36
  end
37
37
  def next
38
- #{{{
38
+ #--{{{
39
39
  ret = self[@idx]
40
40
  @idx = ((@idx + 1) % self.size)
41
41
  ret
42
- #}}}
42
+ #--}}}
43
43
  end
44
44
  def reset
45
- #{{{
45
+ #--{{{
46
46
  @idx = 0
47
- #}}}
47
+ #--}}}
48
48
  end
49
- #}}}
49
+ #--}}}
50
50
  end
51
51
 
52
52
  HOSTNAME = Socket::gethostname
53
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
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
65
66
 
66
- DEBUG = ENV['LOCKFILE_DEBUG'] || false
67
+ DEFAULT_DEBUG = ENV['LOCKFILE_DEBUG'] || false
67
68
 
68
69
  class << self
69
- #{{{
70
+ #--{{{
70
71
  attr :retries, true
71
72
  attr :max_age, true
72
73
  attr :sleep_inc, true
@@ -79,26 +80,30 @@ unless defined? $__lockfile__
79
80
  attr :dont_clean, true
80
81
  attr :poll_retries, true
81
82
  attr :poll_max_sleep, true
83
+ attr :dont_sweep, true
84
+
82
85
  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
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
96
101
 
97
102
  STDOUT.sync = true if @debug
98
103
  STDERR.sync = true if @debug
99
- #}}}
104
+ #--}}}
100
105
  end
101
- #}}}
106
+ #--}}}
102
107
  end
103
108
  self.init
104
109
 
@@ -110,173 +115,230 @@ unless defined? $__lockfile__
110
115
  attr :dirname
111
116
  attr :basename
112
117
  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
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
+
121
131
  attr :debug, true
122
- attr :dont_clean, true
123
- attr :poll_retries, true
124
- attr :poll_max_sleep, true
125
132
 
126
133
  alias thief? thief
127
134
  alias locked? locked
128
135
  alias debug? debug
129
136
 
130
137
  def initialize(path, opts = {}, &block)
131
- #{{{
138
+ #--{{{
132
139
  @klass = self.class
133
- @path = path
134
- @opts = opts
140
+ @path = path
141
+ @opts = opts
135
142
 
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
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
147
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
148
157
 
149
- @sleep_cycle = SleepCycle.new @min_sleep, @max_sleep, @sleep_inc
158
+ @sleep_cycle = SleepCycle::new @min_sleep, @max_sleep, @sleep_inc
150
159
 
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
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
156
165
 
157
166
  lock(&block) if block
158
- #}}}
167
+ #--}}}
159
168
  end
160
169
  def lock
161
- #{{{
170
+ #--{{{
162
171
  raise StackingLockError, "<#{ @path }> is locked!" if @locked
163
172
 
164
- ret = nil
173
+ sweep unless @dont_sweep
165
174
 
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
+ ret = nil
175
176
 
176
- trace{ "attempting to lock <#{ @path }>..." }
177
- begin
178
- i = 0
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 }>..." }
179
187
  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" }
188
+ i = 0
189
+ begin
190
+ trace{ "polling attempt <#{ i }>..." }
214
191
  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
192
+ File::link tmp_path, @path
221
193
  rescue Errno::ENOENT
194
+ try_again!
222
195
  end
223
- when nil
224
- # nothing
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
225
210
  end
226
211
 
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
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
234
245
 
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
+ if block_given?
247
+ stolen = false
248
+ refresher = (@refresh ? new_refresher : nil)
246
249
  begin
247
- refresher.kill if refresher and refresher.status
250
+ begin
251
+ ret = yield @path
252
+ rescue StolenLockError
253
+ stolen = true
254
+ raise
255
+ end
248
256
  ensure
249
- unlock unless stolen
257
+ begin
258
+ refresher.kill if refresher and refresher.status
259
+ ensure
260
+ unlock unless stolen
261
+ end
250
262
  end
263
+ else
264
+ ObjectSpace.define_finalizer self, @clean if @clean
265
+ ret = self
251
266
  end
252
- else
253
- ObjectSpace.define_finalizer self, @clean if @clean
254
- ret = self
267
+ rescue Errno::ESTALE, Errno::EIO => e
268
+ raise(NFSLockError, errmsg(e))
255
269
  end
256
- rescue Errno::ESTALE, Errno::EIO => e
257
- raise(NFSLockError, errmsg(e))
258
270
  end
259
271
 
260
272
  return ret
261
- #}}}
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
+ #----}}}
262
324
  end
263
325
  def unlock
264
- #{{{
326
+ #--{{{
265
327
  raise UnLockError, "<#{ @path }> is not locked!" unless @locked
266
328
  begin
267
- File.unlink @path
268
- @locked = false
269
- ObjectSpace.undefine_finalizer self if @clean
329
+ File::unlink @path
270
330
  rescue Errno::ENOENT
331
+ raise StolenLockError, @path
332
+ ensure
333
+ @thief = false
271
334
  @locked = false
272
335
  ObjectSpace.undefine_finalizer self if @clean
273
- raise StolenLockError, @path
274
336
  end
275
- #}}}
337
+ #--}}}
276
338
  end
277
339
  def new_refresher
278
- #{{{
279
- Thread.new(Thread.current, @path, @refresh) do |thread, path, refresh|
340
+ #--{{{
341
+ Thread::new(Thread::current, @path, @refresh) do |thread, path, refresh|
280
342
  loop do
281
343
  touch path
282
344
  trace{"touched <#{ path }> @ <#{ Time.now.to_f }>"}
@@ -287,49 +349,49 @@ unless defined? $__lockfile__
287
349
  rescue => e
288
350
  trace{errmsg e}
289
351
  thread.raise StolenLockError
290
- Thread.exit
352
+ Thread::exit
291
353
  end
292
354
  sleep refresh
293
355
  end
294
356
  end
295
- #}}}
357
+ #--}}}
296
358
  end
297
359
  def validlock?
298
- #{{{
360
+ #--{{{
299
361
  if @max_age
300
362
  uncache @path rescue nil
301
363
  begin
302
- return((Time.now - File.stat(@path).mtime) < @max_age)
364
+ return((Time.now - File::stat(@path).mtime) < @max_age)
303
365
  rescue Errno::ENOENT
304
366
  return nil
305
367
  end
306
368
  else
307
- exist = File.exist?(@path)
369
+ exist = File::exist?(@path)
308
370
  return(exist ? true : nil)
309
371
  end
310
- #}}}
372
+ #--}}}
311
373
  end
312
374
  def uncache file
313
- #{{{
375
+ #--{{{
314
376
  refresh = nil
315
377
  begin
316
378
  is_a_file = File === file
317
379
  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
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
323
385
  ensure
324
386
  begin
325
- File.unlink refresh if refresh
387
+ File::unlink refresh if refresh
326
388
  rescue Errno::ENOENT
327
389
  end
328
390
  end
329
- #}}}
391
+ #--}}}
330
392
  end
331
393
  def create_tmplock
332
- #{{{
394
+ #--{{{
333
395
  tmplock = tmpnam @dirname
334
396
  begin
335
397
  create(tmplock) do |f|
@@ -341,36 +403,36 @@ unless defined? $__lockfile__
341
403
  yield f
342
404
  end
343
405
  ensure
344
- begin; File.unlink tmplock; rescue Errno::ENOENT; end if tmplock
406
+ begin; File::unlink tmplock; rescue Errno::ENOENT; end if tmplock
345
407
  end
346
- #}}}
408
+ #--}}}
347
409
  end
348
410
  def gen_lock_id
349
- #{{{
411
+ #--{{{
350
412
  Hash[
351
413
  'host' => "#{ HOSTNAME }",
352
414
  'pid' => "#{ Process.pid }",
353
415
  'ppid' => "#{ Process.ppid }",
354
416
  'time' => timestamp,
355
417
  ]
356
- #}}}
418
+ #--}}}
357
419
  end
358
420
  def timestamp
359
- #{{{
421
+ #--{{{
360
422
  time = Time.now
361
423
  usec = time.usec.to_s
362
424
  usec << '0' while usec.size < 6
363
425
  "#{ time.strftime('%Y-%m-%d %H:%M:%S') }.#{ usec }"
364
- #}}}
426
+ #--}}}
365
427
  end
366
428
  def dump_lock_id lock_id = @lock_id
367
- #{{{
429
+ #--{{{
368
430
  "host: %s\npid: %s\nppid: %s\ntime: %s\n" %
369
431
  lock_id.values_at('host','pid','ppid','time')
370
- #}}}
432
+ #--}}}
371
433
  end
372
434
  def load_lock_id buf
373
- #{{{
435
+ #--{{{
374
436
  lock_id = {}
375
437
  kv = %r/([^:]+):(.*)/o
376
438
  buf.each do |line|
@@ -380,58 +442,77 @@ unless defined? $__lockfile__
380
442
  lock_id[k.strip] = v.strip
381
443
  end
382
444
  lock_id
383
- #}}}
445
+ #--}}}
384
446
  end
385
- def tmpnam dir, seed = File.basename($0)
386
- #{{{
447
+ def tmpnam dir, seed = File::basename($0)
448
+ #--{{{
387
449
  pid = Process.pid
388
450
  time = Time.now
389
451
  sec = time.to_i
390
452
  usec = time.usec
391
- "%s%s.%s_%d_%s_%d_%d_%d" %
453
+ "%s%s.%s_%d_%s_%d_%d_%d.lck" %
392
454
  [dir, File::SEPARATOR, HOSTNAME, pid, seed, sec, usec, rand(sec)]
393
- #}}}
455
+ #--}}}
394
456
  end
395
457
  def create path
396
- #{{{
458
+ #--{{{
397
459
  umask = nil
398
460
  f = nil
399
461
  begin
400
- umask = File.umask 022
462
+ umask = File::umask 022
401
463
  f = open path, File::WRONLY|File::CREAT|File::EXCL, 0644
402
464
  ensure
403
- File.umask umask if umask
465
+ File::umask umask if umask
404
466
  end
405
467
  return(block_given? ? begin; yield f; ensure; f.close; end : f)
406
- #}}}
468
+ #--}}}
407
469
  end
408
470
  def touch path
409
- #{{{
471
+ #--{{{
410
472
  FileUtils.touch path
411
- #}}}
473
+ #--}}}
412
474
  end
413
475
  def getopt key
414
- #{{{
476
+ #--{{{
415
477
  @opts[key] || @opts[key.to_s] || @opts[key.to_s.intern]
416
- #}}}
478
+ #--}}}
417
479
  end
418
480
  def to_str
419
- #{{{
481
+ #--{{{
420
482
  @path
421
- #}}}
483
+ #--}}}
422
484
  end
423
485
  alias to_s to_str
424
486
  def trace s = nil
425
- #{{{
487
+ #--{{{
426
488
  STDERR.puts((s ? s : yield)) if @debug
427
- #}}}
489
+ #--}}}
428
490
  end
429
491
  def errmsg e
430
- #{{{
431
- "%s: %s\n%s\n" % [e.class, e.message, e.backtrace.join("\n")]
432
- #}}}
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
+ #----}}}
433
502
  end
434
- #}}}
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
+ #--}}}
435
515
  end
516
+
436
517
  $__lockfile__ == __FILE__
437
518
  end