timers 4.1.2 → 4.3.5
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.
- checksums.yaml +5 -5
- checksums.yaml.gz.sig +0 -0
- data/lib/timers/events.rb +105 -105
- data/lib/timers/group.rb +125 -119
- data/lib/timers/interval.rb +41 -0
- data/lib/timers/priority_heap.rb +146 -0
- data/lib/timers/timer.rb +135 -125
- data/lib/timers/version.rb +6 -1
- data/lib/timers/wait.rb +48 -42
- data/lib/timers.rb +18 -3
- data/license.md +48 -0
- data/{README.md → readme.md} +29 -32
- data.tar.gz.sig +0 -0
- metadata +119 -46
- metadata.gz.sig +0 -0
- data/.coveralls.yml +0 -1
- data/.gitignore +0 -17
- data/.rspec +0 -6
- data/.rubocop.yml +0 -28
- data/.ruby-version +0 -1
- data/.travis.yml +0 -21
- data/AUTHORS.md +0 -15
- data/CHANGES.md +0 -62
- data/Gemfile +0 -19
- data/LICENSE +0 -23
- data/Rakefile +0 -10
- data/spec/spec_helper.rb +0 -21
- data/spec/timers/cancel_spec.rb +0 -45
- data/spec/timers/events_spec.rb +0 -56
- data/spec/timers/every_spec.rb +0 -33
- data/spec/timers/group_spec.rb +0 -255
- data/spec/timers/performance_spec.rb +0 -96
- data/spec/timers/strict_spec.rb +0 -36
- data/spec/timers/wait_spec.rb +0 -30
- data/timers.gemspec +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e7c61c9db77955d85c69ca83499add3629c71628af3a825d78978b2383182b4a
|
4
|
+
data.tar.gz: 27f570753d83d4b4a98ee6ede089908d571347df8ba411edb56fe4ae965e703c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a7bac196cc0e3e1a784f7ba961c4a09539c875f80ac366da0b0d091d9f6ad1a505fb0a86a0c9d61d44a3495342029d003749b3ac072904b37f667d8e6e90b356
|
7
|
+
data.tar.gz: 4fa05113f72aef3abe63804a097e7fd9322c5b84a2a9ad14d590906e3ca59754857378a085388c52f9eb7ef1beb934b6b75789c3c087eee0b900f3446ace7207
|
checksums.yaml.gz.sig
ADDED
Binary file
|
data/lib/timers/events.rb
CHANGED
@@ -1,111 +1,111 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2014-2022, by Samuel Williams.
|
5
|
+
# Copyright, 2014-2016, by Tony Arcieri.
|
6
|
+
# Copyright, 2014, by Lavir the Whiolet.
|
7
|
+
# Copyright, 2015, by Utenmiki.
|
8
|
+
# Copyright, 2015, by Donovan Keme.
|
9
|
+
# Copyright, 2021, by Wander Hillen.
|
5
10
|
|
6
|
-
|
11
|
+
require_relative "timer"
|
12
|
+
require_relative "priority_heap"
|
7
13
|
|
8
14
|
module Timers
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
l
|
109
|
-
end
|
110
|
-
end
|
15
|
+
# Maintains a PriorityHeap of events ordered on time, which can be cancelled.
|
16
|
+
class Events
|
17
|
+
# Represents a cancellable handle for a specific timer event.
|
18
|
+
class Handle
|
19
|
+
include Comparable
|
20
|
+
|
21
|
+
def initialize(time, callback)
|
22
|
+
@time = time
|
23
|
+
@callback = callback
|
24
|
+
end
|
25
|
+
|
26
|
+
# The absolute time that the handle should be fired at.
|
27
|
+
attr_reader :time
|
28
|
+
|
29
|
+
# Cancel this timer, O(1).
|
30
|
+
def cancel!
|
31
|
+
# The simplest way to keep track of cancelled status is to nullify the
|
32
|
+
# callback. This should also be optimal for garbage collection.
|
33
|
+
@callback = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
# Has this timer been cancelled? Cancelled timer's don't fire.
|
37
|
+
def cancelled?
|
38
|
+
@callback.nil?
|
39
|
+
end
|
40
|
+
|
41
|
+
def <=> other
|
42
|
+
@time <=> other.time
|
43
|
+
end
|
44
|
+
|
45
|
+
# Fire the callback if not cancelled with the given time parameter.
|
46
|
+
def fire(time)
|
47
|
+
@callback.call(time) if @callback
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize
|
52
|
+
# A sequence of handles, maintained in sorted order, future to present.
|
53
|
+
# @sequence.last is the next event to be fired.
|
54
|
+
@sequence = PriorityHeap.new
|
55
|
+
@queue = []
|
56
|
+
end
|
57
|
+
|
58
|
+
# Add an event at the given time.
|
59
|
+
def schedule(time, callback)
|
60
|
+
flush!
|
61
|
+
|
62
|
+
handle = Handle.new(time.to_f, callback)
|
63
|
+
|
64
|
+
@queue << handle
|
65
|
+
|
66
|
+
return handle
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the first non-cancelled handle.
|
70
|
+
def first
|
71
|
+
merge!
|
72
|
+
|
73
|
+
while (handle = @sequence.peek)
|
74
|
+
return handle unless handle.cancelled?
|
75
|
+
@sequence.pop
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns the number of pending (possibly cancelled) events.
|
80
|
+
def size
|
81
|
+
@sequence.size + @queue.size
|
82
|
+
end
|
83
|
+
|
84
|
+
# Fire all handles for which Handle#time is less than the given time.
|
85
|
+
def fire(time)
|
86
|
+
merge!
|
87
|
+
|
88
|
+
while handle = @sequence.peek and handle.time <= time
|
89
|
+
@sequence.pop
|
90
|
+
handle.fire(time)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
# Move all non-cancelled timers from the pending queue to the priority heap
|
97
|
+
def merge!
|
98
|
+
while handle = @queue.pop
|
99
|
+
next if handle.cancelled?
|
100
|
+
|
101
|
+
@sequence.push(handle)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def flush!
|
106
|
+
while @queue.last&.cancelled?
|
107
|
+
@queue.pop
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
111
|
end
|
data/lib/timers/group.rb
CHANGED
@@ -1,127 +1,133 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2014-2022, by Samuel Williams.
|
5
|
+
# Copyright, 2014-2016, by Tony Arcieri.
|
6
|
+
# Copyright, 2015, by Donovan Keme.
|
7
|
+
# Copyright, 2015, by Tommy Ong Gia Phu.
|
8
|
+
|
3
9
|
require "set"
|
4
10
|
require "forwardable"
|
5
|
-
require "hitimes"
|
6
11
|
|
7
|
-
|
8
|
-
|
12
|
+
require_relative "interval"
|
13
|
+
require_relative "timer"
|
14
|
+
require_relative "events"
|
9
15
|
|
10
16
|
module Timers
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
17
|
+
# A collection of timers which may fire at different times
|
18
|
+
class Group
|
19
|
+
include Enumerable
|
20
|
+
|
21
|
+
extend Forwardable
|
22
|
+
def_delegators :@timers, :each, :empty?
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@events = Events.new
|
26
|
+
|
27
|
+
@timers = Set.new
|
28
|
+
@paused_timers = Set.new
|
29
|
+
|
30
|
+
@interval = Interval.new
|
31
|
+
@interval.start
|
32
|
+
end
|
33
|
+
|
34
|
+
# Scheduled events:
|
35
|
+
attr_reader :events
|
36
|
+
|
37
|
+
# Active timers:
|
38
|
+
attr_reader :timers
|
39
|
+
|
40
|
+
# Paused timers:
|
41
|
+
attr_reader :paused_timers
|
42
|
+
|
43
|
+
# Call the given block after the given interval. The first argument will be
|
44
|
+
# the time at which the group was asked to fire timers for.
|
45
|
+
def after(interval, &block)
|
46
|
+
Timer.new(self, interval, false, &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Call the given block immediately, and then after the given interval. The first
|
50
|
+
# argument will be the time at which the group was asked to fire timers for.
|
51
|
+
def now_and_after(interval, &block)
|
52
|
+
yield
|
53
|
+
after(interval, &block)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Call the given block periodically at the given interval. The first
|
57
|
+
# argument will be the time at which the group was asked to fire timers for.
|
58
|
+
def every(interval, recur = true, &block)
|
59
|
+
Timer.new(self, interval, recur, &block)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Call the given block immediately, and then periodically at the given interval. The first
|
63
|
+
# argument will be the time at which the group was asked to fire timers for.
|
64
|
+
def now_and_every(interval, recur = true, &block)
|
65
|
+
yield
|
66
|
+
every(interval, recur, &block)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Wait for the next timer and fire it. Can take a block, which should behave
|
70
|
+
# like sleep(n), except that n may be nil (sleep forever) or a negative
|
71
|
+
# number (fire immediately after return).
|
72
|
+
def wait
|
73
|
+
if block_given?
|
74
|
+
yield wait_interval
|
75
|
+
|
76
|
+
while (interval = wait_interval) && interval > 0
|
77
|
+
yield interval
|
78
|
+
end
|
79
|
+
else
|
80
|
+
while (interval = wait_interval) && interval > 0
|
81
|
+
# We cannot assume that sleep will wait for the specified time, it might be +/- a bit.
|
82
|
+
sleep interval
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
fire
|
87
|
+
end
|
88
|
+
|
89
|
+
# Interval to wait until when the next timer will fire.
|
90
|
+
# - nil: no timers
|
91
|
+
# - -ve: timers expired already
|
92
|
+
# - 0: timers ready to fire
|
93
|
+
# - +ve: timers waiting to fire
|
94
|
+
def wait_interval(offset = current_offset)
|
95
|
+
handle = @events.first
|
96
|
+
handle.time - Float(offset) if handle
|
97
|
+
end
|
98
|
+
|
99
|
+
# Fire all timers that are ready.
|
100
|
+
def fire(offset = current_offset)
|
101
|
+
@events.fire(offset)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Pause all timers.
|
105
|
+
def pause
|
106
|
+
@timers.dup.each(&:pause)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Resume all timers.
|
110
|
+
def resume
|
111
|
+
@paused_timers.dup.each(&:resume)
|
112
|
+
end
|
113
|
+
|
114
|
+
alias continue resume
|
115
|
+
|
116
|
+
# Delay all timers.
|
117
|
+
def delay(seconds)
|
118
|
+
@timers.each do |timer|
|
119
|
+
timer.delay(seconds)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Cancel all timers.
|
124
|
+
def cancel
|
125
|
+
@timers.dup.each(&:cancel)
|
126
|
+
end
|
127
|
+
|
128
|
+
# The group's current time.
|
129
|
+
def current_offset
|
130
|
+
@interval.to_f
|
131
|
+
end
|
132
|
+
end
|
127
133
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2018-2022, by Samuel Williams.
|
5
|
+
|
6
|
+
module Timers
|
7
|
+
# A collection of timers which may fire at different times
|
8
|
+
class Interval
|
9
|
+
# Get the current elapsed monotonic time.
|
10
|
+
def initialize
|
11
|
+
@total = 0.0
|
12
|
+
@current = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def start
|
16
|
+
return if @current
|
17
|
+
|
18
|
+
@current = now
|
19
|
+
end
|
20
|
+
|
21
|
+
def stop
|
22
|
+
return unless @current
|
23
|
+
|
24
|
+
@total += duration
|
25
|
+
|
26
|
+
@current = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_f
|
30
|
+
@total + duration
|
31
|
+
end
|
32
|
+
|
33
|
+
protected def duration
|
34
|
+
now - @current
|
35
|
+
end
|
36
|
+
|
37
|
+
protected def now
|
38
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2021, by Wander Hillen.
|
5
|
+
# Copyright, 2021-2022, by Samuel Williams.
|
6
|
+
|
7
|
+
module Timers
|
8
|
+
# A priority queue implementation using a standard binary minheap. It uses straight comparison
|
9
|
+
# of its contents to determine priority. This works because a Handle from Timers::Events implements
|
10
|
+
# the '<' operation by comparing the expiry time.
|
11
|
+
# See <https://en.wikipedia.org/wiki/Binary_heap> for explanations of the main methods.
|
12
|
+
class PriorityHeap
|
13
|
+
def initialize
|
14
|
+
# The heap is represented with an array containing a binary tree. See
|
15
|
+
# https://en.wikipedia.org/wiki/Binary_heap#Heap_implementation for how this array
|
16
|
+
# is built up.
|
17
|
+
@contents = []
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the earliest timer or nil if the heap is empty.
|
21
|
+
def peek
|
22
|
+
@contents[0]
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns the number of elements in the heap
|
26
|
+
def size
|
27
|
+
@contents.size
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns the earliest timer if the heap is non-empty and removes it from the heap.
|
31
|
+
# Returns nil if the heap is empty. (and doesn't change the heap in that case)
|
32
|
+
def pop
|
33
|
+
# If the heap is empty:
|
34
|
+
if @contents.empty?
|
35
|
+
return nil
|
36
|
+
end
|
37
|
+
|
38
|
+
# If we have only one item, no swapping is required:
|
39
|
+
if @contents.size == 1
|
40
|
+
return @contents.pop
|
41
|
+
end
|
42
|
+
|
43
|
+
# Take the root of the tree:
|
44
|
+
value = @contents[0]
|
45
|
+
|
46
|
+
# Remove the last item in the tree:
|
47
|
+
last = @contents.pop
|
48
|
+
|
49
|
+
# Overwrite the root of the tree with the item:
|
50
|
+
@contents[0] = last
|
51
|
+
|
52
|
+
# Bubble it down into place:
|
53
|
+
bubble_down(0)
|
54
|
+
|
55
|
+
# validate!
|
56
|
+
|
57
|
+
return value
|
58
|
+
end
|
59
|
+
|
60
|
+
# Inserts a new timer into the heap, then rearranges elements until the heap invariant is true again.
|
61
|
+
def push(element)
|
62
|
+
# Insert the item at the end of the heap:
|
63
|
+
@contents.push(element)
|
64
|
+
|
65
|
+
# Bubble it up into position:
|
66
|
+
bubble_up(@contents.size - 1)
|
67
|
+
|
68
|
+
# validate!
|
69
|
+
|
70
|
+
return self
|
71
|
+
end
|
72
|
+
|
73
|
+
# Empties out the heap, discarding all elements
|
74
|
+
def clear!
|
75
|
+
@contents = []
|
76
|
+
end
|
77
|
+
|
78
|
+
# Validate the heap invariant. Every element except the root must not be smaller than
|
79
|
+
# its parent element. Note that it MAY be equal.
|
80
|
+
def valid?
|
81
|
+
# notice we skip index 0 on purpose, because it has no parent
|
82
|
+
(1..(@contents.size - 1)).all? { |e| @contents[e] >= @contents[(e - 1) / 2] }
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def swap(i, j)
|
88
|
+
@contents[i], @contents[j] = @contents[j], @contents[i]
|
89
|
+
end
|
90
|
+
|
91
|
+
def bubble_up(index)
|
92
|
+
parent_index = (index - 1) / 2 # watch out, integer division!
|
93
|
+
|
94
|
+
while index > 0 && @contents[index] < @contents[parent_index]
|
95
|
+
# if the node has a smaller value than its parent, swap these nodes
|
96
|
+
# to uphold the minheap invariant and update the index of the 'current'
|
97
|
+
# node. If the node is already at index 0, we can also stop because that
|
98
|
+
# is the root of the heap.
|
99
|
+
# swap(index, parent_index)
|
100
|
+
@contents[index], @contents[parent_index] = @contents[parent_index], @contents[index]
|
101
|
+
|
102
|
+
index = parent_index
|
103
|
+
parent_index = (index - 1) / 2 # watch out, integer division!
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def bubble_down(index)
|
108
|
+
swap_value = 0
|
109
|
+
swap_index = nil
|
110
|
+
|
111
|
+
while true
|
112
|
+
left_index = (2 * index) + 1
|
113
|
+
left_value = @contents[left_index]
|
114
|
+
|
115
|
+
if left_value.nil?
|
116
|
+
# This node has no children so it can't bubble down any further.
|
117
|
+
# We're done here!
|
118
|
+
return
|
119
|
+
end
|
120
|
+
|
121
|
+
# Determine which of the child nodes has the smallest value:
|
122
|
+
right_index = left_index + 1
|
123
|
+
right_value = @contents[right_index]
|
124
|
+
|
125
|
+
if right_value.nil? or right_value > left_value
|
126
|
+
swap_value = left_value
|
127
|
+
swap_index = left_index
|
128
|
+
else
|
129
|
+
swap_value = right_value
|
130
|
+
swap_index = right_index
|
131
|
+
end
|
132
|
+
|
133
|
+
if @contents[index] < swap_value
|
134
|
+
# No need to swap, the minheap invariant is already satisfied:
|
135
|
+
return
|
136
|
+
else
|
137
|
+
# At least one of the child node has a smaller value than the current node, swap current node with that child and update current node for if it might need to bubble down even further:
|
138
|
+
# swap(index, swap_index)
|
139
|
+
@contents[index], @contents[swap_index] = @contents[swap_index], @contents[index]
|
140
|
+
|
141
|
+
index = swap_index
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|