lockfile 1.4.3 → 2.0.1

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