thoughtafter-lockfile 2.0.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.
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