concurrent-ruby 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,28 @@
1
+ require 'concurrent/actor'
2
+ require 'concurrent/stoppable'
3
+
4
+ module Concurrent
5
+
6
+ class Channel < Actor
7
+ include Stoppable
8
+
9
+ def initialize(&block)
10
+ raise ArgumentError.new('no block given') unless block_given?
11
+ super()
12
+ @task = block
13
+ end
14
+
15
+ protected
16
+
17
+ def on_stop # :nodoc:
18
+ before_stop_proc.call if before_stop_proc
19
+ super
20
+ end
21
+
22
+ private
23
+
24
+ def act(*message)
25
+ return @task.call(*message)
26
+ end
27
+ end
28
+ end
@@ -1,7 +1,24 @@
1
1
  module Concurrent
2
2
 
3
+ # Object references in Ruby are mutable. This can lead to serious problems when
4
+ # the `#value` of a concurrent object is a mutable reference. Which is always the
5
+ # case unless the value is a `Fixnum`, `Symbol`, or similar "primitive" data type.
6
+ # Most classes in this library that expose a `#value` getter method do so using
7
+ # this mixin module.
3
8
  module Dereferenceable
4
9
 
10
+ # Set the options which define the operations #value performs before
11
+ # returning data to the caller (dereferencing).
12
+ #
13
+ # @note Many classes that include this module will call #set_deref_options
14
+ # from within the constructor, thus allowing these options to be set at
15
+ # object creation.
16
+ #
17
+ # @param [Hash] opts the options defining dereference behavior.
18
+ # @option opts [String] :dup_on_deref Call #dup before returning the data (default: false)
19
+ # @option opts [String] :freeze_on_deref Call #freeze before returning the data (default: false)
20
+ # @option opts [String] :copy_on_deref Call the given `Proc` passing the internal value and
21
+ # returning the value returned from the proc (default: `nil`)
5
22
  def set_deref_options(opts = {})
6
23
  mutex.synchronize do
7
24
  @dup_on_deref = opts[:dup_on_deref] || opts[:dup] || false
@@ -11,6 +28,8 @@ module Concurrent
11
28
  end
12
29
  end
13
30
 
31
+ # Return the value this object represents after applying the options specified
32
+ # by the #set_deref_options method.
14
33
  def value
15
34
  return nil if @value.nil?
16
35
  return @value if @do_nothing_on_deref
@@ -26,7 +45,8 @@ module Concurrent
26
45
 
27
46
  protected
28
47
 
29
- def mutex
48
+ # @private
49
+ def mutex # :nodoc:
30
50
  @mutex ||= Mutex.new
31
51
  end
32
52
  end
@@ -3,18 +3,37 @@ require 'concurrent/utilities'
3
3
 
4
4
  module Concurrent
5
5
 
6
+ # Old school kernel-style event reminiscent of Win32 programming in C++.
7
+ #
8
+ # When an `Event` is created it is in the `unset` state. Threads can choose to
9
+ # `#wait` on the event, blocking until released by another thread. When one
10
+ # thread wants to alert all blocking threads it calls the `#set` method which
11
+ # will then wake up all listeners. Once an `Event` has been set it remains set.
12
+ # New threads calling `#wait` will return immediately. An `Event` may be
13
+ # `#reset` at any time once it has been set.
14
+ #
15
+ # @see http://msdn.microsoft.com/en-us/library/windows/desktop/ms682655.aspx
6
16
  class Event
7
17
 
18
+ # Creates a new `Event` in the unset state. Threads calling `#wait` on the
19
+ # `Event` will block.
8
20
  def initialize
9
21
  @set = false
10
22
  @mutex = Mutex.new
11
23
  @waiters = []
12
24
  end
13
25
 
26
+ # Is the object in the set state?
27
+ #
28
+ # @return [Boolean] indicating whether or not the `Event` has been set
14
29
  def set?
15
30
  return @set == true
16
31
  end
17
32
 
33
+ # Trigger the event, setting the state to `set` and releasing all threads
34
+ # waiting on the event. Has no effect if the `Event` has already been set.
35
+ #
36
+ # @return [Boolean] should always return `true`
18
37
  def set
19
38
  return true if set?
20
39
  @mutex.synchronize do
