lockfile 1.4.3 → 2.0.1

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.
@@ -0,0 +1,46 @@
1
+ ## lockfile.gemspec
2
+ #
3
+
4
+ Gem::Specification::new do |spec|
5
+ spec.name = "lockfile"
6
+ spec.version = "2.0.1"
7
+ spec.platform = Gem::Platform::RUBY
8
+ spec.summary = "lockfile"
9
+ spec.description = "a ruby library for creating perfect and NFS safe lockfiles"
10
+ spec.license = "same as ruby's"
11
+
12
+ spec.files =
13
+ ["README",
14
+ "bin",
15
+ "bin/rlock",
16
+ "doc",
17
+ "doc/rlock.help",
18
+ "lib",
19
+ "lib/lockfile.rb",
20
+ "lockfile.gemspec",
21
+ "rakefile",
22
+ "readme.erb",
23
+ "samples",
24
+ "samples/a.rb",
25
+ "samples/lock",
26
+ "samples/lock.sh",
27
+ "samples/lockfile",
28
+ "samples/nfsstore.rb",
29
+ "samples/out"]
30
+
31
+ spec.executables = ["rlock"]
32
+
33
+ spec.require_path = "lib"
34
+
35
+ spec.test_files = nil
36
+
37
+ ### spec.add_dependency 'lib', '>= version'
38
+ #### spec.add_dependency 'map'
39
+
40
+ spec.extensions.push(*[])
41
+
42
+ spec.rubyforge_project = "codeforpeople"
43
+ spec.author = "Ara T. Howard"
44
+ spec.email = "ara.t.howard@gmail.com"
45
+ spec.homepage = "https://github.com/ahoward/lockfile"
46
+ end
@@ -0,0 +1,376 @@
1
+ This.rubyforge_project = 'codeforpeople'
2
+ This.author = "Ara T. Howard"
3
+ This.email = "ara.t.howard@gmail.com"
4
+ This.homepage = "https://github.com/ahoward/#{ This.lib }"
5
+
6
+ task :license do
7
+ open('LICENSE', 'w'){|fd| fd.puts "same as ruby's"}
8
+ end
9
+
10
+ task :default do
11
+ puts((Rake::Task.tasks.map{|task| task.name.gsub(/::/,':')} - ['default']).sort)
12
+ end
13
+
14
+ task :test do
15
+ run_tests!
16
+ end
17
+
18
+ namespace :test do
19
+ task(:unit){ run_tests!(:unit) }
20
+ task(:functional){ run_tests!(:functional) }
21
+ task(:integration){ run_tests!(:integration) }
22
+ end
23
+
24
+ def run_tests!(which = nil)
25
+ which ||= '**'
26
+ test_dir = File.join(This.dir, "test")
27
+ test_glob ||= File.join(test_dir, "#{ which }/**_test.rb")
28
+ test_rbs = Dir.glob(test_glob).sort
29
+
30
+ div = ('=' * 119)
31
+ line = ('-' * 119)
32
+
33
+ test_rbs.each_with_index do |test_rb, index|
34
+ testno = index + 1
35
+ command = "#{ This.ruby } -w -I ./lib -I ./test/lib #{ test_rb }"
36
+
37
+ puts
38
+ say(div, :color => :cyan, :bold => true)
39
+ say("@#{ testno } => ", :bold => true, :method => :print)
40
+ say(command, :color => :cyan, :bold => true)
41
+ say(line, :color => :cyan, :bold => true)
42
+
43
+ system(command)
44
+
45
+ say(line, :color => :cyan, :bold => true)
46
+
47
+ status = $?.exitstatus
48
+
49
+ if status.zero?
50
+ say("@#{ testno } <= ", :bold => true, :color => :white, :method => :print)
51
+ say("SUCCESS", :color => :green, :bold => true)
52
+ else
53
+ say("@#{ testno } <= ", :bold => true, :color => :white, :method => :print)
54
+ say("FAILURE", :color => :red, :bold => true)
55
+ end
56
+ say(line, :color => :cyan, :bold => true)
57
+
58
+ exit(status) unless status.zero?
59
+ end
60
+ end
61
+
62
+
63
+ task :gemspec do
64
+ ignore_extensions = ['git', 'svn', 'tmp', /sw./, 'bak', 'gem']
65
+ ignore_directories = ['pkg']
66
+ ignore_files = ['test/log']
67
+
68
+ shiteless =
69
+ lambda do |list|
70
+ list.delete_if do |entry|
71
+ next unless test(?e, entry)
72
+ extension = File.basename(entry).split(%r/[.]/).last
73
+ ignore_extensions.any?{|ext| ext === extension}
74
+ end
75
+ list.delete_if do |entry|
76
+ next unless test(?d, entry)
77
+ dirname = File.expand_path(entry)
78
+ ignore_directories.any?{|dir| File.expand_path(dir) == dirname}
79
+ end
80
+ list.delete_if do |entry|
81
+ next unless test(?f, entry)
82
+ filename = File.expand_path(entry)
83
+ ignore_files.any?{|file| File.expand_path(file) == filename}
84
+ end
85
+ end
86
+
87
+ lib = This.lib
88
+ object = This.object
89
+ version = This.version
90
+ files = shiteless[Dir::glob("**/**")]
91
+ executables = shiteless[Dir::glob("bin/*")].map{|exe| File.basename(exe)}
92
+ #has_rdoc = true #File.exist?('doc')
93
+ test_files = "test/#{ lib }.rb" if File.file?("test/#{ lib }.rb")
94
+ summary = object.respond_to?(:summary) ? object.summary : "summary: #{ lib } kicks the ass"
95
+ description = object.respond_to?(:description) ? object.description : "description: #{ lib } kicks the ass"
96
+ license = object.respond_to?(:license) ? object.license : "same as ruby's"
97
+
98
+ if This.extensions.nil?
99
+ This.extensions = []
100
+ extensions = This.extensions
101
+ %w( Makefile configure extconf.rb ).each do |ext|
102
+ extensions << ext if File.exists?(ext)
103
+ end
104
+ end
105
+ extensions = [extensions].flatten.compact
106
+
107
+ template =
108
+ if test(?e, 'gemspec.erb')
109
+ Template{ IO.read('gemspec.erb') }
110
+ else
111
+ Template {
112
+ <<-__
113
+ ## #{ lib }.gemspec
114
+ #
115
+
116
+ Gem::Specification::new do |spec|
117
+ spec.name = #{ lib.inspect }
118
+ spec.version = #{ version.inspect }
119
+ spec.platform = Gem::Platform::RUBY
120
+ spec.summary = #{ lib.inspect }
121
+ spec.description = #{ description.inspect }
122
+ spec.license = #{ license.inspect }
123
+
124
+ spec.files =\n#{ files.sort.pretty_inspect }
125
+ spec.executables = #{ executables.inspect }
126
+
127
+ spec.require_path = "lib"
128
+
129
+ spec.test_files = #{ test_files.inspect }
130
+
131
+ ### spec.add_dependency 'lib', '>= version'
132
+ #### spec.add_dependency 'map'
133
+
134
+ spec.extensions.push(*#{ extensions.inspect })
135
+
136
+ spec.rubyforge_project = #{ This.rubyforge_project.inspect }
137
+ spec.author = #{ This.author.inspect }
138
+ spec.email = #{ This.email.inspect }
139
+ spec.homepage = #{ This.homepage.inspect }
140
+ end
141
+ __
142
+ }
143
+ end
144
+
145
+ Fu.mkdir_p(This.pkgdir)
146
+ gemspec = "#{ lib }.gemspec"
147
+ open(gemspec, "w"){|fd| fd.puts(template)}
148
+ This.gemspec = gemspec
149
+ end
150
+
151
+ task :gem => [:clean, :gemspec] do
152
+ Fu.mkdir_p(This.pkgdir)
153
+ before = Dir['*.gem']
154
+ cmd = "gem build #{ This.gemspec }"
155
+ `#{ cmd }`
156
+ after = Dir['*.gem']
157
+ gem = ((after - before).first || after.first) or abort('no gem!')
158
+ Fu.mv(gem, This.pkgdir)
159
+ This.gem = File.join(This.pkgdir, File.basename(gem))
160
+ end
161
+
162
+ task :readme do
163
+ samples = ''
164
+ prompt = '~ > '
165
+ lib = This.lib
166
+ version = This.version
167
+
168
+ Dir['sample*/*'].sort.each do |sample|
169
+ samples << "\n" << " <========< #{ sample } >========>" << "\n\n"
170
+
171
+ cmd = "cat #{ sample }"
172
+ samples << Util.indent(prompt + cmd, 2) << "\n\n"
173
+ samples << Util.indent(`#{ cmd }`, 4) << "\n"
174
+
175
+ cmd = "ruby #{ sample }"
176
+ samples << Util.indent(prompt + cmd, 2) << "\n\n"
177
+
178
+ cmd = "ruby -e'STDOUT.sync=true; exec %(ruby -I ./lib #{ sample })'"
179
+ samples << Util.indent(`#{ cmd } 2>&1`, 4) << "\n"
180
+ end
181
+
182
+ template =
183
+ if test(?e, 'README.erb')
184
+ Template{ IO.read('README.erb') }
185
+ else
186
+ Template {
187
+ <<-__
188
+ NAME
189
+ #{ lib }
190
+
191
+ DESCRIPTION
192
+
193
+ INSTALL
194
+ gem install #{ lib }
195
+
196
+ SAMPLES
197
+ #{ samples }
198
+ __
199
+ }
200
+ end
201
+
202
+ open("README", "w"){|fd| fd.puts template}
203
+ end
204
+
205
+
206
+ task :clean do
207
+ Dir[File.join(This.pkgdir, '**/**')].each{|entry| Fu.rm_rf(entry)}
208
+ end
209
+
210
+
211
+ task :release => [:clean, :gemspec, :gem] do
212
+ gems = Dir[File.join(This.pkgdir, '*.gem')].flatten
213
+ raise "which one? : #{ gems.inspect }" if gems.size > 1
214
+ raise "no gems?" if gems.size < 1
215
+
216
+ cmd = "gem push #{ This.gem }"
217
+ puts cmd
218
+ puts
219
+ system(cmd)
220
+ abort("cmd(#{ cmd }) failed with (#{ $?.inspect })") unless $?.exitstatus.zero?
221
+
222
+ cmd = "rubyforge login && rubyforge add_release #{ This.rubyforge_project } #{ This.lib } #{ This.version } #{ This.gem }"
223
+ puts cmd
224
+ puts
225
+ system(cmd)
226
+ abort("cmd(#{ cmd }) failed with (#{ $?.inspect })") unless $?.exitstatus.zero?
227
+ end
228
+
229
+
230
+
231
+
232
+
233
+ BEGIN {
234
+ # support for this rakefile
235
+ #
236
+ $VERBOSE = nil
237
+
238
+ require 'ostruct'
239
+ require 'erb'
240
+ require 'fileutils'
241
+ require 'rbconfig'
242
+ require 'pp'
243
+
244
+ # fu shortcut
245
+ #
246
+ Fu = FileUtils
247
+
248
+ # cache a bunch of stuff about this rakefile/environment
249
+ #
250
+ This = OpenStruct.new
251
+
252
+ This.file = File.expand_path(__FILE__)
253
+ This.dir = File.dirname(This.file)
254
+ This.pkgdir = File.join(This.dir, 'pkg')
255
+
256
+ # grok lib
257
+ #
258
+ lib = ENV['LIB']
259
+ unless lib
260
+ lib = File.basename(Dir.pwd).sub(/[-].*$/, '')
261
+ end
262
+ This.lib = lib
263
+
264
+ # grok version
265
+ #
266
+ version = ENV['VERSION']
267
+ unless version
268
+ require "./lib/#{ This.lib }"
269
+ This.name = lib.capitalize
270
+ This.object = eval(This.name)
271
+ version = This.object.send(:version)
272
+ end
273
+ This.version = version
274
+
275
+ # we need to know the name of the lib an it's version
276
+ #
277
+ abort('no lib') unless This.lib
278
+ abort('no version') unless This.version
279
+
280
+ # discover full path to this ruby executable
281
+ #
282
+ c = Config::CONFIG
283
+ bindir = c["bindir"] || c['BINDIR']
284
+ ruby_install_name = c['ruby_install_name'] || c['RUBY_INSTALL_NAME'] || 'ruby'
285
+ ruby_ext = c['EXEEXT'] || ''
286
+ ruby = File.join(bindir, (ruby_install_name + ruby_ext))
287
+ This.ruby = ruby
288
+
289
+ # some utils
290
+ #
291
+ module Util
292
+ def indent(s, n = 2)
293
+ s = unindent(s)
294
+ ws = ' ' * n
295
+ s.gsub(%r/^/, ws)
296
+ end
297
+
298
+ def unindent(s)
299
+ indent = nil
300
+ s.each_line do |line|
301
+ next if line =~ %r/^\s*$/
302
+ indent = line[%r/^\s*/] and break
303
+ end
304
+ indent ? s.gsub(%r/^#{ indent }/, "") : s
305
+ end
306
+ extend self
307
+ end
308
+
309
+ # template support
310
+ #
311
+ class Template
312
+ def initialize(&block)
313
+ @block = block
314
+ @template = block.call.to_s
315
+ end
316
+ def expand(b=nil)
317
+ ERB.new(Util.unindent(@template)).result((b||@block).binding)
318
+ end
319
+ alias_method 'to_s', 'expand'
320
+ end
321
+ def Template(*args, &block) Template.new(*args, &block) end
322
+
323
+ # colored console output support
324
+ #
325
+ This.ansi = {
326
+ :clear => "\e[0m",
327
+ :reset => "\e[0m",
328
+ :erase_line => "\e[K",
329
+ :erase_char => "\e[P",
330
+ :bold => "\e[1m",
331
+ :dark => "\e[2m",
332
+ :underline => "\e[4m",
333
+ :underscore => "\e[4m",
334
+ :blink => "\e[5m",
335
+ :reverse => "\e[7m",
336
+ :concealed => "\e[8m",
337
+ :black => "\e[30m",
338
+ :red => "\e[31m",
339
+ :green => "\e[32m",
340
+ :yellow => "\e[33m",
341
+ :blue => "\e[34m",
342
+ :magenta => "\e[35m",
343
+ :cyan => "\e[36m",
344
+ :white => "\e[37m",
345
+ :on_black => "\e[40m",
346
+ :on_red => "\e[41m",
347
+ :on_green => "\e[42m",
348
+ :on_yellow => "\e[43m",
349
+ :on_blue => "\e[44m",
350
+ :on_magenta => "\e[45m",
351
+ :on_cyan => "\e[46m",
352
+ :on_white => "\e[47m"
353
+ }
354
+ def say(phrase, *args)
355
+ options = args.last.is_a?(Hash) ? args.pop : {}
356
+ options[:color] = args.shift.to_s.to_sym unless args.empty?
357
+ keys = options.keys
358
+ keys.each{|key| options[key.to_s.to_sym] = options.delete(key)}
359
+
360
+ color = options[:color]
361
+ bold = options.has_key?(:bold)
362
+
363
+ parts = [phrase]
364
+ parts.unshift(This.ansi[color]) if color
365
+ parts.unshift(This.ansi[:bold]) if bold
366
+ parts.push(This.ansi[:clear]) if parts.size > 1
367
+
368
+ method = options[:method] || :puts
369
+
370
+ Kernel.send(method, parts.join)
371
+ end
372
+
373
+ # always run out of the project dir
374
+ #
375
+ Dir.chdir(This.dir)
376
+ }
@@ -0,0 +1,227 @@
1
+ URLS
2
+
3
+ http://rubyforge.org/projects/codeforpeople/
4
+ http://codeforpeople.com/lib/ruby/
5
+
6
+ SYNOPSIS
7
+
8
+ lib/lockfile.rb : a ruby library for creating NFS safe lockfiles
9
+
10
+ bin/rlock : ruby command line tool which uses this library to create lockfiles
11
+ and to run arbitrary commands while holding them.
12
+
13
+ for example
14
+
15
+ rlock lockfile -- cp -r huge/ huge.bak/
16
+
17
+ run 'rlock -h' for more info
18
+
19
+ INSTALL
20
+
21
+ sudo ruby install.rb
22
+
23
+
24
+ BASIC ALGORITHIM
25
+
26
+ * create a globally uniq filename in the same filesystem as the desired
27
+ lockfile - this can be nfs mounted
28
+
29
+ * link(2) this file to the desired lockfile, ignore all errors
30
+
31
+ * stat the uniq filename and desired lockfile to determine is they are the
32
+ same, use only stat.rdev and stat.ino - ignore stat.nlink as NFS can cause
33
+ this report incorrect values
34
+
35
+ * iff same, you have lock. either return or run optional code block with
36
+ optional refresher thread keeping lockfile fresh during execution of code
37
+ block, ensuring that the lockfile is removed..
38
+
39
+ * iff not same try again a few times in rapid succession (poll), then, iff
40
+ this fails, sleep using incremental backoff time. optionally remove
41
+ lockfile if it is older than a certain time, timeout if more than a certain
42
+ amount of time has passed attempting to lock file.
43
+
44
+
45
+ BASIC USAGE
46
+
47
+ 1)
48
+ lockfile = Lockfile.new 'file.lock'
49
+ begin
50
+ lockfile.lock
51
+ p 42
52
+ ensure
53
+ lockfile.unlock
54
+ end
55
+
56
+
57
+ 2)
58
+ require 'pstore' # which is NOT nfs safe on it's own
59
+
60
+ opts = { # the keys can be symbols or strings
61
+
62
+ :retries => nil, # we will try forever to aquire the lock
63
+
64
+ :sleep_inc => 2, # we will sleep 2 seconds longer than the
65
+ # previous sleep after each retry, cycling from
66
+ # min_sleep upto max_sleep downto min_sleep upto
67
+ # max_sleep, etc., etc.
68
+
69
+ :min_sleep => 2, # we will never sleep less than 2 seconds
70
+
71
+ :max_sleep => 32, # we will never sleep longer than 32 seconds
72
+
73
+ :max_age => 3600, # we will blow away any files found to be older
74
+ # than this (lockfile.thief? #=> true)
75
+
76
+ :suspend => 1800, # iff we steal the lock from someone else - wait
77
+ # this long to give them a chance to realize it
78
+
79
+ :refresh => 8, # we will spawn a bg thread that touches file
80
+ # every 8 sec. this thread also causes a
81
+ # StolenLockError to be thrown if the lock
82
+ # disappears from under us - note that the
83
+ # 'detection' rate is limited to the refresh
84
+ # interval - this is a race condition
85
+
86
+ :timeout => nil, # we will wait forever
87
+
88
+ :poll_retries => 16, # the initial attempt to grab a lock is done in a
89
+ # polling fashion, this number controls how many
90
+ # times this is done - the total polling attempts
91
+ # are considered ONE actual attempt (see retries
92
+ # above)
93
+
94
+ :poll_max_sleep => 0.08, # when polling a very brief sleep is issued
95
+ # between attempts, this is the upper limit of
96
+ # that sleep timeout
97
+
98
+ :dont_clean => false, # normally a finalizer is defined to clean up
99
+ # after lockfiles, settin this to true prevents this
100
+
101
+ :dont_sweep => false, # normally locking causes a sweep to be made. a
102
+ # sweep removes any old tmp files created by
103
+ # processes of this host only which are no
104
+ # longer alive
105
+
106
+ :debug => true, # trace execution step on stdout
107
+ }
108
+
109
+ pstore = PStore.new 'file.db'
110
+ lockfile = Lockfile.new 'file.db.lock', opts
111
+ lockfile.lock do
112
+ pstore.transaction do
113
+ pstore[:last_update_time] = Time.now
114
+ end
115
+ end
116
+
117
+ 3) same as 1 above - Lockfile.new takes a block and ensures unlock is called
118
+
119
+ Lockfile.new('file.lock') do
120
+ p 42
121
+ end
122
+
123
+ 4) watch locking algorithim in action (debugging only)
124
+
125
+ Lockfile.debug = true
126
+ Lockfile.new('file.lock') do
127
+ p 42
128
+ end
129
+
130
+ you can also set debugging via the ENV var LOCKFILE_DEBUG, eg.
131
+
132
+ ~ > LOCKFILE_DEBUG=true rlock lockfile
133
+
134
+ 5) simplified interface : no lockfile object required
135
+
136
+ Lockfile('lock', :retries => 0) do
137
+ puts 'only one instance running!'
138
+ end
139
+
140
+
141
+ SAMPLES
142
+
143
+ * see samples/a.rb
144
+ * see samples/nfsstore.rb
145
+ * see samples/lock.sh
146
+ * see bin/rlock
147
+
148
+ AUTHOR
149
+
150
+ Ara T. Howard
151
+
152
+ EMAIL
153
+
154
+ Ara.T.Howard@gmail.com
155
+
156
+ BUGS
157
+
158
+ bugno > 1 && bugno < 42
159
+
160
+ HISTORY
161
+
162
+ 1.4.3:
163
+ - fixed a small non-critical bug in the require gaurd
164
+
165
+ 1.4.2:
166
+ - upped defaults for max_age to 3600 and suspend to 1800.
167
+ - tweaked a few things to play nice with rubygems
168
+
169
+ 1.4.1:
170
+ - Mike Kasick <mkasick@club.cc.cmu.edu> reported a bug whereby false/nil
171
+ values for options were ignored. patched to address this bug
172
+ - added Lockfile method for high level interface sans lockfile object
173
+ - updated rlock program to allow nil/true/false values passed on command
174
+ line. eg
175
+
176
+ rlock --max_age=nil lockfile -- date --iso-8601=seconds
177
+
178
+ 1.4.0:
179
+ - gem'd up
180
+ - added Lockfile::create method which atomically creates a file and opens
181
+ it:
182
+
183
+ Lockfile::create("atomic_even_on_nfs", "r+") do |f|
184
+ f.puts 42
185
+ end
186
+
187
+ arguments are the same as those for File::open. note that there is no way
188
+ to accomplish this otherwise since File::O_EXCL fails silently on nfs,
189
+ flock does not work on nfs, and posixlock (see raa) can only lock a file
190
+ after it is open so the open itself is still a race.
191
+
192
+ 1.3.0:
193
+ - added sweep functionality. hosts can clean up any tmp files left around
194
+ by other processes that may have been killed using -9 to prevent proper
195
+ clean up. it is only possible to clean up after processes on the same
196
+ host as there is no other way to determine if the process that created the
197
+ file is alive. in summary - hosts clean up after other processes on that
198
+ same host if needed.
199
+ - added attempt/try_again methods
200
+ 1.2.0:
201
+ - fixed bug where stale locks not removed when retries == 0
202
+ 1.1.0
203
+ - unfortunately i've forgotten
204
+ 1.0.1:
205
+ - fixed bugette in sleep cycle where repeated locks with same lockfile would
206
+ not reset the cycle at the start of each lock
207
+ 1.0.0:
208
+ - allow rertries to be nil, meaning to try forever
209
+ - default timeout is now nil - never timeout
210
+ - default refresh is now 8
211
+ - fixed bug where refresher thread was not actually touching lockfile! (ouch)
212
+ - added cycle method to timeouts
213
+ 1-2-3-2-1-2-3-1...
214
+ pattern is constructed using min_sleep, sleep_inc, max_sleep
215
+ 0.3.0:
216
+ - removed use of yaml in favour of hand parsing the lockfile contents, the
217
+ file as so small it just wasn't worth and i have had one core dump when yaml
218
+ failed to parse a (corrupt) file
219
+ 0.2.0:
220
+ - added an initial polling style attempt to grab lock before entering normal
221
+ sleep/retry loop. this has really helped performance when lock is under
222
+ heavy contention: i see almost no sleeping done by any of in the interested
223
+ processes
224
+ 0.1.0:
225
+ - added ability of Lockfile.new to accept a block
226
+ 0.0.0:
227
+ - initial version