lockfile 1.1.0

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