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.
@@ -1,437 +0,0 @@
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
data/lockfile-1.1.0.gem DELETED
File without changes
data/lockfile.gemspec DELETED
@@ -1,22 +0,0 @@
1
- require 'date'
2
- Gem::Specification.new do |s|
3
- s.name = %q{lockfile}
4
- s.version = "1.1.0"
5
- s.date = Date.today.to_s
6
- s.summary = %q{A ruby library for creating NFS safe lockfiles}
7
- s.description =<<DESCRIPTION
8
- A ruby library for creating NFS safe lockfiles
9
- DESCRIPTION
10
- s.author = %q{-a}
11
- s.email = %q{Ara.T.Howard@noaa.gov}
12
- s.homepage = %q{http://www.codeforpeople.com/lib/ruby/lockfile/}
13
- s.files = Dir.glob('**/*')
14
- s.require_paths = %w{. lib}
15
- s.autorequire = %q{lockfile}
16
- s.has_rdoc = true
17
- s.rdoc_options = ["--main", "README"]
18
- s.extra_rdoc_files = ["README", "doc/rlock.help"]
19
- s.executables = %w{rlock}
20
- s.bindir = %q{bin}
21
- s.platform = Gem::Platform::RUBY
22
- end