@@ -24,11 +43,24 @@ module Concurrent
24
43
  return true
25
44
  end
26
45
 
46
+ # Reset a previously set event back to the `unset` state.
47
+ # Has no effect if the `Event` has not yet been set.
48
+ #
49
+ # @return [Boolean] should always return `true`
27
50
  def reset
28
- @mutex.synchronize { @set = false; @waiters.clear }
51
+ return true unless set?
52
+ @mutex.synchronize do
53
+ @set = false
54
+ @waiters.clear # just in case there's garbage
55
+ end
29
56
  return true
30
57
  end
31
58
 
59
+ # Wait a given number of seconds for the `Event` to be set by another
60
+ # thread. Will wait forever when no `timeout` value is given. Returns
61
+ # immediately if the `Event` has already been set.
62
+ #
63
+ # @return [Boolean] true if the `Event` was set before timeout else false
32
64
  def wait(timeout = nil)
33
65
  return true if set?
34
66
 
@@ -0,0 +1,98 @@
1
+ module Concurrent
2
+
3
+ module Postable
4
+
5
+ # @!visibility private
6
+ Package = Struct.new(:message, :handler, :notifier) # :nodoc:
7
+
8
+ # Sends a message to and returns. It's a fire-and-forget interaction.
9
+ #
10
+ # @param [Array] message one or more arguments representing a single message
11
+ # to be sent to the receiver.
12
+ #
13
+ # @return [Object] false when the message cannot be queued else the number
14
+ # of messages in the queue *after* this message has been post
15
+ #
16
+ # @raise ArgumentError when the message is empty
17
+ #
18
+ # @example
19
+ # class EchoActor < Concurrent::Actor
20
+ # def act(*message)
21
+ # p message
22
+ # end
23
+ # end
24
+ #
25
+ # echo = EchoActor.new
26
+ # echo.run!
27
+ #
28
+ # echo.post("Don't panic") #=> true
29
+ # #=> ["Don't panic"]
30
+ #
31
+ # echo.post(1, 2, 3, 4, 5) #=> true
32
+ # #=> [1, 2, 3, 4, 5]
33
+ #
34
+ # echo << "There's a frood who really knows where his towel is." #=> #<EchoActor:0x007fc8012b8448...
35
+ # #=> ["There's a frood who really knows where his towel is."]
36
+ def post(*message)
37
+ raise ArgumentError.new('empty message') if message.empty?
38
+ return false unless ready?
39
+ queue.push(Package.new(message))
40
+ return queue.length
41
+ end
42
+
43
+ def <<(message)
44
+ post(*message)
45
+ return self
46
+ end
47
+
48
+ def post?(*message)
49
+ raise ArgumentError.new('empty message') if message.empty?
50
+ return nil unless ready?
51
+ contract = Contract.new
52
+ queue.push(Package.new(message, contract))
53
+ return contract
54
+ end
55
+
56
+ def post!(seconds, *message)
57
+ raise ArgumentError.new('empty message') if message.empty?
58
+ raise Concurrent::Runnable::LifecycleError unless ready?
59
+ raise Concurrent::TimeoutError if seconds.to_f <= 0.0
60
+ event = Event.new
61
+ cback = Queue.new
62
+ queue.push(Package.new(message, cback, event))
63
+ if event.wait(seconds)
64
+ result = cback.pop
65
+ if result.is_a?(Exception)
66
+ raise result
67
+ else
68
+ return result
69
+ end
70
+ else
71
+ event.set # attempt to cancel
72
+ raise Concurrent::TimeoutError
73
+ end
74
+ end
75
+
76
+ def forward(receiver, *message)
77
+ raise ArgumentError.new('empty message') if message.empty?
78
+ return false unless ready?
79
+ queue.push(Package.new(message, receiver))
80
+ return queue.length
81
+ end
82
+
83
+ def ready?
84
+ if self.respond_to?(:running?) && ! running?
85
+ return false
86
+ else
87
+ return true
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ # @private
94
+ def queue # :nodoc:
95
+ @queue ||= Queue.new
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,20 @@
1
+ require 'concurrent/runnable'
2
+
3
+ module Concurrent
4
+
5
+ module Stoppable
6
+
7
+ def before_stop(&block)
8
+ raise ArgumentError.new('no block given') unless block_given?
9
+ raise Runnable::LifecycleError.new('#before_stop already set') if @before_stop_proc
10
+ @before_stop_proc = block
11
+ return self
12
+ end
13
+
14
+ protected
15
+
16
+ def before_stop_proc
17
+ return @before_stop_proc
18
+ end
19
+ end
20
+ end
@@ -3,32 +3,236 @@ require 'observer'
3
3
 
