thoughtafter-lockfile 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/rakefile ADDED
@@ -0,0 +1,228 @@
1
+
2
+ This.rubyforge_project = 'codeforpeople'
3
+ This.author = "Ara T. Howard"
4
+ This.email = "ara.t.howard@gmail.com"
5
+ This.homepage = "http://github.com/ahoward/#{ This.lib }/tree/master"
6
+
7
+
8
+ task :default do
9
+ puts(Rake::Task.tasks.map{|task| task.name} - ['default'])
10
+ end
11
+
12
+
13
+ task :gemspec do
14
+ ignore_extensions = 'git', 'svn', 'tmp', /sw./, 'bak', 'gem'
15
+ ignore_directories = 'pkg'
16
+ ignore_files = 'test/log'
17
+
18
+ shiteless =
19
+ lambda do |list|
20
+ list.delete_if do |entry|
21
+ next unless test(?e, entry)
22
+ extension = File.basename(entry).split(%r/[.]/).last
23
+ ignore_extensions.any?{|ext| ext === extension}
24
+ end
25
+ list.delete_if do |entry|
26
+ next unless test(?d, entry)
27
+ dirname = File.expand_path(entry)
28
+ ignore_directories.any?{|dir| File.expand_path(dir) == dirname}
29
+ end
30
+ list.delete_if do |entry|
31
+ next unless test(?f, entry)
32
+ filename = File.expand_path(entry)
33
+ ignore_files.any?{|file| File.expand_path(file) == filename}
34
+ end
35
+ end
36
+
37
+ lib = This.lib
38
+ version = This.version
39
+ files = shiteless[Dir::glob("**/**")]
40
+ executables = shiteless[Dir::glob("bin/*")].map{|exe| File.basename(exe)}
41
+ has_rdoc = true #File.exist?('doc')
42
+ test_files = "test/#{ lib }.rb" if File.file?("test/#{ lib }.rb")
43
+
44
+ extensions = This.extensions
45
+ if extensions.nil?
46
+ %w( Makefile configure extconf.rb ).each do |ext|
47
+ extensions << ext if File.exists?(ext)
48
+ end
49
+ end
50
+ extensions = [extensions].flatten.compact
51
+
52
+ template =
53
+ if test(?e, 'gemspec.erb')
54
+ Template{ IO.read('gemspec.erb') }
55
+ else
56
+ Template {
57
+ <<-__
58
+ ## #{ lib }.gemspec
59
+ #
60
+
61
+ Gem::Specification::new do |spec|
62
+ spec.name = #{ lib.inspect }
63
+ spec.version = #{ version.inspect }
64
+ spec.platform = Gem::Platform::RUBY
65
+ spec.summary = #{ lib.inspect }
66
+
67
+ spec.files = #{ files.inspect }
68
+ spec.executables = #{ executables.inspect }
69
+
70
+ spec.require_path = "lib"
71
+
72
+ spec.has_rdoc = #{ has_rdoc.inspect }
73
+ spec.test_files = #{ test_files.inspect }
74
+ #spec.add_dependency 'lib', '>= version'
75
+ #spec.add_dependency 'fattr'
76
+
77
+ spec.extensions.push(*#{ extensions.inspect })
78
+
79
+ spec.rubyforge_project = #{ This.rubyforge_project.inspect }
80
+ spec.author = #{ This.author.inspect }
81
+ spec.email = #{ This.email.inspect }
82
+ spec.homepage = #{ This.homepage.inspect }
83
+ end
84
+ __
85
+ }
86
+ end
87
+
88
+ open("#{ lib }.gemspec", "w"){|fd| fd.puts template}
89
+ This.gemspec = "#{ lib }.gemspec"
90
+ end
91
+
92
+ task :gem => [:clean, :gemspec] do
93
+ Fu.mkdir_p This.pkgdir
94
+ before = Dir['*.gem']
95
+ cmd = "gem build #{ This.gemspec }"
96
+ `#{ cmd }`
97
+ after = Dir['*.gem']
98
+ gem = ((after - before).first || after.first) or abort('no gem!')
99
+ Fu.mv gem, This.pkgdir
100
+ This.gem = File.basename(gem)
101
+ end
102
+
103
+ task :readme do
104
+ samples = ''
105
+ prompt = '~ > '
106
+ lib = This.lib
107
+ version = This.version
108
+
109
+ Dir['sample*/*'].sort.each do |sample|
110
+ samples << "\n" << " <========< #{ sample } >========>" << "\n\n"
111
+
112
+ cmd = "cat #{ sample }"
113
+ samples << Util.indent(prompt + cmd, 2) << "\n\n"
114
+ samples << Util.indent(`#{ cmd }`, 4) << "\n"
115
+
116
+ cmd = "ruby #{ sample }"
117
+ samples << Util.indent(prompt + cmd, 2) << "\n\n"
118
+
119
+ cmd = "ruby -e'STDOUT.sync=true; exec %(ruby -Ilib #{ sample })'"
120
+ samples << Util.indent(`#{ cmd } 2>&1`, 4) << "\n"
121
+ end
122
+
123
+ template =
124
+ if test(?e, 'readme.erb')
125
+ Template{ IO.read('readme.erb') }
126
+ else
127
+ Template {
128
+ <<-__
129
+ NAME
130
+ #{ lib }
131
+
132
+ DESCRIPTION
133
+
134
+ INSTALL
135
+ gem install #{ lib }
136
+
137
+ SAMPLES
138
+ #{ samples }
139
+ __
140
+ }
141
+ end
142
+
143
+ open("README", "w"){|fd| fd.puts template}
144
+ end
145
+
146
+
147
+ task :clean do
148
+ Dir[File.join(This.pkgdir, '**/**')].each{|entry| Fu.rm_rf(entry)}
149
+ end
150
+
151
+
152
+ task :release => [:clean, :gemspec, :gem] do
153
+ gems = Dir[File.join(This.pkgdir, '*.gem')].flatten
154
+ raise "which one? : #{ gems.inspect }" if gems.size > 1
155
+ raise "no gems?" if gems.size < 1
156
+ cmd = "rubyforge login && rubyforge add_release #{ This.rubyforge_project } #{ This.lib } #{ This.version } #{ This.pkgdir }/#{ This.gem }"
157
+ puts cmd
158
+ system cmd
159
+ end
160
+
161
+
162
+
163
+
164
+
165
+ BEGIN {
166
+ $VERBOSE = nil
167
+
168
+ require 'ostruct'
169
+ require 'erb'
170
+ require 'fileutils'
171
+
172
+ Fu = FileUtils
173
+
174
+ This = OpenStruct.new
175
+
176
+ This.file = File.expand_path(__FILE__)
177
+ This.dir = File.dirname(This.file)
178
+ This.pkgdir = File.join(This.dir, 'pkg')
179
+
180
+ lib = ENV['LIB']
181
+ unless lib
182
+ lib = File.basename(Dir.pwd)
183
+ end
184
+ This.lib = lib
185
+
186
+ version = ENV['VERSION']
187
+ unless version
188
+ name = lib.capitalize
189
+ require "./lib/#{ lib }"
190
+ version = eval(name).send(:version)
191
+ end
192
+ This.version = version
193
+
194
+ abort('no lib') unless This.lib
195
+ abort('no version') unless This.version
196
+
197
+ module Util
198
+ def indent(s, n = 2)
199
+ s = unindent(s)
200
+ ws = ' ' * n
201
+ s.gsub(%r/^/, ws)
202
+ end
203
+
204
+ def unindent(s)
205
+ indent = nil
206
+ s.each do |line|
207
+ next if line =~ %r/^\s*$/
208
+ indent = line[%r/^\s*/] and break
209
+ end
210
+ indent ? s.gsub(%r/^#{ indent }/, "") : s
211
+ end
212
+ extend self
213
+ end
214
+
215
+ class Template
216
+ def initialize(&block)
217
+ @block = block
218
+ @template = block.call.to_s
219
+ end
220
+ def expand(b=nil)
221
+ ERB.new(Util.unindent(@template)).result(b||@block)
222
+ end
223
+ alias_method 'to_s', 'expand'
224
+ end
225
+ def Template(*args, &block) Template.new(*args, &block) end
226
+
227
+ Dir.chdir(This.dir)
228
+ }
data/readme.erb ADDED
@@ -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