1
1
require 'thread'
2
2
require 'concurrent/options_parser'
3
+ require 'concurrent/atomic/event'
3
4
require 'concurrent/collection/priority_queue'
4
5
5
6
module Concurrent
6
7
8
+ # Executes a collection of tasks at the specified times. A master thread
9
+ # monitors the set and schedules each task for execution at the appropriate
10
+ # time. Tasks are run on the global task pool or on the supplied executor.
7
11
class TimerSet
8
12
13
+ # Create a new set of timed tasks.
14
+ #
15
+ # @param [Hash] opts the options controlling how the future will be processed
16
+ # @option opts [Boolean] :operation (false) when `true` will execute the future on the global
17
+ # operation pool (for long-running operations), when `false` will execute the future on the
18
+ # global task pool (for short-running tasks)
19
+ # @option opts [object] :executor when provided will run all operations on
20
+ # this executor rather than the global thread pool (overrides :operation)
9
21
def initialize ( opts = { } )
10
22
@mutex = Mutex . new
23
+ @shutdown = Event . new
11
24
@queue = PriorityQueue . new ( order : :min )
12
25
@executor = OptionsParser ::get_executor_from ( opts )
13
26
@thread = nil
14
27
end
15
28
29
+ # Am I running?
30
+ #
31
+ # @return [Boolean] `true` when running, `false` when shutting down or shutdown
32
+ def running?
33
+ ! @shutdown . set?
34
+ end
35
+
36
+ # Am I shutdown?
37
+ #
38
+ # @return [Boolean] `true` when shutdown, `false` when shutting down or running
39
+ def shutdown?
40
+ @shutdown . set?
41
+ end
42
+
43
+ # Block until shutdown is complete or until `timeout` seconds have passed.
44
+ #
45
+ # @note Does not initiate shutdown or termination. Either `shutdown` or `kill`
46
+ # must be called before this method (or on another thread).
47
+ #
48
+ # @param [Integer] timeout the maximum number of seconds to wait for shutdown to complete
49
+ #
50
+ # @return [Boolean] `true` if shutdown complete or false on `timeout`
51
+ def wait_for_termination ( timeout )
52
+ @shutdown . wait ( timeout . to_f )
53
+ end
54
+
55
+ # Post a task to be execute at the specified time. The given time may be either
56
+ # a `Time` object or the number of seconds to wait. If the intended execution
57
+ # time is within 1/100th of a second of the current time the task will be
58
+ # immediately post to the executor.
59
+ #
60
+ # @param [Object] intended_time the time to schedule the task for execution
61
+ #
62
+ # @yield the task to be performed
63
+ #
64
+ # @return [Boolean] true if the message is post, false after shutdown
65
+ #
66
+ # @raise [ArgumentError] if the intended execution time is not in the future
67
+ # @raise [ArgumentError] if no block is given
16
68
def post ( intended_time , &block )
17
- raise ArgumentError . new ( 'no block given' ) unless block_given?
18
- time = calculate_schedule_time ( intended_time )
19
- @mutex . synchronize { @queue . push ( Task . new ( time , block ) ) }
20
- check_processing_thread
69
+ @mutex . synchronize do
70
+ return false if shutdown?
71
+ raise ArgumentError . new ( 'no block given' ) unless block_given?
72
+ time = calculate_schedule_time ( intended_time )
73
+
74
+ if ( time - Time . now . to_f ) <= 0.01
75
+ @executor . post ( &block )
76
+ else
77
+ @queue . push ( Task . new ( time , block ) )
78
+ end
79
+ end
80
+ check_processing_thread!
81
+ true
82
+ end
83
+
84
+ def shutdown
85
+ @mutex . synchronize do
86
+ unless @shutdown . set?
87
+ @queue . clear
88
+ @thread . kill if @thread
89
+ @shutdown . set
90
+ end
91
+ end
92
+ true
21
93
end
94
+ alias_method :kill , :shutdown
22
95
23
96
private
24
97
98
+ # A struct for encapsulating a task and its intended execution time.
99
+ # It facilitates proper prioritization by overriding the comparison
100
+ # (spaceship) operator as a comparison of the intended execution
101
+ # times.
102
+ #
103
+ # @!visibility private
25
104
Task = Struct . new ( :time , :op ) do
26
105
include Comparable
27
106
def <=>( other )
28
107
self . time <=> other . time
29
108
end
30
109
end
31
110
111
+ # Calculate an Epoch time with milliseconds at which to execute a
112
+ # task. If the given time is a `Time` object it will be converted
113
+ # accordingly. If the time is an integer value greate than zero
114
+ # it will be understood as a number of seconds in the future and
115
+ # will be added to the current time to calculate Epoch.
116
+ #
117
+ # @raise [ArgumentError] if the intended execution time is not in the future
118
+ #
119
+ # @!visibility private
32
120
def calculate_schedule_time ( intended_time , now = Time . now )
33
121
if intended_time . is_a? ( Time )
34
- raise SchedulingError . new ( 'schedule time must be in the future' ) if intended_time <= now
122
+ raise ArgumentError . new ( 'schedule time must be in the future' ) if intended_time <= now
35
123
intended_time . to_f
36
124
else
37
- raise SchedulingError . new ( 'seconds must be greater than zero' ) if intended_time . to_f <= 0.0
125
+ raise ArgumentError . new ( 'seconds must be greater than zero' ) if intended_time . to_f < 0.0
38
126
now . to_f + intended_time . to_f
39
127
end
40
128
end
41
129
42
- def check_processing_thread
43
- if @thread && @thread . status == 'sleep'
44
- @thread . wakeup
45
- elsif @thread . nil? || ! @thread . alive?
46
- @thread = Thread . new do
47
- Thread . current . abort_on_exception = false
48
- process_tasks
130
+ # Check the status of the processing thread. This thread is responsible
131
+ # for monitoring the internal task queue and sending tasks to the
132
+ # executor when it is time for them to be processed. If there is no
133
+ # processing thread one will be created. If the processing thread is
134
+ # sleeping it will be worken up. If the processing thread has died it
135
+ # will be garbage collected and a new one will be created.
136
+ #
137
+ # @!visibility private
138
+ def check_processing_thread!
139
+ @mutex . synchronize do
140
+ return if shutdown? || @queue . empty?
141
+ if @thread && @thread . status == 'sleep'
142
+ @thread . wakeup
143
+ elsif @thread . nil? || ! @thread . alive?
144
+ @thread = Thread . new do
145
+ Thread . current . abort_on_exception = false
146
+ process_tasks
147
+ end
49
148
end
50
149
end
51
150
end
52
151
152
+ # Check the head of the internal task queue for a ready task.
153
+ #
154
+ # @return [Task] the next task to be executed or nil if none are ready
155
+ #
156
+ # @!visibility private
53
157
def next_task
54
158
@mutex . synchronize do
55
159
unless @queue . empty? || @queue . peek . time > Time . now . to_f
@@ -60,6 +164,13 @@ def next_task
60
164
end
61
165
end
62
166
167
+ # Calculate the time difference, in seconds and milliseconds, between
168
+ # now and the intended execution time of the next task to be ececuted.
169
+ #
170
+ # @return [Integer] the number of seconds and milliseconds to sleep
171
+ # or nil if the task queue is empty
172
+ #
173
+ # @!visibility private
63
174
def next_sleep_interval
64
175
@mutex . synchronize do
65
176
if @queue . empty?
@@ -70,6 +181,12 @@ def next_sleep_interval
70
181
end
71
182
end
72
183
184
+ # Run a loop and execute tasks in the scheduled order and at the approximate
185
+ # shceduled time. If no tasks remain the thread will exit gracefully so that
186
+ # garbage collection can occur. If there are no ready tasks it will sleep
187
+ # for up to 60 seconds waiting for the next scheduled task.
188
+ #
189
+ # @!visibility private
73
190
def process_tasks
74
191
loop do
75
192
while task = next_task do
0 commit comments