4
4
  require 'concurrent/dereferenceable'
5
5
  require 'concurrent/runnable'
6
+ require 'concurrent/stoppable'
6
7
  require 'concurrent/utilities'
7
8
 
8
9
  module Concurrent
9
10
 
11
+ # A very common currency pattern is to run a thread that performs a task at regular
12
+ # intervals. The thread that peforms the task sleeps for the given interval then
13
+ # wakes up and performs the task. Lather, rinse, repeat... This pattern causes two
14
+ # problems. First, it is difficult to test the business logic of the task becuse the
15
+ # task itself is tightly coupled with the concurrency logic. Second, an exception in
16
+ # raised while performing the task can cause the entire thread to abend. In a
17
+ # long-running application where the task thread is intended to run for days/weeks/years
18
+ # a crashed task thread can pose a significant problem. `TimerTask` alleviates both problems.
19
+ #
20
+ # When a `TimerTask` is launched it starts a thread for monitoring the execution interval.
21
+ # The `TimerTask` thread does not perform the task, however. Instead, the TimerTask
22
+ # launches the task on a separate thread. Should the task experience an unrecoverable
23
+ # crash only the task thread will crash. This makes the `TimerTask` very fault tolerant
24
+ # Additionally, the `TimerTask` thread can respond to the success or failure of the task,
25
+ # performing logging or ancillary operations. `TimerTask` can also be configured with a
26
+ # timeout value allowing it to kill a task that runs too long.
27
+ #
28
+ # One other advantage of `TimerTask` is it forces the bsiness logic to be completely decoupled
29
+ # from the concurrency logic. The business logic can be tested separately then passed to the
30
+ # `TimerTask` for scheduling and running.
31
+ #
32
+ # In some cases it may be necessary for a `TimerTask` to affect its own execution cycle.
33
+ # To facilitate this a reference to the task object is passed into the block as a block
34
+ # argument every time the task is executed.
35
+ #
36
+ # The `TimerTask` class includes the `Dereferenceable` mixin module so the result of
37
+ # the last execution is always available via the `#value` method. Derefencing options
38
+ # can be passed to the `TimerTask` during construction or at any later time using the
39
+ # `#set_deref_options` method.
40
+ #
41
+ # `TimerTask` supports notification through the Ruby standard library
42
+ # {http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html Observable}
43
+ # module. On execution the `TimerTask` will notify the observers
44
+ # with threes arguments: time of execution, the result of the block (or nil on failure),
45
+ # and any raised exceptions (or nil on success). If the timeout interval is exceeded
46
+ # the observer will receive a `Concurrent::TimeoutError` object as the third argument.
47
+ #
48
+ # @example Basic usage
49
+ # require 'concurrent'
50
+ #
51
+ # task = Concurrent::TimerTask.new{ puts 'Boom!' }
52
+ # task.run!
53
+ #
54
+ # task.execution_interval #=> 60 (default)
55
+ # task.timeout_interval #=> 30 (default)
56
+ #
57
+ # # wait 60 seconds...
58
+ # #=> 'Boom!'
59
+ #
60
+ # task.stop #=> true
61
+ #
62
+ # @example Configuring `:execution_interval` and `:timeout_interval`
63
+ # task = Concurrent::TimerTask.new(execution_interval: 5, timeout_interval: 5) do
64
+ # puts 'Boom!'
65
+ # end
66
+ #
67
+ # task.execution_interval #=> 5
68
+ # task.timeout_interval #=> 5
69
+ #
70
+ # @example Immediate execution with `:run_now`
71
+ # task = Concurrent::TimerTask.new(run_now: true){ puts 'Boom!' }
72
+ # task.run!
73
+ #
74
+ # #=> 'Boom!'
75
+ #
76
+ # @example Last `#value` and `Dereferenceable` mixin
77
+ # task = Concurrent::TimerTask.new(
78
+ # dup_on_deref: true,
79
+ # execution_interval: 5
80
+ # ){ Time.now }
81
+ #
82
+ # task.run!
83
+ # Time.now #=> 2013-11-07 18:06:50 -0500
84
+ # sleep(10)
85
+ # task.value #=> 2013-11-07 18:06:55 -0500
86
+ #
87
+ # @example Controlling execution from within the block
88
+ # timer_task = Concurrent::TimerTask.new(execution_interval: 1) do |task|
89
+ # task.execution_interval.times{ print 'Boom! ' }
90
+ # print "\n"
91
+ # task.execution_interval += 1
92
+ # if task.execution_interval > 5
93
+ # puts 'Stopping...'
94
+ # task.stop
95
+ # end
96
+ # end
97
+ #
98
+ # timer_task.run # blocking call - this task will stop itself
99
+ # #=> Boom!
100
+ # #=> Boom! Boom!
101
+ # #=> Boom! Boom! Boom!
102
+ # #=> Boom! Boom! Boom! Boom!
103
+ # #=> Boom! Boom! Boom! Boom! Boom!
104
+ # #=> Stopping...
105
+ #
106
+ # @example Observation
107
+ # class TaskObserver
108
+ # def update(time, result, ex)
109
+ # if result
110
+ # print "(#{time}) Execution successfully returned #{result}\n"
111
+ # elsif ex.is_a?(Concurrent::TimeoutError)
112
+ # print "(#{time}) Execution timed out\n"
113
+ # else
114
+ # print "(#{time}) Execution failed with error #{ex}\n"
115
+ # end
116
+ # end
117
+ # end
118
+ #
119
+ # task = Concurrent::TimerTask.new(execution_interval: 1, timeout_interval: 1){ 42 }
120
+ # task.add_observer(TaskObserver.new)
121
+ # task.run!
122
+ #
123
+ # #=> (2013-10-13 19:08:58 -0400) Execution successfully returned 42
124
+ # #=> (2013-10-13 19:08:59 -0400) Execution successfully returned 42
125
+ # #=> (2013-10-13 19:09:00 -0400) Execution successfully returned 42
126
+ # task.stop
127
+ #
128
+ # task = Concurrent::TimerTask.new(execution_interval: 1, timeout_interval: 1){ sleep }
129
+ # task.add_observer(TaskObserver.new)
130
+ # task.run!
131
+ #
132
+ # #=> (2013-10-13 19:07:25 -0400) Execution timed out
133
+ # #=> (2013-10-13 19:07:27 -0400) Execution timed out
134
+ # #=> (2013-10-13 19:07:29 -0400) Execution timed out
135
+ # task.stop
136
+ #
137
+ # task = Concurrent::TimerTask.new(execution_interval: 1){ raise StandardError }
138
+ # task.add_observer(TaskObserver.new)
139
+ # task.run!
140
+ #
141
+ # #=> (2013-10-13 19:09:37 -0400) Execution failed with error StandardError
142
+ # #=> (2013-10-13 19:09:38 -0400) Execution failed with error StandardError
143
+ # #=> (2013-10-13 19:09:39 -0400) Execution failed with error StandardError
144
+ # task.stop
145
+ #
146
+ # @see http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html
147
+ # @see http://docs.oracle.com/javase/7/docs/api/java/util/TimerTask.html
10
148
  class TimerTask
