rufus-scheduler 2.0.24 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/CHANGELOG.txt +76 -0
  2. data/CREDITS.txt +23 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +1439 -0
  5. data/Rakefile +1 -5
  6. data/TODO.txt +149 -55
  7. data/lib/rufus/{sc → scheduler}/cronline.rb +167 -53
  8. data/lib/rufus/scheduler/job_array.rb +92 -0
  9. data/lib/rufus/scheduler/jobs.rb +633 -0
  10. data/lib/rufus/scheduler/locks.rb +95 -0
  11. data/lib/rufus/scheduler/util.rb +306 -0
  12. data/lib/rufus/scheduler/zones.rb +174 -0
  13. data/lib/rufus/scheduler/zotime.rb +154 -0
  14. data/lib/rufus/scheduler.rb +608 -27
  15. data/rufus-scheduler.gemspec +6 -4
  16. data/spec/basics_spec.rb +54 -0
  17. data/spec/cronline_spec.rb +479 -152
  18. data/spec/error_spec.rb +139 -0
  19. data/spec/job_array_spec.rb +39 -0
  20. data/spec/job_at_spec.rb +58 -0
  21. data/spec/job_cron_spec.rb +128 -0
  22. data/spec/job_every_spec.rb +104 -0
  23. data/spec/job_in_spec.rb +20 -0
  24. data/spec/job_interval_spec.rb +68 -0
  25. data/spec/job_repeat_spec.rb +357 -0
  26. data/spec/job_spec.rb +498 -109
  27. data/spec/lock_custom_spec.rb +47 -0
  28. data/spec/lock_flock_spec.rb +47 -0
  29. data/spec/lock_lockfile_spec.rb +61 -0
  30. data/spec/lock_spec.rb +59 -0
  31. data/spec/parse_spec.rb +263 -0
  32. data/spec/schedule_at_spec.rb +158 -0
  33. data/spec/schedule_cron_spec.rb +66 -0
  34. data/spec/schedule_every_spec.rb +109 -0
  35. data/spec/schedule_in_spec.rb +80 -0
  36. data/spec/schedule_interval_spec.rb +128 -0
  37. data/spec/scheduler_spec.rb +928 -124
  38. data/spec/spec_helper.rb +126 -0
  39. data/spec/threads_spec.rb +96 -0
  40. data/spec/zotime_spec.rb +396 -0
  41. metadata +56 -33
  42. data/README.rdoc +0 -661
  43. data/lib/rufus/otime.rb +0 -3
  44. data/lib/rufus/sc/jobqueues.rb +0 -160
  45. data/lib/rufus/sc/jobs.rb +0 -471
  46. data/lib/rufus/sc/rtime.rb +0 -363
  47. data/lib/rufus/sc/scheduler.rb +0 -636
  48. data/lib/rufus/sc/version.rb +0 -32
  49. data/spec/at_in_spec.rb +0 -47
  50. data/spec/at_spec.rb +0 -125
  51. data/spec/blocking_spec.rb +0 -64
  52. data/spec/cron_spec.rb +0 -134
  53. data/spec/every_spec.rb +0 -304
  54. data/spec/exception_spec.rb +0 -113
  55. data/spec/in_spec.rb +0 -150
  56. data/spec/mutex_spec.rb +0 -159
  57. data/spec/rtime_spec.rb +0 -137
  58. data/spec/schedulable_spec.rb +0 -97
  59. data/spec/spec_base.rb +0 -87
  60. data/spec/stress_schedule_unschedule_spec.rb +0 -159
  61. data/spec/timeout_spec.rb +0 -148
  62. data/test/kjw.rb +0 -113
  63. data/test/t.rb +0 -20
