thread_storm 0.5.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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