lockfile 1.1.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/BUGS ADDED
@@ -0,0 +1,3 @@
1
+ none known
2
+
3
+ email Ara.T.Howard@noaa.gov with any reports please!
data/README ADDED
@@ -0,0 +1,165 @@
1
+ = URLS
2
+
3
+ http://raa.ruby-lang.org/project/lockfile/
4
+ http://www.codeforpeople.com/lib/ruby/lockfile/
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
11
+ lockfiles. run 'rlock -h' for more info
12
+
13
+ = INSTALL
14
+
15
+ sudo ruby install.rb
16
+
17
+
18
+ = BASIC ALGORITHIM
19
+
20
+ * create a globally uniq filename in the same filesystem as the desired
21
+ lockfile - this can be nfs mounted
22
+
23
+ * link(2) this file to the desired lockfile, ignore all errors
24
+
25
+ * stat the uniq filename and desired lockfile to determine is they are the
26
+ same, use only stat.rdev and stat.ino - ignore stat.nlink as NFS can cause
27
+ this report incorrect values
28
+
29
+ * iff same, you have lock. either return or run optional code block with
30
+ optional refresher thread keeping lockfile fresh during execution of code
31
+ block, ensuring that the lockfile is removed..
32
+
33
+ * iff not same try again a few times in rapid succession (poll), then, iff
34
+ this fails, sleep using incremental backoff time. optionally remove
35
+ lockfile if it is older than a certain time, timeout if more than a certain
36
+ amount of time has passed attempting to lock file.
37
+
38
+
39
+ = BASIC USAGE
40
+
41
+ 1)
42
+ lockfile = Lockfile.new 'file.lock'
43
+ begin
44
+ lockfile.lock
45
+ p 42
46
+ ensure
47
+ lockfile.unlock
48
+ end
49
+
50
+
51
+ 2)
52
+ require 'pstore' # which is NOT nfs safe on it's own
53
+
54
+ opts = { # the keys can be symbols or strings
55
+
56
+ :retries => nil, # we will try forever to aquire the lock
57
+
58
+ :sleep_inc => 2, # we will sleep 2 seconds longer than the
59
+ # previous sleep after each retry, cycling from
60
+ # min_sleep upto max_sleep downto min_sleep upto
61
+ # max_sleep, etc., etc.
62
+
63
+ :min_sleep => 2, # we will never sleep less than 2 seconds
64
+
65
+ :max_sleep => 32, # we will never sleep longer than 32 seconds
66
+
67
+ :max_age => 1024, # we will blow away any files found to be older
68
+ # than this (lockfile.thief? #=> true)
69
+
70
+ :suspend => 64, # iff we steal the lock from someone else - wait
71
+ # this long to give them a chance to realize it
72
+
73
+ :refresh => 8, # we will spawn a bg thread that touches file
74
+ # every 8 sec. this thread also causes a
75
+ # StolenLockError to be thrown if the lock
76
+ # disappears from under us - note that the
77
+ # 'detection' rate is limited to the refresh
78
+ # interval - this is a race condition
79
+
80
+ :timeout => nil, # we will wait forever
81
+
82
+ :poll_retries => 16, # the initial attempt to grab a lock is done in a
83
+ # polling fashion, this number controls how many
84
+ # times this is done - the total polling attempts
85
+ # are considered ONE actual attempt (see retries
86
+ # above)
87
+
88
+ :poll_max_sleep => 0.08, # when polling a very brief sleep is issued
89
+ # between attempts, this is the upper limit of
90
+ # that sleep timeout
91
+
92
+ :debug => true, # trace execution step on stdout
93
+ }
94
+
95
+ pstore = PStore.new 'file.db'
96
+ lockfile = Lockfile.new 'file.db.lock', opts
97
+ lockfile.lock do
98
+ pstore.transaction do
99
+ pstore[:last_update_time] = Time.now
100
+ end
101
+ end
102
+
103
+ 3) same as 1 above - Lockfile.new takes a block and ensures unlock is called
104
+
105
+ Lockfile.new('file.lock') do
106
+ p 42
107
+ end
108
+
109
+ 4) watch locking algorithim in action
110
+
111
+ Lockfile.debug = true
112
+ Lockfile.new('file.lock') do
113
+ p 42
114
+ end
115
+
116
+ you can also set debugging via the ENV var LOCKFILE_DEBUG, eg.
117
+
118
+ ~ > LOCKFILE_DEBUG=true rlock lockfile
119
+
120
+ = SAMPLES
121
+
122
+ * see samples/a.rb
123
+ * see samples/nfsstore.rb
124
+ * see bin/rlock
125
+
126
+ = AUTHOR
127
+
128
+ -a
129
+
130
+ = EMAIL
131
+
132
+ Ara.T.Howard@noaa.gov
133
+
134
+ = BUGS
135
+
136
+ > 1
137
+
138
+ further notifications to email above
139
+
140
+ = HISTORY
141
+
142
+ 1.0.1:
143
+ - fixed bugette in sleep cycle where repeated locks with same lockfile would
144
+ not reset the cycle at the start of each lock
145
+ 1.0.0:
146
+ - allow rertries to be nil, meaning to try forever
147
+ - default timeout is now nil - never timeout
148
+ - default refresh is now 8
149
+ - fixed bug where refresher thread was not actually touching lockfile! (ouch)
150
+ - added cycle method to timeouts
151
+ 1-2-3-2-1-2-3-1...
152
+ pattern is constructed using min_sleep, sleep_inc, max_sleep
153
+ 0.3.0:
154
+ - removed use of yaml in favour of hand parsing the lockfile contents, the
155
+ file as so small it just wasn't worth and i have had one core dump when yaml
156
+ failed to parse a (corrupt) file
157
+ 0.2.0:
158
+ - added an initial polling style attempt to grab lock before entering normal
159
+ sleep/retry loop. this has really helped performance when lock is under
160
+ heavy contention: i see almost no sleeping done by any of in the interested
161
+ processes
162
+ 0.1.0:
163
+ - added ability of Lockfile.new to accept a block
164
+ 0.0.0:
165
+ - initial version
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.1.0
data/bin/rlock ADDED
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #
4
+ # builtin
5
+ #
6
+ require 'rbconfig'
7
+ major, minor, teeny = %w(MAJOR MINOR TEENY).map{|k| Config::CONFIG[k].to_i}
8
+ unless major >= 1 and minor >= 8
9
+ STDERR.puts "ruby-1.8.0 or greater is required, you are running <ruby-#{ major }.#{ minor }.#{ teeny }>"
10
+ STDERR.puts "check your PATH setting or install ruby-1.8.0 or greater"
11
+ exit 1
12
+ end
13
+ require 'optparse'
14
+ require 'logger'
15
+ require 'yaml'
16
+ require 'pp'
17
+
18
+ #
19
+ # RAA
20
+ #
21
+ require 'lockfile.rb'
22
+
23
+ class Main
24
+ #{{{
25
+ VERSION = '0.0.0'
26
+ PROGNAM = File.basename(File.expand_path($0))
27
+
28
+ USAGE =
29
+ #{{{
30
+ <<-usage
31
+ NAME
32
+ #{ PROGNAM } v#{ VERSION }
33
+
34
+ SYNOPSIS
35
+ #{ PROGNAM } [options]+ file.lock [program [-- [options]+] [args]+]
36
+
37
+ DESCRIPTTION
38
+ #{ PROGNAM } creates NFS resistent lockfiles
39
+
40
+ ENVIRONMENT
41
+ LOCKFILE_DEBUG=1 will show internal actions of the library
42
+
43
+ DIAGNOSTICS
44
+ success => $? == 0
45
+ failure => $? != 0
46
+
47
+ AUTHOR
48
+ ara.t.howard@noaa.gov
49
+
50
+ BUGS
51
+ > 1
52
+
53
+ OPTIONS
54
+ usage
55
+ #}}}
56
+
57
+ EXAMPLES =
58
+ #{{{
59
+ <<-examples
60
+ EXAMPLES
61
+
62
+ 0) simple usage - just create a file.lock in an atomic fashion
63
+
64
+ ~ > #{ PROGNAM } file.lock
65
+
66
+ 1) safe usage - create a file.lock, execute a command, and remove file.lock
67
+
68
+ ~ > #{ PROGNAM } file.lock ls file.lock
69
+
70
+ 2) same as above, but logging verbose messages
71
+
72
+ ~ > #{ PROGNAM } -v4 file.lock ls file.lock
73
+
74
+ 3) same as above, but logging verbose messages and showing actions internal to
75
+ lockfile library
76
+
77
+ ~ > #{ PROGNAM } -v4 -d file.lock ls file.lock
78
+
79
+ 4) same as above
80
+
81
+ ~ > LOCKFILE_DEBUG=1 #{ PROGNAM } -v4 file.lock ls file.lock
82
+
83
+ 5) same as above
84
+
85
+ ~ > export LOCKFILE_DEBUG=1
86
+ ~ > #{ PROGNAM } -v4 file.lock ls file.lock
87
+
88
+ 6) note that you need to tell the option parser to stop parsing #{ PROGNAM }
89
+ options if you intend to pass options to 'program'
90
+
91
+ ~ > #{ PROGNAM } -v4 -d file.lock -- ls -ltar file.lock
92
+
93
+ without the '--' #{ PROGNAM } would consume the '-ltar' options, parsing it
94
+ as the logfile name 'tar'
95
+
96
+ 7) lock file.lock and exec 'program' - remove the file.lock if it is older
97
+ than 4242 seconds
98
+
99
+ ~ > #{ PROGNAM } --max_age=4242 file.lock program
100
+
101
+ 8) lock file.lock and exec 'program' - remove the file.lock if it is older
102
+ than 4242 seconds, also spawn a background thread which will refresh
103
+ file.lock every 8 seonds will 'program' is executing
104
+
105
+ ~ > #{ PROGNAM } --max_age=4242 --refresh=8 file.lock program
106
+
107
+ 9) same as above, but fail if file.lock cannot be obtained within 1 minute
108
+
109
+ ~ > #{ PROGNAM } --max_age=4242 --refresh=8 --timeout=60 file.lock program
110
+
111
+ examples
112
+ #}}}
113
+
114
+ EXIT_SUCCESS = 0
115
+ EXIT_FAILURE = 1
116
+
117
+ attr :argv
118
+ attr :op
119
+ attr :logger
120
+ attr :config
121
+
122
+ def initialize argv = cp(ARGV)
123
+ #{{{
124
+ @argv = argv
125
+ parse_opts
126
+ parse_argv
127
+ #}}}
128
+ end
129
+ def run
130
+ #{{{
131
+ usage and exit EXIT_SUCCESS if @opt_help
132
+ init_logging
133
+
134
+ debug{ "lockpath <#{ @lockpath }>" }
135
+
136
+ opts = {}
137
+ options =
138
+ %w(retries max_age sleep_inc min_sleep max_sleep suspend timeout refresh poll_retries poll_max_sleep)
139
+ options.each do |opt|
140
+ if((val = eval("opt_#{ opt }")))
141
+ begin
142
+ opts[opt] = Integer val
143
+ logger.debug{ "<#{ opt }> <#{ val }>" }
144
+ rescue
145
+ logger.fatal{ "illegal value <#{ val.inspect }> for opt <#{ opt }>" }
146
+ exit EXIT_FAILURE
147
+ end
148
+ end
149
+ end
150
+
151
+ opts['debug'] = true if opt_debug
152
+
153
+ begin
154
+ case @argv.size
155
+ when 0
156
+ opts['dont_clean'] = true
157
+ logger.debug{ "opts <#{ pretty opts }>" }
158
+ logger.debug{ "aquiring lock <#{ @lockpath }>..." }
159
+ #
160
+ # simple usage - just create the lockfile with opts
161
+ #
162
+ lockfile = ::Lockfile.new @lockpath, opts
163
+ lockfile.lock
164
+
165
+ logger.debug{ "aquired lock <#{ @lockpath }>" }
166
+ else
167
+ logger.debug{ "opts <#{ pretty opts }>" }
168
+ logger.debug{ "aquiring lock <#{ @lockpath }>..." }
169
+ #
170
+ # block usage - create the lockfile with opts, run block, rm lockfile
171
+ #
172
+ lockfile = ::Lockfile.new @lockpath, opts
173
+ lockfile.lock do
174
+ logger.debug{ "aquired lock <#{ @lockpath }>" }
175
+ cmd = @argv.join ' '
176
+ logger.debug{ "cmd <#{ cmd }>" }
177
+ v = nil
178
+ begin
179
+ v = $VERBOSE
180
+ $VERBOSE = nil
181
+ system cmd
182
+ ensure
183
+ $VERBOSE = v
184
+ end
185
+ logger.debug{ "status <#{ $? }>" }
186
+ end
187
+ exit $?
188
+ end
189
+ rescue => e
190
+ logger.fatal{ e }
191
+ exit EXIT_FAILURE
192
+ end
193
+
194
+ exit EXIT_SUCCESS
195
+ #}}}
196
+ end
197
+ def parse_opts
198
+ #{{{
199
+ @op = OptionParser.new
200
+ @op.banner = ''
201
+ define_options
202
+ #begin
203
+ @op.parse! argv
204
+ #rescue OptionParser::InvalidOption => e
205
+ # preverve unknown options
206
+ #e.recover(argv)
207
+ #rescue Exception => e
208
+ #STDERR.puts PROGNAM
209
+ #STDERR.puts @op
210
+ #exit 2
211
+ #end
212
+
213
+ #}}}
214
+ end
215
+ def parse_argv
216
+ #{{{
217
+ usage and exit EXIT_FAILURE if @argv.empty?
218
+ @lockpath = @argv.shift
219
+ #}}}
220
+ end
221
+ def usage io = STDOUT
222
+ #{{{
223
+ io << USAGE
224
+ io << "\n"
225
+ io << @op
226
+ io << "\n"
227
+ io << EXAMPLES if defined? EXAMPLES
228
+ self
229
+ #}}}
230
+ end
231
+ def define_options
232
+ #{{{
233
+ options = [
234
+ ['--retries=n','-r', "default(#{ Lockfile.retries.inspect }) - (nil => forever)"],
235
+ ['--max_age=n','-a', "default(#{ Lockfile.max_age.inspect })"],
236
+ ['--sleep_inc=n','-s', "default(#{ Lockfile.sleep_inc.inspect })"],
237
+ ['--max_sleep=n','-p', "default(#{ Lockfile.max_sleep.inspect })"],
238
+ ['--min_sleep=n','-P', "default(#{ Lockfile.min_sleep.inspect })"],
239
+ ['--suspend=n','-u', "default(#{ Lockfile.suspend.inspect })"],
240
+ ['--timeout=n','-t', "default(#{ Lockfile.timeout.inspect }) - (nil => never)"],
241
+ ['--refresh=n','-f', "default(#{ Lockfile.refresh.inspect })"],
242
+ ['--debug','-d', "default(#{ Lockfile.debug.inspect })"],
243
+ ['--poll_retries=n','-R', "default(#{ Lockfile.poll_retries.inspect })"],
244
+ ['--poll_max_sleep=n','-S', "default(#{ Lockfile.poll_max_sleep.inspect })"],
245
+
246
+ ['--verbosity=0-4|debug|info|warn|error|fatal','-v'],
247
+ ['--log=path','-l'],
248
+ ['--log_age=log_age'],
249
+ ['--log_size=log_size'],
250
+ ['--help','-h'],
251
+ ]
252
+ options.each do |option|
253
+ opt = option.first.gsub(%r/(?:--)|(?:=.*$)/o,'').strip
254
+ get, set = opt_attr opt
255
+ @op.def_option(*option){|v| self.send(set, (v or true))}
256
+ end
257
+ #}}}
258
+ end
259
+ %w(debug info warn error fatal).each do |m|
260
+ eval "def #{ m }(*args,&block);@logger.#{ m }(*args,&block);end"
261
+ end
262
+ def init_logging
263
+ #{{{
264
+ if @opt_log_age
265
+ @opt_log_age = @opt_log_age.to_i if @opt_log_age =~ /\d/
266
+ end
267
+ if @opt_log_size
268
+ @opt_log_size = @opt_log_size.to_i if @opt_log_size =~ /\d/
269
+ end
270
+ $logger = @logger = Logger.new(@opt_log || $stdout, @opt_log_age, @opt_log_size)
271
+
272
+ level = nil
273
+ @opt_verbosity ||= 'info'
274
+ @opt_verbosity =
275
+ case @opt_verbosity
276
+ when /^\s*(?:4|d|debug)\s*$/io
277
+ level = 'Logging::DEBUG'
278
+ 4
279
+ when /^\s*(?:3|i|info)\s*$/io
280
+ level = 'Logging::INFO'
281
+ 3
282
+ when /^\s*(?:2|w|warn)\s*$/io
283
+ level = 'Logging::WARN'
284
+ 2
285
+ when /^\s*(?:1|e|error)\s*$/io
286
+ level = 'Logging::ERROR'
287
+ 1
288
+ when /^\s*(?:0|f|fatal)\s*$/io
289
+ level = 'Logging::FATAL'
290
+ 0
291
+ else
292
+ abort "illegal verbosity setting <#{ @opt_verbosity }>"
293
+ end
294
+ @logger.level = 2 - ((@opt_verbosity % 5) - 2)
295
+ debug {"logging level <#{ level }>"}
296
+ #}}}
297
+ end
298
+ def opt_attr opt
299
+ #{{{
300
+ get = "opt_#{ opt }"
301
+ set = "#{ get }="
302
+ code = <<-code
303
+ class << self
304
+ def #{ get }; defined?(@#{ get }) ? @#{ get } : nil; end
305
+ def #{ set } value; @#{ get } = value; end
306
+ end
307
+ code
308
+ instance_eval code
309
+ [get, set]
310
+ #}}}
311
+ end
312
+ def cp obj
313
+ #{{{
314
+ Marshal.load(Marshal.dump(obj))
315
+ #}}}
316
+ end
317
+ def pretty obj
318
+ #{{{
319
+ PP::pp obj, ''
320
+ #}}}
321
+ end
322
+ #}}}
323
+ end
324
+
325
+
326
+ Main.new(ARGV).run if $0 == __FILE__