thread_storm 0.5.1 → 0.7.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.
data/CHANGELOG CHANGED
@@ -1,3 +1,15 @@
1
+ 0.6.0
2
+ - Fixed a bug with the :execute_blocks option.
3
+ - ThreadStorm::Execution#options (options specific to an execution).
4
+ - Added ThreadStorm#options (options specific to a ThreadStorm instance).
5
+ - Added ThreadStorm.options (global options).
6
+ - Removed ThreadStorm#size.
7
+ - Removed ThreadStorm#busy_workers.
8
+ - ThreadStorm#execution can now take an execution instance.
9
+ - ThreadStorm::Execution.new creates an execution in the :new state.
10
+ - Execution states (:new, :queued, :started, :finished)
11
+ - Changed Execution#duration to return nil if the execution is not in the :started or :finished state.
12
+
1
13
  0.5.1
2
14
  - Fixed crash when calling Execution#duration before it has started.
3
15
  - ThreadStorm#clear_executions can now take no arguments at all.
data/README.rdoc CHANGED
@@ -4,6 +4,8 @@ Simple thread pool with a few advanced features.
4
4
 
5
5
  == Features
6
6
 
7
+ Some notable features.
8
+
7
9
  * execution state querying
8
10
  * timeouts and configurable timeout implementation
9
11
  * graceful error handling
@@ -11,6 +13,8 @@ Simple thread pool with a few advanced features.
11
13
 
12
14
  == Example
13
15
 
16
+ A simple example to get you started.
17
+
14
18
  storm = ThreadStorm.new :size => 2
15
19
  storm.execute{ sleep(0.01); "a" }
16
20
  storm.execute{ sleep(0.01); "b" }
@@ -24,29 +28,49 @@ You can query the state of an execution.
24
28
 
25
29
  storm = ThreadStorm.new :size => 2
26
30
  execution = storm.execute{ sleep(0.01); "a" }