11
149
  include Dereferenceable
12
150
  include Runnable
151
+ include Stoppable
13
152
  include Observable
14
153
 
154
+ # Default `:execution_interval`
15
155
  EXECUTION_INTERVAL = 60
156
+
157
+ # Default `:timeout_interval`
16
158
  TIMEOUT_INTERVAL = 30
17
159
 
18
- attr_accessor :execution_interval
19
- attr_accessor :timeout_interval
160
+ # Number of seconds after the task completes before the task is
161
+ # performed again.
162
+ attr_reader :execution_interval
163
+
164
+ # Number of seconds the task can run before it is considered to have failed.
165
+ # Failed tasks are forcibly killed.
166
+ attr_reader :timeout_interval
20
167
 
168
+ # Create a new TimerTask with the given task and configuration.
169
+ #
170
+ # @param [Hash] opts the options defining task execution.
171
+ # @option opts [Integer] :execution_interval number of seconds between
172
+ # task executions (default: EXECUTION_INTERVAL)
173
+ # @option opts [Integer] :timeout_interval number of seconds a task can
174
+ # run before it is considered to have failed (default: TIMEOUT_INTERVAL)
175
+ # @option opts [Boolean] :run_now Whether to run the task immediately
176
+ # upon instanciation or to wait until the first #execution_interval
177
+ # has passed (default: false)
178
+ #
179
+ # @raise ArgumentError when no block is given.
180
+ #
181
+ # @yield to the block after :execution_interval seconds have passed since
182
+ # the last yield
183
+ # @yieldparam task a reference to the `TimerTask` instance so that the
184
+ # block can control its own lifecycle. Necessary since `self` will
185
+ # refer to the execution context of the block rather than the running
186
+ # `TimerTask`.
187
+ #
188
+ # @note Calls Concurrent::Dereferenceable#set_deref_options passing `opts`.
189
+ # All options supported by Concurrent::Dereferenceable can be set
190
+ # during object initialization.
191
+ #
192
+ # @see Concurrent::Dereferenceable#set_deref_options
21
193
  def initialize(opts = {}, &block)
