lockfile 1.1.0 → 1.3.0

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