27
- storm.execute{ sleep(0.01); "b" }
28
- storm.execute{ sleep(0.01); "c" }
29
- storm.join
30
- execution.started? # true
31
- execution.finished? # true
32
- execution.timed_out? # false
33
- execution.duration # ~0.01
34
- execution.value # "a"
31
+ execution.join
32
+ execution.finished? # true
33
+
34
+
35
+ An execution can be in one of 4 states at any given time: +initialized+, +queued+, +started+, +finished+
36
+
37
+ Initialized means the execution has been created, but not yet scheduled to be run by the thread pool (i.e. ThreadStorm#execute hasn't been called on it yet).
38
+
39
+ Queued means the execution has been scheduled to run, but there are no free threads available to run it yet.
40
+
41
+ Started means that it is currently running on a thread.
42
+
43
+ Finished means it has completed running.
44
+
45
+ == Execution status
46
+
47
+ You can query the status of an execution.
48
+
49
+ storm = ThreadStorm.new :size => 2
50
+ execution = storm.execute{ sleep(0.01); "a" }
51
+ execution.join
52
+ execution.success? # true
53
+ execution.failure? # false
54
+ execution.timeout? # false
55
+
56
+ An execution can have one of three statuses after it has entered the +finished+ state: +success+, +failure+, +timeout+
57
+
58
+ Success means it finished without raising an exception.
59
+
60
+ Failure means it raised an exception.
61
+
62
+ Timeout means it ran longer than the timeout limit and was aborted.
35
63
 
36
64
  == Timeouts
37
65
 
38
66
  You can restrict how long executions are allowed to run for.
39
67
 
40
- storm = ThreadStorm.new :size => 2, :timeout => 0.02, :default_value => "failed"
41
- storm.execute{ sleep(0.01); "a" }
42
- storm.execute{ sleep(0.03); "b" }
43
- storm.execute{ sleep(0.01); "c" }
44
- storm.join
45
- storm.executions[1].started? # true
46
- storm.executions[1].finished? # true
47
- storm.executions[1].timed_out? # true
48
- storm.executions[1].duration # ~0.02
49
- storm.executions[1].value # "failed"
68
+ storm = ThreadStorm.new :size => 2, :timeout => 0.02
69
+ execution = storm.execute{ sleep(0.03); "b" }
70
+ execution.join
71
+ execution.finished? # true
72
+ execution.timeout? # true
73
+ executions.duration # ~0.02
50
74
 
51
75
  == Error handling
52
76
 
@@ -54,8 +78,9 @@ If an execution causes an exception, it will be reraised when ThreadStorm#join (
54
78
 
55
79
  storm = ThreadStorm.new :size => 2, :reraise => false, :default_value => "failure"
56
80
  execution = storm.execute{ raise("busted"); "a" }
57
- storm.join
58
- execution.value # "failure"
81
+ execution.join
82
+ execution.failure? # true
83
+ execution.value # "failure"
59
84
  execution.exception # RuntimeError: busted
60
85
 
61
86
  == Joining vs shutting down
@@ -74,20 +99,35 @@ Sometimes it can be a pain to remember to call #shutdown, so as a convenience, y
74
99
 
75
100
  == Configurable timeout method
76
101
 
77
- <tt>Timeout.timeout</tt> is unreliable in MRI 1.8.x. To address this, you can have ThreadStorm use an alternative implementation.
102
+ <tt>Timeout.timeout</tt> is unreliable in MRI 1.8. To address this, you can have ThreadStorm use an alternative implementation.
78
103
 
79
104
  require "system_timer"
80
105
  storm = ThreadStorm.new :timeout_method => SystemTimer.method(:timeout) do
81
106
  ...
82
107
  end
83
108
 
84
- The <tt>:timeout_method</tt> option takes any callable object (i.e. <tt>responds_to?(:call)</tt>) that implements something similar to <tt>Timeout.timeout</tt> (i.e. takes the same arguments and raises <tt>Timeout::Error</tt>).
109
+ The <tt>:timeout_method</tt> option takes any callable object that has the same signature as <tt>Timeout.timeout</tt>.
85
110
 
86
111
  require "system_timer"
87
112
  storm = ThreadStorm.new :timeout_method => Proc.new{ |seconds, &block| SystemTimer.timeout(seconds, &block) }
88
113
  ...
89
114
  end
90
115
 
116
+ == Caveats and Gotchas
117
+
118
+ This is tricky...
119
+
120
+ ThreadStorm.new do |s|
121
+ s.execute{ raise RuntimeError }
122
+ begin
123
+ s.join
124
+ rescue RuntimeError => e
125
+ puts "execution failed"
126
+ end
127
+ end
128
+
129
+ This will still raise an exception because ThreadStorm#join will be called again after the block is finished. This same problem happens with ThreadStorm#run.
130
+
91
131
  == Copyright
92
132
 
93
133
  Copyright (c) 2010 Christopher J. Bottaro. See LICENSE for details.
data/TODO ADDED
File without changes
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.1
1
+ 0.7.0
@@ -35,6 +35,10 @@ end
35
35
 
36
36
  class Object #:nodoc:
37
37
 
38
+ def metaclass
39
+ class << self; self; end
40
+ end
41
+
38
42
  def tap
39
43
  yield(self)
40
44
  self
@@ -3,92 +3,302 @@ require "monitor"
3
3
  class ThreadStorm
4
4
  # Encapsulates a unit of work to be sent to the thread pool.
5
5
  class Execution
6
- attr_writer :value, :exception #:nodoc:
7
- attr_reader :args, :block, :thread #:nodoc:
8
6
 
9
- def initialize(args, default_value, &block) #:nodoc:
10
- @args = args
11
- @value = default_value
12
- @block = block
13
- @start_time = nil
14
- @finish_time = nil
7
+ # When an execution has been created, but hasn't been scheduled to run.
8
+ STATE_INITIALIZED = 0
9
+ # When an execution has been scheduled to run but is waiting for an available thread.
10
+ STATE_QUEUED = 1
11
+ # When an execution is running on a thread.
12
+ STATE_STARTED = 2
13
+ # When an execution has finished running.
14
+ STATE_FINISHED = 3
15
+
16
+ # A hash mapping state symbols (:initialized, :queued, :started, :finished) to their
17
+ # corresponding state constant values.
18
+ STATE_SYMBOLS = {
19
+ :initialized => STATE_INITIALIZED,
20
+ :queued => STATE_QUEUED,
21
+ :started => STATE_STARTED,
22
+ :finished => STATE_FINISHED
23
+ }
24
+
25
+ # Inverted STATE_SYMBOLS.
26
+ STATE_SYMBOLS_INVERTED = STATE_SYMBOLS.invert
27
+
28
+ # The arguments passed into new or ThreadStorm#execute.
29
+ attr_reader :args
30
+
31
+ # The value of an execution's block.
32
+ attr_reader :value
33
+
34
+ # If an exception was raised when running an execution, it is stored here.
35
+ attr_reader :exception
36
+
37
+ # Options specific to an Execution instance. Note that you cannot modify
38
+ # the options once ThreadStorm#execute has been called on the execution.
39
+ attr_reader :options
40
+
41
+ attr_reader :block, :thread #:nodoc:
42
+
43
+ # call-seq:
44
+ # new(options = {}) -> Execution
45
+ # new(*args){ |*args| ... } -> Execution
46
+ #
47
+ # Create an execution. The execution will be in the :initialized state. Call
48
+ # ThreadStorm#execute to schedule the execution to be run and transition
49
+ # it into the :queued state.
50
+ #
51
+ # Default options come from the global <tt>ThreadStorm.options</tt>. If you want options specific
52
+ # to a ThreadStorm instance, use ThreadStorm#new_execution.
53
+ def initialize(*args, &block)
54
+ if block_given?
55
+ @args = args
56
+ @block = block
57
+ @options = ThreadStorm.options.dup
58
+ elsif args.length == 0
59
+ @args = []
60
+ @block = nil
61
+ @options = ThreadStorm.options.dup
62
+ elsif args.length == 1 and args.first.kind_of?(Hash)
63
+ @args = []
64
+ @block = nil
65
+ @options = ThreadStorm.options.merge(args.first)
66
+ else
67
+ raise ArgumentError, "illegal call-seq"
68
+ end
69
+
70
+ @state = nil
71
+ @state_at = []
72
+ @value = nil
15
73
  @exception = nil
16
- @timed_out = false
17
74
  @thread = nil
18
75
  @lock = Monitor.new
19
76
  @cond = @lock.new_cond
77
+ @callback_exceptions = {}
78
+
79
+ enter_state(:initialized)
20
80
  end
21
81
 
22
- def start! #:nodoc:
23
- @thread = Thread.current
24
- @start_time = Time.now
82
+ # This code:
83
+ # execution = ThreadStorm::Execution.new
84
+ # execution.define(1, 2, 3){ |a1, a2, a3| ... some code ... }
85
+ # Is equivalent to:
86
+ # ThreadStorm::Execution.new(1, 2, 3){ |a1, a2, a3| ... some code ... }
87
+ # The advantage is that you can use the first form of Execution.new to pass in options.
88
+ def define(*args, &block)
89
+ @args = args
90
+ @block = block
91
+ self
25
92
  end
26
93
 
27
- # True if this execution has started running.
28
- def started?
29
- !!start_time
94
+ # Returns the state of an execution. If _how_ is set to :sym, returns the state as symbol.
95
+ def state(how = :const)
96
+ if how == :sym
97
+ STATE_SYMBOLS_INVERTED[@state] or raise RuntimeError, "invalid state: #{@state.inspect}"
98
+ else
99
+ @state
100
+ end
30
101
  end
31
102
 
32
- # When this execution began to run.
33
- def start_time
34
- @start_time
103
+ # Returns true if the execution is currently in the given state.
104
+ # _state_ can be either a state constant or symbol.
105
+ def state?(state)
106
+ self.state == state_to_const(state)
35
107
  end
36
108
 
37
- def finish! #:nodoc:
38
- @lock.synchronize do
39
- @finish_time = Time.now
40
- @cond.signal
41
- end
109
+ # Returns true if the execution is currently in the :initialized state.
110
+ def initialized?
111
+ state?(STATE_INITIALIZED)
42
112
  end
43
113
 
44
- # True if this execution has finished running.
114
+ # Returns true if the execution is currently in the :queued state.
115
+ def queued?
116
+ state?(STATE_QUEUED)
117
+ end
118
+
119
+ # Returns true if the execution is currently in the :started state.
120
+ def started?
121
+ state?(STATE_STARTED)
122
+ end
123
+
124
+ # Returns true if the execution is currently in the :finished state.
45
125
  def finished?
46
- !!finish_time
126
+ state?(STATE_FINISHED)
127
+ end
128
+
129
+ # Returns the time when the execution entered the given state.
130
+ # _state_ can be either a state constant or symbol.
131
+ def state_at(state)
132
+ @state_at[state_to_const(state)]
47
133
  end
48
134
 
49
- # When this execution finished running (either cleanly or with error).
50
- def finish_time
51
- @finish_time
135
+ # When this execution entered the :initialized state.
136
+ def initialized_at
137
+ state_at(:initialized)
52
138
  end
53
139
 
54
- # How long this this execution ran for (i.e. finish_time - start_time)
55
- # or if it hasn't finished, how long it has been running for.
56
- def duration
57
- if finished?
58
- finish_time - start_time
59
- elsif started?
60
- Time.now - start_time
140
+ # When this execution entered the :queued state.
141
+ def queued_at
142
+ state_at(:queued)
143
+ end
144
+
145
+ # When this execution entered the :started state.
146
+ def started_at
147
+ state_at(:started)
148
+ end
149
+
150
+ # When this execution entered the :finished state.
151
+ def finished_at
152
+ state_at(:finished)
153
+ end
154
+
155
+ # How long an execution was (or has been) in a given state.
156
+ # _state_ can be either a state constant or symbol.
157
+ def duration(state = :started)
158
+ state = state_to_const(state)
159
+ if state == @state
160
+ Time.now - state_at(state)
161
+ elsif state < @state and state_at(state)
162
+ next_state_at(state) - state_at(state)
61
163
  else
62
- -1
164
+ nil
63
165
  end
64
166
  end
65
167
 
66
- def timed_out! #:nodoc:
67
- @timed_out = true
168
+ # This is soley for ThreadStorm to put the execution into the queued state.
169
+ def queued! #:nodoc:
170
+ options.freeze
171
+ enter_state(STATE_QUEUED)
68
172
  end
69
173
 
174
+ def execute #:nodoc:
175
+ timeout = options[:timeout]
176
+ timeout_method = options[:timeout_method]
177
+ timeout_exception = options[:timeout_exception]
178
+ default_value = options[:default_value]
179
+
180
+ @thread = Thread.current
181
+ enter_state(STATE_STARTED)
182
+
183
+ begin
184
+ timeout_method.call(timeout){ @value = @block.call(*args) }
185
+ rescue timeout_exception => e
186
+ @exception = e
187
+ @value = default_value
188
+ rescue Exception => e
189
+ @exception = e
190
+ @value = default_value
191
+ ensure
192
+ enter_state(STATE_FINISHED)
193
+ end
194
+ end
195
+
196
+ # True if the execution finished without failure (exception) or timeout.
197
+ def success?
198
+ !exception? and !timeout?
199
+ end
200
+
201
+ # True if this execution raised an exception.
202
+ def failure?
203
+ !!@exception and !timeout?
204
+ end
205
+
206
+ # Deprecated... for backwards compatibility.
207
+ alias_method :exception?, :failure? #:nodoc:
208
+
70
209
  # True if the execution went over the timeout limit.
71
- def timed_out?
72
- !!@timed_out
210
+ def timeout?
211
+ !!@exception and @exception.kind_of?(options[:timeout_exception])
73
212
  end
74
213
 
75
- # Block until this execution has finished running.
76
- def join
77
- @lock.synchronize do
78
- @cond.wait_until{ finished? }
214
+ # Deprecated... for backwards compatibility.
215
+ alias_method :timed_out?, :timeout? #:nodoc:
216
+
217
+ def callback_exception?(state = nil)
218
+ ![nil, {}].include?(callback_exception(state))
219
+ end
220
+
221
+ def callback_exception(state = nil)
222
+ if state
223
+ @callback_exceptions[state]
224
+ else
225
+ @callback_exceptions
79
226
  end
80
227
  end
81
228
 
82
- # If this execution finished with an exception, it is stored here.
83
- def exception
84
- @exception
229
+ # Block until this execution has finished running.
230
+ def join
231
+ @lock.synchronize{ @cond.wait_until{ finished? } }
232
+ raise exception if exception? and options[:reraise]
233
+ true
85
234
  end
86
235
 
87
236
  # The value returned by the execution's code block.
88
237
  # This implicitly calls join.
89
238
  def value
90
- join
91
- @value
239
+ join and @value
240
+ end
241
+
242
+ private
243
+
244
+ # Enters _state_ doing some error checking, callbacks, and special case for entering the finished state.
245
+ def enter_state(state) #:nodoc:
246
+ state = state_to_const(state)
247
+ raise RuntimeError, "invalid state transition from #{@state} to #{state}" unless @state.nil? or state > @state
248
+
249
+ # We need state changes and callbacks to be atomic so that if we query a state change
250
+ # we can be sure that its corresponding callback has finished running as well. Thus
251
+ # we need to make sure to synchronize querying state (see #state).
252
+
253
+ handle_callback(state)
254
+
255
+ @lock.synchronize do
256
+ do_enter_state(state)
257
+ @cond.broadcast if state == STATE_FINISHED # Wake any threads that called join and are waiting.
258
+ end
259
+ end
260
+
261
+ # Enters _state_ and set records the time.
262
+ def do_enter_state(state)
263
+ @state = state
264
+ @state_at[@state] = Time.now
265
+ end
266
+
267
+ def handle_callback(state)
268
+ state = state_to_sym(state)
269
+ callback = options["#{state}_callback".to_sym]
270
+ return unless callback
271
+ begin
272
+ callback.call(self)
273
+ rescue Exception => e
274
+ @callback_exceptions[state] = e
275
+ end
276
+ end
277
+
278
+ # Finds the next state from _state_ that has a state_at time.
279
+ # Ex:
280
+ # [0:10, nil, 0:15, 0:20]
281
+ # next_state_at(0) -> 0:15
282
+ def next_state_at(state)
283
+ @state_at[state+1..-1].detect{ |time| !time.nil? }
284
+ end
285
+
286
+ # Normalizes _state_ to a constant (integer).
287
+ def state_to_const(state)
288
+ if state.kind_of?(Symbol)
289
+ STATE_SYMBOLS[state]
290
+ else
291
+ state
292
+ end
293
+ end
294
+
295
+ # Normalizes _state_ to a symbol.
296
+ def state_to_sym(state)
297
+ if state.kind_of?(Symbol)
298
+ state
299
+ else
300
+ STATE_SYMBOLS_INVERTED[state]
301
+ end
92
302
  end
93
303
 
94
304
  end
@@ -0,0 +1,69 @@
1
+ require "monitor"
2
+
3
+ class ThreadStorm
4
+ # This is tricky... we need to maintain both real queue size and fake queue size.
5
+ # If we use just the real queue size alone, then we will see the following
6
+ # (incorrect) behavior:
7
+ # storm = ThreadStorm.new :size => 2, :execute_blocks => true
8
+ # storm.execute{ sleep }
9
+ # storm.execute{ sleep }
10
+ # storm.execute{ sleep } # Doesn't block, but should.
11
+ # storm.execute{ sleep } # Finally blocks.
12
+ # The reason is that popping the queue (and thus decrementing its size) does not
13
+ # imply that the worker thread has actually finished the execution and is ready to
14
+ # accept another one.
15
+ class Queue #:nodoc:
16
+
17
+ def initialize(max_size, enqueue_blocks)
18
+ @max_size = max_size
19
+ @enqueue_blocks = enqueue_blocks
20
+ @size = 0
21
+ @array = []
22
+ @lock = Monitor.new
23
+ @cond1 = @lock.new_cond # Wish I could come up with better names.
24
+ @cond2 = @lock.new_cond
25
+ end
26
+
27
+ def synchronize(&block)
28
+ @lock.synchronize{ yield(self) }
29
+ end
30
+
31
+ # +enqueue+ needs to wait on the fake size, otherwise @max_size+1 calls to
32
+ # +enqueue+ could be made when @enqueue_blocks is true.
33
+ def enqueue(item)
34
+ @lock.synchronize do
35
+ @cond2.wait_until{ @size < @max_size } if @enqueue_blocks
36
+ @size += 1
37
+ @array << item
38
+ @cond1.broadcast
39
+ end
40
+ end
41
+
42
+ # +dequeue+ needs to wait until the real size, otherwise a single call to
43
+ # +enqueue+ could result to multiple successful calls to +dequeue+ before
44
+ # a call to +decr_size+ is made.
45
+ def dequeue
46
+ @lock.synchronize do
47
+ @cond1.wait_until{ @array.size > 0 }
48
+ @array.shift
49
+ end
50
+ end
51
+
52
+ # Decrement the fake size, thus signaling that we're ready to call +enqueue+.
53
+ def decr_size
54
+ @lock.synchronize do
55
+ @size -= 1 unless @size == 0
56
+ @cond2.broadcast
57
+ end
58
+ end
59
+
60
+ def shutdown
61
+ @lock.synchronize do
62
+ @array = [nil] * @max_size
63
+ @size = @max_size
64
+ @cond1.broadcast
65
+ end
66
+ end
67
+
68
+ end
69
+ end
@@ -1,76 +1,18 @@
1
1
  class ThreadStorm
2
2
  class Worker #:nodoc:
3
- attr_reader :thread, :execution
3
+ attr_reader :thread
4
4
 
5
- # Takes the threadsafe queue and options from the thread pool.
6
- def initialize(queue, sentinel, options)
5
+ def initialize(queue)
7
6
  @queue = queue
8
- @sentinel = sentinel
9
- @options = options
10
- @execution = nil # Current execution we're working on.
11
7
  @thread = Thread.new(self){ |me| me.run }
12
8
  end
13
9
 
14
- def timeout
15
- @timeout ||= @options[:timeout]
16
- end
17
-
18
- def timeout_method
19
- @timeout_method ||= @options[:timeout_method]
20
- end
21
-
22
10
  # Pop executions and process them until we're signaled to die.
23
11
  def run
24
- pop_and_process_execution while not die?
25
- end
26
-
27
- # Pop an execution off the queue and process it, or pass off control to a different thread.
28
- def pop_and_process_execution
29
- @sentinel.synchronize do |e_cond, p_cond|
30
- # Become idle and signal that we're idle.
31
- @execution = nil
32
- e_cond.signal
33
-
34
- # Give up the lock and wait until there is work to do.
35
- p_cond.wait_while{ @queue.empty? }
36
-
37
- # Get the work to do (implicitly becoming busy).
38
- @execution = @queue.pop
12
+ while (execution = @queue.dequeue)
13
+ execution.execute
14
+ @queue.decr_size
39
15
  end
40
-
41
- process_execution_with_timeout unless die?
42
- end
43
-
44
- # Process the execution, handling timeouts and exceptions.
45
- def process_execution_with_timeout
46
- execution.start!
47
- begin
48
- if timeout
49
- timeout_method.call(timeout){ process_execution }
50
- else
51
- process_execution
52
- end
53
- rescue Timeout::Error => e
54
- execution.timed_out!
55
- rescue Exception => e
56
- execution.exception = e
57
- ensure
58
- execution.finish!
59
- end
60
- end
61
-
62
- # Seriously, process the execution.
63
- def process_execution
64
- execution.value = execution.block.call(*execution.args)
65
- end
66
-
67
- def busy?
68
- !!@execution and not die?
69
- end
70
-
71
- # True if this worker's thread should die.
72
- def die?
73
- @execution == :die
74
16
  end
75
17
 
76
18
  end