lockfile 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/lockfile.rb ADDED
@@ -0,0 +1,437 @@
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
File without changes
data/lockfile.gemspec ADDED
@@ -0,0 +1,22 @@
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