rufus-scheduler 2.0.24 → 3.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.
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
+