@@ -0,0 +1,633 @@
1
+ #--
2
+ # Copyright (c) 2006-2015, John Mettraux, jmettraux@gmail.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+ # Made in Japan.
23
+ #++
24
+
25
+
26
+ module Rufus
27
+
28
+ class Scheduler
29
+
30
+ #--
31
+ # job classes
32
+ #++
33
+
34
+ class Job
35
+
36
+ #
37
+ # Used by Job#kill
38
+ #
39
+ class KillSignal < StandardError; end
40
+
41
+ attr_reader :id
42
+ attr_reader :opts
43
+ attr_reader :original
44
+ attr_reader :scheduled_at
45
+ attr_reader :last_time
46
+ attr_reader :unscheduled_at
47
+ attr_reader :tags
48
+ attr_reader :count
49
+ attr_reader :last_work_time
50
+ attr_reader :mean_work_time
51
+
52
+ # next trigger time
53
+ #
54
+ attr_accessor :next_time
55
+
56
+ # anything with a #call(job[, timet]) method,
57
+ # what gets actually triggered
58
+ #
59
+ attr_reader :callable
60
+
61
+ # a reference to the instance whose call method is the @callable
62
+ #
63
+ attr_reader :handler
64
+
65
+ def initialize(scheduler, original, opts, block)
66
+
67
+ @scheduler = scheduler
68
+ @original = original
69
+ @opts = opts
70
+
71
+ @handler = block
72
+
73
+ @callable =
74
+ if block.respond_to?(:arity)
75
+ block
76
+ elsif block.respond_to?(:call)
77
+ block.method(:call)
78
+ elsif block.is_a?(Class)
79
+ @handler = block.new
80
+ @handler.method(:call) rescue nil
81
+ else
82
+ nil
83
+ end
84
+
85
+ @scheduled_at = Time.now
86
+ @unscheduled_at = nil
87
+ @last_time = nil
88
+
89
+ @locals = {}
90
+ @local_mutex = Mutex.new
91
+
92
+ @id = determine_id
93
+
94
+ raise(
95
+ ArgumentError,
96
+ 'missing block or callable to schedule',
97
+ caller[2..-1]
98
+ ) unless @callable
99
+
100
+ @tags = Array(opts[:tag] || opts[:tags]).collect { |t| t.to_s }
101
+
102
+ @count = 0
103
+ @last_work_time = 0.0
104
+ @mean_work_time = 0.0
105
+
106
+ # tidy up options
107
+
108
+ if @opts[:allow_overlap] == false || @opts[:allow_overlapping] == false
109
+ @opts[:overlap] = false
110
+ end
111
+ if m = @opts[:mutex]
112
+ @opts[:mutex] = Array(m)
113
+ end
114
+ end
115
+
116
+ alias job_id id
117
+
118
+ def trigger(time)
119
+
120
+ set_next_time(time)
121
+
122
+ return if (
123
+ opts[:overlap] == false &&
124
+ running?
125
+ )
126
+ return if (
127
+ callback(:confirm_lock, time) &&
128
+ callback(:on_pre_trigger, time)
129
+ ) == false
130
+
131
+ @count += 1
132
+
133
+ if opts[:blocking]
134
+ do_trigger(time)
135
+ else
136
+ do_trigger_in_thread(time)
137
+ end
138
+ end
139
+
140
+ def unschedule
141
+
142
+ @unscheduled_at = Time.now
143
+ end
144
+
145
+ def threads
146
+
147
+ Thread.list.select { |t| t[:rufus_scheduler_job] == self }
148
+ end
149
+
150
+ # Kills all the threads this Job currently has going on.
151
+ #
152
+ def kill
153
+
154
+ threads.each { |t| t.raise(KillSignal) }
155
+ end
156
+
157
+ def running?
158
+
159
+ threads.any?
160
+ end
161
+
162
+ def scheduled?
163
+
164
+ @scheduler.scheduled?(self)
165
+ end
166
+
167
+ def []=(key, value)
168
+
169
+ @local_mutex.synchronize { @locals[key] = value }
170
+ end
171
+
172
+ def [](key)
173
+
174
+ @local_mutex.synchronize { @locals[key] }
175
+ end
176
+
177
+ def key?(key)
178
+
179
+ @local_mutex.synchronize { @locals.key?(key) }
180
+ end
181
+
182
+ def keys
183
+
184
+ @local_mutex.synchronize { @locals.keys }
185
+ end
186
+
187
+ #def hash
188
+ # self.object_id
189
+ #end
190
+ #def eql?(o)
191
+ # o.class == self.class && o.hash == self.hash
192
+ #end
193
+ #
194
+ # might be necessary at some point
195
+
196
+ # Calls the callable (usually a block) wrapped in this Job instance.
197
+ #
198
+ # Warning: error rescueing is the responsibity of the caller.
199
+ #
200
+ def call(do_rescue=false)
201
+
202
+ do_call(Time.now, do_rescue)
203
+ end
204
+
205
+ protected
206
+
207
+ def callback(meth, time)
208
+
209
+ return true unless @scheduler.respond_to?(meth)
210
+
211
+ arity = @scheduler.method(meth).arity
212
+ args = [ self, time ][0, (arity < 0 ? 2 : arity)]
213
+
214
+ @scheduler.send(meth, *args)
215
+ end
216
+
217
+ def compute_timeout
218
+
219
+ if to = @opts[:timeout]
220
+ Rufus::Scheduler.parse(to)
221
+ else
222
+ nil
223
+ end
224
+ end
225
+
226
+ def mutex(m)
227
+
228
+ m.is_a?(Mutex) ? m : (@scheduler.mutexes[m.to_s] ||= Mutex.new)
229
+ end
230
+
231
+ def do_call(time, do_rescue)
232
+
233
+ args = [ self, time ][0, @callable.arity]
234
+ @callable.call(*args)
235
+
236
+ rescue StandardError => se
237
+
238
+ raise se unless do_rescue
239
+
240
+ return if se.is_a?(KillSignal) # discard
241
+
242
+ @scheduler.on_error(self, se)
243
+
244
+ # exceptions above StandardError do pass through
245
+ end
246
+
247
+ def do_trigger(time)
248
+
249
+ t = Time.now
250
+ # if there are mutexes, t might be really bigger than time
251
+
252
+ Thread.current[:rufus_scheduler_job] = self
253
+ Thread.current[:rufus_scheduler_time] = t
254
+ Thread.current[:rufus_scheduler_timeout] = compute_timeout
255
+
256
+ @last_time = t
257
+
258
+ do_call(time, true)
259
+
260
+ ensure
261
+
262
+ @last_work_time =
263
+ Time.now - Thread.current[:rufus_scheduler_time]
264
+ @mean_work_time =
265
+ ((@count - 1) * @mean_work_time + @last_work_time) / @count
266
+
267
+ post_trigger(time)
268
+
269
+ Thread.current[:rufus_scheduler_job] = nil
270
+ Thread.current[:rufus_scheduler_time] = nil
271
+ Thread.current[:rufus_scheduler_timeout] = nil
272
+ end
273
+
274
+ def post_trigger(time)
275
+
276
+ set_next_time(time, true)
277
+
278
+ callback(:on_post_trigger, time)
279
+ end
280
+
281
+ def start_work_thread
282
+
283
+ thread =
284
+ Thread.new do
285
+
286
+ Thread.current[@scheduler.thread_key] = true
287
+ Thread.current[:rufus_scheduler_job_thread] = true
288
+
289
+ loop do
290
+
291
+ job, time = @scheduler.work_queue.pop
292
+
293
+ break if @scheduler.started_at == nil
294
+
295
+ next if job.unscheduled_at
296
+
297
+ begin
298
+
299
+ (job.opts[:mutex] || []).reduce(
300
+ lambda { job.do_trigger(time) }
301
+ ) do |b, m|
302
+ lambda { mutex(m).synchronize { b.call } }
303
+ end.call
304
+
305
+ rescue KillSignal
306
+
307
+ # simply go on looping
308
+ end
309
+ end
310
+ end
311
+
312
+ thread[@scheduler.thread_key] = true
313
+ thread[:rufus_scheduler_work_thread] = true
314
+ #
315
+ # same as above (in the thead block),
316
+ # but since it has to be done as quickly as possible.
317
+ # So, whoever is running first (scheduler thread vs job thread)
318
+ # sets this information
319
+ end
320
+
321
+ def do_trigger_in_thread(time)
322
+
323
+ threads = @scheduler.work_threads
324
+
325
+ cur = threads.size
326
+ vac = threads.select { |t| t[:rufus_scheduler_job] == nil }.size
327
+ #min = @scheduler.min_work_threads
328
+ max = @scheduler.max_work_threads
329
+ que = @scheduler.work_queue.size
330
+
331
+ start_work_thread if vac - que < 1 && cur < max
332
+
333
+ @scheduler.work_queue << [ self, time ]
334
+ end
335
+ end
336
+
337
+ class OneTimeJob < Job
338
+
339
+ alias time next_time
340
+
341
+ def occurrences(time0, time1)
342
+
343
+ (time >= time0 && time <= time1) ? [ time ] : []
344
+ end
345
+
346
+ protected
347
+
348
+ def determine_id
349
+
350
+ [
351
+ self.class.name.split(':').last.downcase[0..-4],
352
+ @scheduled_at.to_f,
353
+ @next_time.to_f,
354
+ opts.hash.abs
355
+ ].map(&:to_s).join('_')
356
+ end
357
+
358
+ # There is no next_time for one time jobs, hence the false.
359
+ #
360
+ def set_next_time(trigger_time, is_post=false)
361
+
362
+ @next_time = is_post ? nil : false
363
+ end
364
+ end
365
+
366
+ class AtJob < OneTimeJob
367
+
368
+ def initialize(scheduler, time, opts, block)
369
+
370
+ super(scheduler, time, opts, block)
371
+
372
+ @next_time =
373
+ opts[:_t] || Rufus::Scheduler.parse_at(time, opts)
374
+ end
375
+ end
376
+
377
+ class InJob < OneTimeJob
378
+
379
+ def initialize(scheduler, duration, opts, block)
380
+
381
+ super(scheduler, duration, opts, block)
382
+
383
+ @next_time =
384
+ @scheduled_at +
385
+ opts[:_t] || Rufus::Scheduler.parse_in(duration, opts)
386
+ end
387
+ end
388
+
389
+ class RepeatJob < Job
390
+
391
+ attr_reader :paused_at
392
+
393
+ attr_reader :first_at
394
+ attr_accessor :last_at
395
+ attr_accessor :times
396
+
397
+ def initialize(scheduler, duration, opts, block)
398
+
399
+ super
400
+
401
+ @paused_at = nil
402
+
403
+ @times = opts[:times]
404
+
405
+ raise ArgumentError.new(
406
+ "cannot accept :times => #{@times.inspect}, not nil or an int"
407
+ ) unless @times == nil || @times.is_a?(Fixnum)
408
+
409
+ self.first_at =
410
+ opts[:first] || opts[:first_time] ||
411
+ opts[:first_at] || opts[:first_in] ||
412
+ nil
413
+ self.last_at =
414
+ opts[:last] || opts[:last_at] || opts[:last_in]
415
+ end
416
+
417
+ def first_at=(first)
418
+
419
+ return @first_at = nil if first == nil
420
+
421
+ n = Time.now
422
+ first = n + 0.003 if first == :now || first == :immediately
423
+
424
+ @first_at = Rufus::Scheduler.parse_to_time(first)
425
+
426
+ raise ArgumentError.new(
427
+ "cannot set first[_at|_in] in the past: " +
428
+ "#{first.inspect} -> #{@first_at.inspect}"
429
+ ) if first != 0 && @first_at < n
430
+ end
431
+
432
+ def last_at=(last)
433
+
434
+ @last_at = last ? Rufus::Scheduler.parse_to_time(last) : nil
435
+
436
+ raise ArgumentError.new(
437
+ "cannot set last[_at|_in] in the past: " +
438
+ "#{last.inspect} -> #{@last_at.inspect}"
439
+ ) if last && @last_at < Time.now
440
+ end
441
+
442
+ def trigger(time)
443
+
444
+ return if @paused_at
445
+
446
+ return (@next_time = nil) if @times && @times < 1
447
+ return (@next_time = nil) if @last_at && time >= @last_at
448
+ #
449
+ # TODO: rework that, jobs are thus kept 1 step too much in @jobs
450
+
451
+ super
452
+
453
+ @times -= 1 if @times
454
+ end
455
+
456
+ def pause
457
+
458
+ @paused_at = Time.now
459
+ end
460
+
461
+ def resume
462
+
463
+ @paused_at = nil
464
+ end
465
+
466
+ def paused?
467
+
468
+ @paused_at != nil
469
+ end
470
+
471
+ def determine_id
472
+
473
+ [
474
+ self.class.name.split(':').last.downcase[0..-4],
475
+ @scheduled_at.to_f,
476
+ opts.hash.abs
477
+ ].map(&:to_s).join('_')
478
+ end
479
+
480
+ def occurrences(time0, time1)
481
+
482
+ a = []
483
+
484
+ nt = @next_time
485
+ ts = @times
486
+
487
+ loop do
488
+
489
+ break if nt > time1
490
+ break if ts && ts <= 0
491
+
492
+ a << nt if nt >= time0
493
+
494
+ nt = next_time_from(nt)
495
+ ts = ts - 1 if ts
496
+ end
497
+
498
+ a
499
+ end
500
+ end
501
+
502
+ #
503
+ # A parent class of EveryJob and IntervalJob
504
+ #
505
+ class EvInJob < RepeatJob
506
+
507
+ def first_at=(first)
508
+
509
+ super
510
+
511
+ @next_time = @first_at
512
+ end
513
+ end
514
+
515
+ class EveryJob < EvInJob
516
+
517
+ attr_reader :frequency
518
+
519
+ def initialize(scheduler, duration, opts, block)
520
+
521
+ super(scheduler, duration, opts, block)
522
+
523
+ @frequency = Rufus::Scheduler.parse_in(@original)
524
+
525
+ raise ArgumentError.new(
526
+ "cannot schedule #{self.class} with a frequency " +
527
+ "of #{@frequency.inspect} (#{@original.inspect})"
528
+ ) if @frequency <= 0
529
+
530
+ set_next_time(nil)
531
+ end
532
+
533
+ protected
534
+
535
+ def set_next_time(trigger_time, is_post=false)
536
+
537
+ return if is_post
538
+
539
+ @next_time =
540
+ if @first_at == nil || @first_at < Time.now
541
+ (trigger_time || Time.now) + @frequency
542
+ else
543
+ @first_at
544
+ end
545
+ end
546
+
547
+ def next_time_from(time)
548
+
549
+ time + @frequency
550
+ end
551
+ end
552
+
553
+ class IntervalJob < EvInJob
554
+
555
+ attr_reader :interval
556
+
557
+ def initialize(scheduler, interval, opts, block)
558
+
559
+ super(scheduler, interval, opts, block)
560
+
561
+ @interval = Rufus::Scheduler.parse_in(@original)
562
+
563
+ raise ArgumentError.new(
564
+ "cannot schedule #{self.class} with an interval " +
565
+ "of #{@interval.inspect} (#{@original.inspect})"
566
+ ) if @interval <= 0
567
+
568
+ set_next_time(nil)
569
+ end
570
+
571
+ protected
572
+
573
+ def set_next_time(trigger_time, is_post=false)
574
+
575
+ @next_time =
576
+ if is_post
577
+ Time.now + @interval
578
+ elsif trigger_time.nil?
579
+ if @first_at == nil || @first_at < Time.now
580
+ Time.now + @interval
581
+ else
582
+ @first_at
583
+ end
584
+ else
585
+ false
586
+ end
587
+ end
588
+
589
+ def next_time_from(time)
590
+
591
+ time + @mean_work_time + @interval
592
+ end
593
+ end
594
+
595
+ class CronJob < RepeatJob
596
+
597
+ def initialize(scheduler, cronline, opts, block)
598
+
599
+ super(scheduler, cronline, opts, block)
600
+
601
+ @cron_line = opts[:_t] || CronLine.new(cronline)
602
+ set_next_time(nil)
603
+ end
604
+
605
+ def frequency
606
+
607
+ @cron_line.frequency
608
+ end
609
+
610
+ def brute_frequency
611
+
612
+ @cron_line.brute_frequency
613
+ end
614
+
615
+ protected
616
+
617
+ def set_next_time(trigger_time, is_post=false)
618
+
619
+ @next_time = next_time_from(trigger_time || Time.now)
620
+ end
621
+
622
+ def next_time_from(time)
623
+
624
+ if @first_at == nil || @first_at < time
625
+ @cron_line.next_time(time)
626
+ else
627
+ @first_at
628
+ end
629
+ end
630
+ end
631
+ end
632
+ end
633
+
@@ -0,0 +1,95 @@
1
+ #--
2
+ # Copyright (c) 2006-2015, John Mettraux, jmettraux@gmail.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+ # Made in Japan.
23
+ #++
24
+
25
+ require 'fileutils'
26
+
27
+
28
+ class Rufus::Scheduler
29
+
30
+ #
31
+ # A lock that can always be acquired
32
+ #
33
+ class NullLock
34
+
35
+ # Locking is always successful.
36
+ #
37
+ def lock; true; end
38
+
39
+ def locked?; true; end
40
+ def unlock; true; end
41
+ end
42
+
43
+ #
44
+ # The standard flock mecha, with its own class thanks to @ecin
45
+ #
46
+ class FileLock
47
+
48
+ attr_reader :path
49
+
50
+ def initialize(path)
51
+
52
+ @path = path.to_s
53
+ end
54
+
55
+ # Locking is successful if this Ruby process can create and lock
56
+ # its lockfile (at the given path).
57
+ #
58
+ def lock
59
+
60
+ return true if locked?
61
+
62
+ @lockfile = nil
63
+
64
+ FileUtils.mkdir_p(::File.dirname(@path))
65
+
66
+ file = File.new(@path, File::RDWR | File::CREAT)
67
+ locked = file.flock(File::LOCK_NB | File::LOCK_EX)
68
+
69
+ return false unless locked
70
+
71
+ now = Time.now
72
+
73
+ file.print("pid: #{$$}, ")
74
+ file.print("scheduler.object_id: #{self.object_id}, ")
75
+ file.print("time: #{now}, ")
76
+ file.print("timestamp: #{now.to_f}")
77
+ file.flush
78
+
79
+ @lockfile = file
80
+
81
+ true
82
+ end
83
+
84
+ def unlock
85
+
86
+ !! (@lockfile && @lockfile.flock(File::LOCK_UN))
87
+ end
88
+
89
+ def locked?
90
+
91
+ !! (@lockfile && @lockfile.flock(File::LOCK_NB | File::LOCK_EX))
92
+ end
93
+ end
94
+ end
95
+