22
194
  raise ArgumentError.new('no block given') unless block_given?
23
195
 
24
- @execution_interval = opts[:execution] || opts[:execution_interval] || EXECUTION_INTERVAL
25
- @timeout_interval = opts[:timeout] || opts[:timeout_interval] || TIMEOUT_INTERVAL
196
+ self.execution_interval = opts[:execution] || opts[:execution_interval] || EXECUTION_INTERVAL
197
+ self.timeout_interval = opts[:timeout] || opts[:timeout_interval] || TIMEOUT_INTERVAL
26
198
  @run_now = opts[:now] || opts[:run_now] || false
27
199
 
28
200
  @task = block
29
201
  set_deref_options(opts)
30
202
  end
31
203
 
204
+ # Number of seconds after the task completes before the task is
205
+ # performed again.
206
+ #
207
+ # @param [Float] value number of seconds
208
+ #
209
+ # @raise ArgumentError when value is non-numeric or not greater than zero
210
+ def execution_interval=(value)
211
+ if (value = value.to_f) <= 0.0
212
+ raise ArgumentError.new("'execution_interval' must be non-negative number")
213
+ end
214
+ @execution_interval = value
215
+ end
216
+
217
+ # Number of seconds the task can run before it is considered to have failed.
218
+ # Failed tasks are forcibly killed.
219
+ #
220
+ # @param [Float] value number of seconds
221
+ #
222
+ # @raise ArgumentError when value is non-numeric or not greater than zero
223
+ def timeout_interval=(value)
224
+ if (value = value.to_f) <= 0.0
225
+ raise ArgumentError.new("'timeout_interval' must be non-negative number")
226
+ end
227
+ @timeout_interval = value
228
+ end
229
+
230
+ # Terminate with extreme prejudice. Useful in cases where `#stop` doesn't
231
+ # work because one of the threads becomes unresponsive.
232
+ #
233
+ # @return [Boolean] indicating whether or not the `TimerTask` was killed
234
+ #
235
+ # @note Do not use this method unless `#stop` has failed.
32
236
  def kill
33
237
  return true unless running?
34
238
  mutex.synchronize do
@@ -48,16 +252,17 @@ module Concurrent
48
252
 
49
253
  protected
50
254
 
51
- def on_run
255
+ def on_run # :nodoc:
52
256
  @monitor = Thread.current
53
257
  end
54
258
 
55
- def on_stop
259
+ def on_stop # :nodoc:
260
+ before_stop_proc.call if before_stop_proc
56
261
  @monitor.wakeup if @monitor.alive?
57
262
  Thread.pass
58
263
  end
59
264
 
60
- def on_task
265
+ def on_task # :nodoc:
61
266
  if @run_now
62
267
  @run_now = false
63
268
  else
@@ -66,7 +271,7 @@ module Concurrent
66
271
  execute_task
67
272
  end
68
273
 
69
- def execute_task
274
+ def execute_task # :nodoc:
70
275
  @value = ex = nil
71
276
  @worker = Thread.new do
72
277
  Thread.current.abort_on_exception = false