concurrent-ruby 0.3.0.pre.2 → 0.3.0.pre.3
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 +4 -4
- data/README.md +15 -8
- data/lib/concurrent.rb +1 -1
- data/lib/concurrent/runnable.rb +17 -25
- data/lib/concurrent/supervisor.rb +1 -1
- data/lib/concurrent/{executor.rb → timer_task.rb} +13 -19
- data/lib/concurrent/version.rb +1 -1
- data/md/timer_task.md +151 -0
- data/spec/concurrent/fixed_thread_pool_spec.rb +2 -13
- data/spec/concurrent/runnable_spec.rb +18 -17
- data/spec/concurrent/thread_pool_shared.rb +1 -0
- data/spec/concurrent/timer_task_spec.rb +195 -0
- metadata +6 -6
- data/md/executor.md +0 -193
- data/spec/concurrent/executor_spec.rb +0 -240
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0823ac7a2cf530803f04b071254f156c32a840a7
|
4
|
+
data.tar.gz: 74a9edb10d10210746b53cb87dec1bdff8c1a971
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f681fd88bf4a4aa2d0a1f186f48fe839ef370c39f08fe3fa9527c539668b02a3d50581e4b8e7bac11212e726231eb7f59c7671447b3b78c87ce6d67037cc6ecd
|
7
|
+
data.tar.gz: 856d32dac93217664581bfd4958d5daee48b5c0525bf4ca6307a83aeef912e7f0b2a441c3fd2c410e00041191af98ec163ac2a5ee9965aa8fb362fec1298f051
|
data/README.md
CHANGED
@@ -59,8 +59,8 @@ Several features from Erlang, Go, Clojure, Java, and JavaScript have been implem
|
|
59
59
|
* Go inspired [Goroutine](https://github.com/jdantonio/concurrent-ruby/blob/master/md/goroutine.md)
|
60
60
|
* JavaScript inspired [Promise](https://github.com/jdantonio/concurrent-ruby/blob/master/md/promise.md)
|
61
61
|
* Java inspired [Thread Pools](https://github.com/jdantonio/concurrent-ruby/blob/master/md/thread_pool.md)
|
62
|
-
* Old school [events](http://msdn.microsoft.com/en-us/library/windows/desktop/ms682655
|
63
|
-
*
|
62
|
+
* Old school [events](http://msdn.microsoft.com/en-us/library/windows/desktop/ms682655.aspx) from back in my Visual C++ days
|
63
|
+
* Repeated task execution with Java inspired [TimerTask](https://github.com/jdantonio/concurrent-ruby/blob/master/md/timer_task.md) service
|
64
64
|
* Erlang inspired [Supervisor](https://github.com/jdantonio/concurrent-ruby/blob/master/md/supervisor.md) for managing long-running threads
|
65
65
|
|
66
66
|
### Is it any good?
|
@@ -199,25 +199,32 @@ sleep(1)
|
|
199
199
|
#=> Zap!
|
200
200
|
```
|
201
201
|
|
202
|
-
####
|
202
|
+
#### TimerTask
|
203
203
|
|
204
204
|
```ruby
|
205
205
|
require 'concurrent'
|
206
206
|
|
207
|
-
ec = Concurrent::
|
207
|
+
ec = Concurrent::TimerTask.run{ puts 'Boom!' }
|
208
208
|
|
209
|
-
ec.
|
210
|
-
ec.
|
211
|
-
ec.timeout_interval #=> 30 == Concurrent::Executor::TIMEOUT_INTERVAL
|
209
|
+
ec.execution_interval #=> 60 == Concurrent::TimerTask::EXECUTION_INTERVAL
|
210
|
+
ec.timeout_interval #=> 30 == Concurrent::TimerTask::TIMEOUT_INTERVAL
|
212
211
|
ec.status #=> "sleep"
|
213
212
|
|
214
213
|
# wait 60 seconds...
|
215
214
|
#=> 'Boom!'
|
216
|
-
#=> ' INFO (2013-08-02 23:20:15) Foo: execution completed successfully'
|
217
215
|
|
218
216
|
ec.kill #=> true
|
219
217
|
```
|
220
218
|
|
219
|
+
## Todo
|
220
|
+
|
221
|
+
* DelayedTask
|
222
|
+
* More methods from Scala's Actor
|
223
|
+
* More Erlang goodness
|
224
|
+
* gen_server
|
225
|
+
* gen_event
|
226
|
+
* gen_fsm
|
227
|
+
|
221
228
|
## Contributing
|
222
229
|
|
223
230
|
1. Fork it
|
data/lib/concurrent.rb
CHANGED
@@ -4,13 +4,13 @@ require 'concurrent/version'
|
|
4
4
|
require 'concurrent/actor'
|
5
5
|
require 'concurrent/agent'
|
6
6
|
require 'concurrent/event'
|
7
|
-
require 'concurrent/executor'
|
8
7
|
require 'concurrent/future'
|
9
8
|
require 'concurrent/goroutine'
|
10
9
|
require 'concurrent/obligation'
|
11
10
|
require 'concurrent/promise'
|
12
11
|
require 'concurrent/runnable'
|
13
12
|
require 'concurrent/supervisor'
|
13
|
+
require 'concurrent/timer_task'
|
14
14
|
require 'concurrent/utilities'
|
15
15
|
|
16
16
|
require 'concurrent/global_thread_pool'
|
data/lib/concurrent/runnable.rb
CHANGED
@@ -2,7 +2,9 @@ require 'thread'
|
|
2
2
|
|
3
3
|
module Concurrent
|
4
4
|
|
5
|
-
module
|
5
|
+
module Runnable
|
6
|
+
|
7
|
+
LifecycleError = Class.new(StandardError)
|
6
8
|
|
7
9
|
class Context
|
8
10
|
attr_reader :runner, :thread
|
@@ -12,28 +14,11 @@ module Concurrent
|
|
12
14
|
Thread.abort_on_exception = false
|
13
15
|
runner.run
|
14
16
|
end
|
15
|
-
#HACK: more consistent on JRuby and Rbx
|
16
|
-
sleep(0.1)
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
20
|
def self.included(base)
|
21
21
|
|
22
|
-
def run!
|
23
|
-
return mutex.synchronize do
|
24
|
-
raise LifecycleError.new('already running') if @running
|
25
|
-
Context.new(self)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
protected
|
30
|
-
|
31
|
-
def mutex
|
32
|
-
@mutex ||= Mutex.new
|
33
|
-
end
|
34
|
-
|
35
|
-
public
|
36
|
-
|
37
22
|
class << base
|
38
23
|
|
39
24
|
def run!(*args, &block)
|
@@ -44,14 +29,15 @@ module Concurrent
|
|
44
29
|
end
|
45
30
|
end
|
46
31
|
end
|
47
|
-
end
|
48
|
-
|
49
|
-
module Runnable
|
50
|
-
|
51
|
-
LifecycleError = Class.new(StandardError)
|
52
32
|
|
53
|
-
def
|
54
|
-
|
33
|
+
def run!(abort_on_exception = false)
|
34
|
+
raise LifecycleError.new('already running') if @running
|
35
|
+
thread = Thread.new{
|
36
|
+
Thread.current.abort_on_exception = abort_on_exception
|
37
|
+
self.run
|
38
|
+
}
|
39
|
+
Thread.pass
|
40
|
+
return thread
|
55
41
|
end
|
56
42
|
|
57
43
|
def run
|
@@ -93,5 +79,11 @@ module Concurrent
|
|
93
79
|
def running?
|
94
80
|
return @running == true
|
95
81
|
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
|
85
|
+
def mutex
|
86
|
+
@mutex ||= Mutex.new
|
87
|
+
end
|
96
88
|
end
|
97
89
|
end
|
@@ -280,7 +280,7 @@ module Concurrent
|
|
280
280
|
|
281
281
|
def exceeded_max_restart_frequency?
|
282
282
|
@restart_times.unshift(Time.now.to_i)
|
283
|
-
diff =
|
283
|
+
diff = (@restart_times.first - @restart_times.last).abs
|
284
284
|
if @restart_times.length >= @max_restart && diff <= @max_time
|
285
285
|
return true
|
286
286
|
elsif diff >= @max_time
|
@@ -1,30 +1,27 @@
|
|
1
1
|
require 'thread'
|
2
|
+
require 'observer'
|
3
|
+
|
2
4
|
require 'concurrent/runnable'
|
5
|
+
require 'concurrent/utilities'
|
3
6
|
|
4
7
|
module Concurrent
|
5
8
|
|
6
|
-
class
|
9
|
+
class TimerTask
|
7
10
|
include Runnable
|
11
|
+
include Observable
|
8
12
|
|
9
13
|
EXECUTION_INTERVAL = 60
|
10
14
|
TIMEOUT_INTERVAL = 30
|
11
15
|
|
12
|
-
STDOUT_LOGGER = proc do |name, level, msg|
|
13
|
-
print "%5s (%s) %s: %s\n" % [level.upcase, Time.now.strftime("%F %T"), name, msg]
|
14
|
-
end
|
15
|
-
|
16
|
-
attr_reader :name
|
17
16
|
attr_reader :execution_interval
|
18
17
|
attr_reader :timeout_interval
|
19
18
|
|
20
|
-
def initialize(
|
19
|
+
def initialize(opts = {}, &block)
|
21
20
|
raise ArgumentError.new('no block given') unless block_given?
|
22
21
|
|
23
|
-
@name = name
|
24
22
|
@execution_interval = opts[:execution] || opts[:execution_interval] || EXECUTION_INTERVAL
|
25
23
|
@timeout_interval = opts[:timeout] || opts[:timeout_interval] || TIMEOUT_INTERVAL
|
26
24
|
@run_now = opts[:now] || opts[:run_now] || false
|
27
|
-
@logger = opts[:logger] || STDOUT_LOGGER
|
28
25
|
@block_args = opts[:args] || opts [:arguments] || []
|
29
26
|
|
30
27
|
@task = block
|
@@ -45,9 +42,7 @@ module Concurrent
|
|
45
42
|
end
|
46
43
|
alias_method :terminate, :kill
|
47
44
|
|
48
|
-
|
49
|
-
return @monitor.status unless @monitor.nil?
|
50
|
-
end
|
45
|
+
alias_method :cancel, :stop
|
51
46
|
|
52
47
|
protected
|
53
48
|
|
@@ -72,15 +67,14 @@ module Concurrent
|
|
72
67
|
def execute_task
|
73
68
|
@worker = Thread.new do
|
74
69
|
Thread.current.abort_on_exception = false
|
75
|
-
@task.call(*@block_args)
|
76
|
-
end
|
77
|
-
if @worker.join(@timeout_interval).nil?
|
78
|
-
@logger.call(@name, :warn, "execution timed out after #{@timeout_interval} seconds")
|
79
|
-
else
|
80
|
-
@logger.call(@name, :info, 'execution completed successfully')
|
70
|
+
Thread.current[:result] = @task.call(*@block_args)
|
81
71
|
end
|
72
|
+
raise TimeoutError if @worker.join(@timeout_interval).nil?
|
73
|
+
changed
|
74
|
+
notify_observers(Time.now, @worker[:result], nil)
|
82
75
|
rescue Exception => ex
|
83
|
-
|
76
|
+
changed
|
77
|
+
notify_observers(Time.now, nil, ex)
|
84
78
|
ensure
|
85
79
|
unless @worker.nil?
|
86
80
|
Thread.kill(@worker)
|
data/lib/concurrent/version.rb
CHANGED
data/md/timer_task.md
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
# Being of Sound Mind
|
2
|
+
|
3
|
+
A very common currency pattern is to run a thread that performs a task at regular
|
4
|
+
intervals. The thread that peforms the task sleeps for the given interval then
|
5
|
+
waked up and performs the task. Later, rinse, repeat... This pattern causes two
|
6
|
+
problems. First, it is difficult to test the business logic of the task becuse the
|
7
|
+
task itself is tightly couple with the threading. Second, an exception in the task
|
8
|
+
can cause the entire thread to abend. In a long-running application where the task
|
9
|
+
thread is intended to run for days/weeks/years a crashed task thread can pose a real
|
10
|
+
problem. The `TimerTask` class alleviates both problems.
|
11
|
+
|
12
|
+
When a TimerTask is launched it starts a thread for monitoring the execution interval.
|
13
|
+
The TimerTask thread does not perform the task, however. Instead, the TimerTask
|
14
|
+
launches the task on a separat thread. The advantage of this approach is that if
|
15
|
+
the task crashes it will only kill the task thread, not the TimerTask thread. The
|
16
|
+
TimerTask thread can then log the success or failure of the task. The TimerTask
|
17
|
+
can even be configured with a timeout value allowing it to kill a task that runs
|
18
|
+
to long and then log the error.
|
19
|
+
|
20
|
+
One other advantage of the `TimerTask` class is that it forces the bsiness logic to
|
21
|
+
be completely decoupled from the threading logic. The business logic can be tested
|
22
|
+
separately then passed to the a TimerTask for scheduling and running.
|
23
|
+
|
24
|
+
The `TimerTask` is the yin to to the
|
25
|
+
[Supervisor's](https://github.com/jdantonio/concurrent-ruby/blob/master/md/supervisor.md)
|
26
|
+
yang. Where the `Supervisor` is intended to manage long-running threads that operate
|
27
|
+
continuously, the `TimerTask` is intended to manage fairly short operations that
|
28
|
+
occur repeatedly at regular intervals.
|
29
|
+
|
30
|
+
Unlike some of the others concurrency objects in the library, TimerTasks do not
|
31
|
+
run on the global thread pool. In my experience the types of tasks that will benefit
|
32
|
+
from the `TimerTask` class tend to also be long running. For this reason they get
|
33
|
+
their own thread every time the task is executed.
|
34
|
+
|
35
|
+
## Observation
|
36
|
+
|
37
|
+
`TimerTask` supports notification through the Ruby standard library
|
38
|
+
[Observable](http://ruby-doc.org/stdlib-1.9.3/libdoc/observer/rdoc/Observable.html)
|
39
|
+
module. On execution the `TimerTask` will notify the observers with thress arguments:
|
40
|
+
time of execution, the result of the block (or nil on failure), and any raised
|
41
|
+
exceptions (or nil on success). If the timeout interval is exceeded the observer
|
42
|
+
will receive a `Concurrent::TimeoutError` object as the third argument.
|
43
|
+
|
44
|
+
## Examples
|
45
|
+
|
46
|
+
A basic example:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
require 'concurrent'
|
50
|
+
|
51
|
+
ec = Concurrent::TimerTask.run{ puts 'Boom!' }
|
52
|
+
|
53
|
+
ec.execution_interval #=> 60 == Concurrent::TimerTask::EXECUTION_INTERVAL
|
54
|
+
ec.timeout_interval #=> 30 == Concurrent::TimerTask::TIMEOUT_INTERVAL
|
55
|
+
ec.status #=> "sleep"
|
56
|
+
|
57
|
+
# wait 60 seconds...
|
58
|
+
#=> 'Boom!'
|
59
|
+
|
60
|
+
ec.kill #=> true
|
61
|
+
```
|
62
|
+
|
63
|
+
Both the execution_interval and the timeout_interval can be configured:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
ec = Concurrent::TimerTask.run(execution_interval: 5, timeout_interval: 5) do
|
67
|
+
puts 'Boom!'
|
68
|
+
end
|
69
|
+
|
70
|
+
ec.runner.execution_interval #=> 5
|
71
|
+
ec.runner.timeout_interval #=> 5
|
72
|
+
```
|
73
|
+
|
74
|
+
By default an `TimerTask` will wait for `:execution_interval` seconds before running the block.
|
75
|
+
To run the block immediately set the `:run_now` option to `true`:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
ec = Concurrent::TimerTask.run(run_now: true){ puts 'Boom!' }
|
79
|
+
#=> 'Boom!''
|
80
|
+
ec.thread.status #=> "sleep"
|
81
|
+
>>
|
82
|
+
```
|
83
|
+
|
84
|
+
A simple example with observation:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
class TaskObserver
|
88
|
+
def update(time, result, ex)
|
89
|
+
if result
|
90
|
+
print "(#{time}) Execution successfully returned #{result}\n"
|
91
|
+
elsif ex.is_a?(Concurrent::TimeoutError)
|
92
|
+
print "(#{time}) Execution timed out\n"
|
93
|
+
else
|
94
|
+
print "(#{time}) Execution failed with error #{ex}\n"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
task = Concurrent::TimerTask.run!(execution_interval: 1, timeout_interval: 1){ 42 }
|
100
|
+
task.runner.add_observer(TaskObserver.new)
|
101
|
+
|
102
|
+
#=> (2013-10-13 19:08:58 -0400) Execution successfully returned 42
|
103
|
+
#=> (2013-10-13 19:08:59 -0400) Execution successfully returned 42
|
104
|
+
#=> (2013-10-13 19:09:00 -0400) Execution successfully returned 42
|
105
|
+
task.runner.stop
|
106
|
+
|
107
|
+
task = Concurrent::TimerTask.run!(execution_interval: 1, timeout_interval: 1){ sleep }
|
108
|
+
task.runner.add_observer(TaskObserver.new)
|
109
|
+
|
110
|
+
#=> (2013-10-13 19:07:25 -0400) Execution timed out
|
111
|
+
#=> (2013-10-13 19:07:27 -0400) Execution timed out
|
112
|
+
#=> (2013-10-13 19:07:29 -0400) Execution timed out
|
113
|
+
task.runner.stop
|
114
|
+
|
115
|
+
task = Concurrent::TimerTask.run!(execution_interval: 1){ raise StandardError }
|
116
|
+
task.runner.add_observer(TaskObserver.new)
|
117
|
+
|
118
|
+
#=> (2013-10-13 19:09:37 -0400) Execution failed with error StandardError
|
119
|
+
#=> (2013-10-13 19:09:38 -0400) Execution failed with error StandardError
|
120
|
+
#=> (2013-10-13 19:09:39 -0400) Execution failed with error StandardError
|
121
|
+
task.runner.stop
|
122
|
+
```
|
123
|
+
|
124
|
+
## Copyright
|
125
|
+
|
126
|
+
*Concurrent Ruby* is Copyright © 2013 [Jerry D'Antonio](https://twitter.com/jerrydantonio).
|
127
|
+
It is free software and may be redistributed under the terms specified in the LICENSE file.
|
128
|
+
|
129
|
+
## License
|
130
|
+
|
131
|
+
Released under the MIT license.
|
132
|
+
|
133
|
+
http://www.opensource.org/licenses/mit-license.php
|
134
|
+
|
135
|
+
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
136
|
+
> of this software and associated documentation files (the "Software"), to deal
|
137
|
+
> in the Software without restriction, including without limitation the rights
|
138
|
+
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
139
|
+
> copies of the Software, and to permit persons to whom the Software is
|
140
|
+
> furnished to do so, subject to the following conditions:
|
141
|
+
>
|
142
|
+
> The above copyright notice and this permission notice shall be included in
|
143
|
+
> all copies or substantial portions of the Software.
|
144
|
+
>
|
145
|
+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
146
|
+
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
147
|
+
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
148
|
+
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
149
|
+
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
150
|
+
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
151
|
+
> THE SOFTWARE.
|
@@ -64,7 +64,7 @@ module Concurrent
|
|
64
64
|
it 'creates new workers when there are none available' do
|
65
65
|
pool = FixedThreadPool.new(5)
|
66
66
|
pool.length.should eq 0
|
67
|
-
5.times{
|
67
|
+
5.times{ pool << proc{ sleep } }
|
68
68
|
sleep(0.1)
|
69
69
|
pool.length.should eq 5
|
70
70
|
pool.kill
|
@@ -72,22 +72,11 @@ module Concurrent
|
|
72
72
|
|
73
73
|
it 'never creates more than :max_threads threads' do
|
74
74
|
pool = FixedThreadPool.new(5)
|
75
|
-
100.times{
|
75
|
+
100.times{ pool << proc{ sleep } }
|
76
76
|
sleep(0.1)
|
77
77
|
pool.length.should eq 5
|
78
78
|
pool.kill
|
79
79
|
end
|
80
|
-
|
81
|
-
it 'creates new threads when garbage collecting' do
|
82
|
-
pool = FixedThreadPool.new(5)
|
83
|
-
pool.length.should == 0
|
84
|
-
pool << proc { sleep }
|
85
|
-
sleep(0.1)
|
86
|
-
pool.length.should == 5
|
87
|
-
pool.instance_variable_set(:@max_threads, 25)
|
88
|
-
pool << proc { sleep }
|
89
|
-
pool.length.should == 25
|
90
|
-
end
|
91
80
|
end
|
92
81
|
|
93
82
|
context 'exception handling' do
|
@@ -141,33 +141,34 @@ module Concurrent
|
|
141
141
|
|
142
142
|
context 'instance #run!' do
|
143
143
|
|
144
|
-
|
145
|
-
|
144
|
+
after(:each) do
|
145
|
+
@thread.kill if @thread
|
146
146
|
end
|
147
147
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
148
|
+
it 'raises an exception when already running' do
|
149
|
+
@thread = Thread.new{ subject.run }
|
150
|
+
@thread.join(0.1)
|
151
|
+
expect {
|
152
|
+
@thread = subject.run!
|
153
|
+
}.to raise_exception(Concurrent::Runnable::LifecycleError)
|
154
|
+
sleep(0.1)
|
153
155
|
end
|
154
156
|
|
155
157
|
it 'creates a new thread' do
|
156
|
-
Thread.should_receive(:new).with(
|
157
|
-
@
|
158
|
-
sleep(0.1)
|
158
|
+
Thread.should_receive(:new).with(no_args())
|
159
|
+
@thread = subject.run!
|
159
160
|
end
|
160
161
|
|
161
|
-
it '
|
162
|
-
|
162
|
+
it 'calls the #run method on the new thread' do
|
163
|
+
subject.should_receive(:run)
|
164
|
+
@thread = subject.run!
|
163
165
|
sleep(0.1)
|
164
|
-
@context.thread.should_not eq Thread.current
|
165
166
|
end
|
166
167
|
|
167
|
-
it 'returns
|
168
|
-
@
|
168
|
+
it 'returns the new thread' do
|
169
|
+
@thread = subject.run!
|
169
170
|
sleep(0.1)
|
170
|
-
@
|
171
|
+
@thread.should be_a(Thread)
|
171
172
|
end
|
172
173
|
end
|
173
174
|
|
@@ -219,7 +220,7 @@ module Concurrent
|
|
219
220
|
it 'returns a context object on success' do
|
220
221
|
@context = runnable.run!
|
221
222
|
sleep(0.1)
|
222
|
-
@context.should be_a(
|
223
|
+
@context.should be_a(Runnable::Context)
|
223
224
|
end
|
224
225
|
|
225
226
|
it 'returns nil on failure' do
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative 'runnable_shared'
|
3
|
+
|
4
|
+
module Concurrent
|
5
|
+
|
6
|
+
describe TimerTask do
|
7
|
+
|
8
|
+
after(:each) do
|
9
|
+
@subject = @subject.runner if @subject.respond_to?(:runner)
|
10
|
+
@subject.kill unless @subject.nil?
|
11
|
+
@thread.kill unless @thread.nil?
|
12
|
+
sleep(0.1)
|
13
|
+
end
|
14
|
+
|
15
|
+
context ':runnable' do
|
16
|
+
|
17
|
+
subject { TimerTask.new{ nil } }
|
18
|
+
|
19
|
+
it_should_behave_like :runnable
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'created with #new' do
|
23
|
+
|
24
|
+
context '#initialize' do
|
25
|
+
|
26
|
+
it 'raises an exception if no block given' do
|
27
|
+
lambda {
|
28
|
+
@subject = Concurrent::TimerTask.new
|
29
|
+
}.should raise_error
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'uses the default execution interval when no interval is given' do
|
33
|
+
@subject = TimerTask.new{ nil }
|
34
|
+
@subject.execution_interval.should eq TimerTask::EXECUTION_INTERVAL
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'uses the default timeout interval when no interval is given' do
|
38
|
+
@subject = TimerTask.new{ nil }
|
39
|
+
@subject.timeout_interval.should eq TimerTask::TIMEOUT_INTERVAL
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'uses the given execution interval' do
|
43
|
+
@subject = TimerTask.new(execution_interval: 5){ nil }
|
44
|
+
@subject.execution_interval.should eq 5
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'uses the given timeout interval' do
|
48
|
+
@subject = TimerTask.new(timeout_interval: 5){ nil }
|
49
|
+
@subject.timeout_interval.should eq 5
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context '#kill' do
|
54
|
+
pending
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'created with TimerTask.run!' do
|
59
|
+
|
60
|
+
context 'arguments' do
|
61
|
+
|
62
|
+
it 'raises an exception if no block given' do
|
63
|
+
lambda {
|
64
|
+
@subject = Concurrent::TimerTask.run
|
65
|
+
}.should raise_error
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'passes the options to the new TimerTask' do
|
69
|
+
opts = {
|
70
|
+
execution_interval: 100,
|
71
|
+
timeout_interval: 100,
|
72
|
+
run_now: false,
|
73
|
+
logger: proc{ nil },
|
74
|
+
block_args: %w[one two three]
|
75
|
+
}
|
76
|
+
@subject = TimerTask.new(opts){ nil }
|
77
|
+
TimerTask.should_receive(:new).with(opts).and_return(@subject)
|
78
|
+
Concurrent::TimerTask.run!(opts)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'passes the block to the new TimerTask' do
|
82
|
+
@expected = false
|
83
|
+
block = proc{ @expected = true }
|
84
|
+
@subject = TimerTask.run!(run_now: true, &block)
|
85
|
+
sleep(0.1)
|
86
|
+
@expected.should be_true
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'creates a new thread' do
|
90
|
+
thread = Thread.new{ sleep(1) }
|
91
|
+
Thread.should_receive(:new).with(any_args()).and_return(thread)
|
92
|
+
@subject = TimerTask.run!{ nil }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'execution' do
|
98
|
+
|
99
|
+
it 'runs the block immediately when the :run_now option is true' do
|
100
|
+
@expected = false
|
101
|
+
@subject = TimerTask.run!(execution: 500, now: true){ @expected = true }
|
102
|
+
sleep(0.1)
|
103
|
+
@expected.should be_true
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'waits for :execution_interval seconds when the :run_now option is false' do
|
107
|
+
@expected = false
|
108
|
+
@subject = TimerTask.run!(execution: 0.5, now: false){ @expected = true }
|
109
|
+
@expected.should be_false
|
110
|
+
sleep(1)
|
111
|
+
@expected.should be_true
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'waits for :execution_interval seconds when the :run_now option is not given' do
|
115
|
+
@expected = false
|
116
|
+
@subject = TimerTask.run!(execution: 0.5){ @expected = true }
|
117
|
+
@expected.should be_false
|
118
|
+
sleep(1)
|
119
|
+
@expected.should be_true
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'yields to the execution block' do
|
123
|
+
@expected = false
|
124
|
+
@subject = TimerTask.run!(execution: 1){ @expected = true }
|
125
|
+
sleep(2)
|
126
|
+
@expected.should be_true
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'passes any given arguments to the execution block' do
|
130
|
+
args = [1,2,3,4]
|
131
|
+
@expected = nil
|
132
|
+
@subject = TimerTask.new(execution_interval: 0.5, args: args) do |*args|
|
133
|
+
@expected = args
|
134
|
+
end
|
135
|
+
@thread = Thread.new { @subject.run }
|
136
|
+
sleep(1)
|
137
|
+
@expected.should eq args
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'kills the worker thread if the timeout is reached' do
|
141
|
+
# the after(:each) block will trigger this expectation
|
142
|
+
Thread.should_receive(:kill).at_least(1).with(any_args())
|
143
|
+
@subject = TimerTask.new(execution_interval: 0.5, timeout_interval: 0.5){ Thread.stop }
|
144
|
+
@thread = Thread.new { @subject.run }
|
145
|
+
sleep(1.5)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
context 'observation' do
|
150
|
+
|
151
|
+
let(:observer) do
|
152
|
+
Class.new do
|
153
|
+
attr_reader :time
|
154
|
+
attr_reader :value
|
155
|
+
attr_reader :ex
|
156
|
+
define_method(:update) do |time, value, ex|
|
157
|
+
@time = time
|
158
|
+
@value = value
|
159
|
+
@ex = ex
|
160
|
+
end
|
161
|
+
end.new
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'notifies all observers on success' do
|
165
|
+
task = TimerTask.new(run_now: true){ sleep(0.1); 42 }
|
166
|
+
task.add_observer(observer)
|
167
|
+
Thread.new{ task.run }
|
168
|
+
sleep(1)
|
169
|
+
observer.value.should == 42
|
170
|
+
observer.ex.should be_nil
|
171
|
+
task.kill
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'notifies all observers on timeout' do
|
175
|
+
task = TimerTask.new(run_now: true, timeout: 1){ sleep }
|
176
|
+
task.add_observer(observer)
|
177
|
+
Thread.new{ task.run }
|
178
|
+
sleep(2)
|
179
|
+
observer.value.should be_nil
|
180
|
+
observer.ex.should be_a(Concurrent::TimeoutError)
|
181
|
+
task.kill
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'notifies all observers on error' do
|
185
|
+
task = TimerTask.new(run_now: true){ sleep(0.1); raise ArgumentError }
|
186
|
+
task.add_observer(observer)
|
187
|
+
Thread.new{ task.run }
|
188
|
+
sleep(1)
|
189
|
+
observer.value.should be_nil
|
190
|
+
observer.ex.should be_a(ArgumentError)
|
191
|
+
task.kill
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: concurrent-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.0.pre.
|
4
|
+
version: 0.3.0.pre.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jerry D'Antonio
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-10-
|
11
|
+
date: 2013-10-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -42,7 +42,6 @@ files:
|
|
42
42
|
- lib/concurrent/cached_thread_pool.rb
|
43
43
|
- lib/concurrent/event.rb
|
44
44
|
- lib/concurrent/event_machine_defer_proxy.rb
|
45
|
-
- lib/concurrent/executor.rb
|
46
45
|
- lib/concurrent/fixed_thread_pool/worker.rb
|
47
46
|
- lib/concurrent/fixed_thread_pool.rb
|
48
47
|
- lib/concurrent/future.rb
|
@@ -52,25 +51,25 @@ files:
|
|
52
51
|
- lib/concurrent/promise.rb
|
53
52
|
- lib/concurrent/runnable.rb
|
54
53
|
- lib/concurrent/supervisor.rb
|
54
|
+
- lib/concurrent/timer_task.rb
|
55
55
|
- lib/concurrent/utilities.rb
|
56
56
|
- lib/concurrent/version.rb
|
57
57
|
- lib/concurrent.rb
|
58
58
|
- lib/concurrent_ruby.rb
|
59
59
|
- md/agent.md
|
60
60
|
- md/event.md
|
61
|
-
- md/executor.md
|
62
61
|
- md/future.md
|
63
62
|
- md/goroutine.md
|
64
63
|
- md/obligation.md
|
65
64
|
- md/promise.md
|
66
65
|
- md/supervisor.md
|
67
66
|
- md/thread_pool.md
|
67
|
+
- md/timer_task.md
|
68
68
|
- spec/concurrent/actor_spec.rb
|
69
69
|
- spec/concurrent/agent_spec.rb
|
70
70
|
- spec/concurrent/cached_thread_pool_spec.rb
|
71
71
|
- spec/concurrent/event_machine_defer_proxy_spec.rb
|
72
72
|
- spec/concurrent/event_spec.rb
|
73
|
-
- spec/concurrent/executor_spec.rb
|
74
73
|
- spec/concurrent/fixed_thread_pool_spec.rb
|
75
74
|
- spec/concurrent/future_spec.rb
|
76
75
|
- spec/concurrent/global_thread_pool_spec.rb
|
@@ -81,6 +80,7 @@ files:
|
|
81
80
|
- spec/concurrent/runnable_spec.rb
|
82
81
|
- spec/concurrent/supervisor_spec.rb
|
83
82
|
- spec/concurrent/thread_pool_shared.rb
|
83
|
+
- spec/concurrent/timer_task_spec.rb
|
84
84
|
- spec/concurrent/uses_global_thread_pool_shared.rb
|
85
85
|
- spec/concurrent/utilities_spec.rb
|
86
86
|
- spec/spec_helper.rb
|
@@ -119,7 +119,6 @@ test_files:
|
|
119
119
|
- spec/concurrent/cached_thread_pool_spec.rb
|
120
120
|
- spec/concurrent/event_machine_defer_proxy_spec.rb
|
121
121
|
- spec/concurrent/event_spec.rb
|
122
|
-
- spec/concurrent/executor_spec.rb
|
123
122
|
- spec/concurrent/fixed_thread_pool_spec.rb
|
124
123
|
- spec/concurrent/future_spec.rb
|
125
124
|
- spec/concurrent/global_thread_pool_spec.rb
|
@@ -130,6 +129,7 @@ test_files:
|
|
130
129
|
- spec/concurrent/runnable_spec.rb
|
131
130
|
- spec/concurrent/supervisor_spec.rb
|
132
131
|
- spec/concurrent/thread_pool_shared.rb
|
132
|
+
- spec/concurrent/timer_task_spec.rb
|
133
133
|
- spec/concurrent/uses_global_thread_pool_shared.rb
|
134
134
|
- spec/concurrent/utilities_spec.rb
|
135
135
|
- spec/spec_helper.rb
|
data/md/executor.md
DELETED
@@ -1,193 +0,0 @@
|
|
1
|
-
# Being of Sound Mind
|
2
|
-
|
3
|
-
A very common currency pattern is to run a thread that performs a task at regular
|
4
|
-
intervals. The thread that peforms the task sleeps for the given interval then
|
5
|
-
waked up and performs the task. Later, rinse, repeat... This pattern causes two
|
6
|
-
problems. First, it is difficult to test the business logic of the task becuse the
|
7
|
-
task itself is tightly couple with the threading. Second, an exception in the task
|
8
|
-
can cause the entire thread to abend. In a long-running application where the task
|
9
|
-
thread is intended to run for days/weeks/years a crashed task thread can pose a real
|
10
|
-
problem. The `Executor` class alleviates both problems.
|
11
|
-
|
12
|
-
When an executor is launched it starts a thread for monitoring the execution interval.
|
13
|
-
The executor thread does not perform the task, however. Instead, the executor
|
14
|
-
launches the task on a separat thread. The advantage of this approach is that if
|
15
|
-
the task crashes it will only kill the task thread, not the executor thread. The
|
16
|
-
executor thread can then log the success or failure of the task. The executor
|
17
|
-
can even be configured with a timeout value allowing it to kill a task that runs
|
18
|
-
to long and then log the error.
|
19
|
-
|
20
|
-
One other advantage of the `Executor` class is that it forces the bsiness logic to
|
21
|
-
be completely decoupled from the threading logic. The business logic can be tested
|
22
|
-
separately then passed to the an executor for scheduling and running.
|
23
|
-
|
24
|
-
The `Executor` is the yin to to the
|
25
|
-
[Supervisor's](https://github.com/jdantonio/concurrent-ruby/blob/master/md/supervisor.md)
|
26
|
-
yang. Where the `Supervisor` is intended to manage long-running threads that operate
|
27
|
-
continuously, the `Executor` is intended to manage fairly short operations that
|
28
|
-
occur repeatedly at regular intervals.
|
29
|
-
|
30
|
-
Unlike some of the others concurrency objects in the library, executors do not
|
31
|
-
run on the global thread pool. In my experience the types of tasks that will benefit
|
32
|
-
from the `Executor` class tend to also be long running. For this reason they get
|
33
|
-
their own thread every time the task is executed.
|
34
|
-
|
35
|
-
## ExecutionContext
|
36
|
-
|
37
|
-
When an executor is run the return value is an `ExecutionContext` object. An
|
38
|
-
`ExecutionContext` object has several attribute readers (`#name`, `#execution_interval`,
|
39
|
-
and `#timeout_interval`). It also provides several `Thread` operations which can
|
40
|
-
be performed against the internal thread. These include `#status`, `#join`, and
|
41
|
-
`kill`.
|
42
|
-
|
43
|
-
## Custom Logging
|
44
|
-
|
45
|
-
An executor will write a log message to standard out at the completion of every
|
46
|
-
task run. When the task is successful the log message is tagged at the `:info`
|
47
|
-
level. When the task times out the log message is tagged at the `warn` level.
|
48
|
-
When the task fails tocomplete (most likely because of exception) the log
|
49
|
-
message is tagged at the `error` level.
|
50
|
-
|
51
|
-
The default logging behavior can be overridden by passing a `proc` to the executor
|
52
|
-
on creation. The block will be passes three (3) arguments every time it is run:
|
53
|
-
executor `name`, log `level`, and the log `msg` (message). The `proc` can do
|
54
|
-
whatever it wanst with these arguments.
|
55
|
-
|
56
|
-
## Examples
|
57
|
-
|
58
|
-
A basic example:
|
59
|
-
|
60
|
-
```ruby
|
61
|
-
require 'concurrent'
|
62
|
-
|
63
|
-
ec = Concurrent::Executor.run('Foo'){ puts 'Boom!' }
|
64
|
-
|
65
|
-
ec.name #=> "Foo"
|
66
|
-
ec.execution_interval #=> 60 == Concurrent::Executor::EXECUTION_INTERVAL
|
67
|
-
ec.timeout_interval #=> 30 == Concurrent::Executor::TIMEOUT_INTERVAL
|
68
|
-
ec.status #=> "sleep"
|
69
|
-
|
70
|
-
# wait 60 seconds...
|
71
|
-
#=> 'Boom!'
|
72
|
-
#=> ' INFO (2013-08-02 23:20:15) Foo: execution completed successfully'
|
73
|
-
|
74
|
-
ec.kill #=> true
|
75
|
-
```
|
76
|
-
|
77
|
-
Both the execution_interval and the timeout_interval can be configured:
|
78
|
-
|
79
|
-
```ruby
|
80
|
-
ec = Concurrent::Executor.run('Foo', execution_interval: 5, timeout_interval: 5) do
|
81
|
-
puts 'Boom!'
|
82
|
-
end
|
83
|
-
|
84
|
-
ec.execution_interval #=> 5
|
85
|
-
ec.timeout_interval #=> 5
|
86
|
-
```
|
87
|
-
|
88
|
-
By default an `Executor` will wait for `:execution_interval` seconds before running the block.
|
89
|
-
To run the block immediately set the `:run_now` option to `true`:
|
90
|
-
|
91
|
-
```ruby
|
92
|
-
ec = Concurrent::Executor.run('Foo', run_now: true){ puts 'Boom!' }
|
93
|
-
#=> 'Boom!''
|
94
|
-
#=> ' INFO (2013-08-15 21:35:14) Foo: execution completed successfully'
|
95
|
-
ec.status #=> "sleep"
|
96
|
-
>>
|
97
|
-
```
|
98
|
-
|
99
|
-
A simple example with timeout and task exception:
|
100
|
-
|
101
|
-
```ruby
|
102
|
-
ec = Concurrent::Executor.run('Foo', execution_interval: 1, timeout_interval: 1){ sleep(10) }
|
103
|
-
|
104
|
-
#=> WARN (2013-08-02 23:45:26) Foo: execution timed out after 1 seconds
|
105
|
-
#=> WARN (2013-08-02 23:45:28) Foo: execution timed out after 1 seconds
|
106
|
-
#=> WARN (2013-08-02 23:45:30) Foo: execution timed out after 1 seconds
|
107
|
-
|
108
|
-
ec = Concurrent::Executor.run('Foo', execution_interval: 1){ raise StandardError }
|
109
|
-
|
110
|
-
#=> ERROR (2013-08-02 23:47:31) Foo: execution failed with error 'StandardError'
|
111
|
-
#=> ERROR (2013-08-02 23:47:32) Foo: execution failed with error 'StandardError'
|
112
|
-
#=> ERROR (2013-08-02 23:47:33) Foo: execution failed with error 'StandardError'
|
113
|
-
```
|
114
|
-
|
115
|
-
For custom logging, simply provide a `proc` when creating an executor:
|
116
|
-
|
117
|
-
```ruby
|
118
|
-
file_logger = proc do |name, level, msg|
|
119
|
-
open('executor.log', 'a') do |f|
|
120
|
-
f << ("%5s (%s) %s: %s\n" % [level.upcase, Time.now.strftime("%F %T"), name, msg])
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
ec = Concurrent::Executor.run('Foo', execution_interval: 5, logger: file_logger) do
|
125
|
-
puts 'Boom!'
|
126
|
-
end
|
127
|
-
|
128
|
-
# the log file contains
|
129
|
-
# INFO (2013-08-02 23:30:19) Foo: execution completed successfully
|
130
|
-
# INFO (2013-08-02 23:30:24) Foo: execution completed successfully
|
131
|
-
# INFO (2013-08-02 23:30:29) Foo: execution completed successfully
|
132
|
-
# INFO (2013-08-02 23:30:34) Foo: execution completed successfully
|
133
|
-
# INFO (2013-08-02 23:30:39) Foo: execution completed successfully
|
134
|
-
# INFO (2013-08-02 23:30:44) Foo: execution completed successfully
|
135
|
-
```
|
136
|
-
|
137
|
-
It is also possible to access the default stdout logger from within a logger `proc`:
|
138
|
-
|
139
|
-
```ruby
|
140
|
-
file_logger = proc do |name, level, msg|
|
141
|
-
Concurrent::Executor::STDOUT_LOGGER.call(name, level, msg)
|
142
|
-
open('executor.log', 'a') do |f|
|
143
|
-
f << ("%5s (%s) %s: %s\n" % [level.upcase, Time.now.strftime("%F %T"), name, msg])
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
ec = Concurrent::Executor.run('Foo', execution_interval: 5, logger: file_logger) do
|
148
|
-
puts 'Boom!'
|
149
|
-
end
|
150
|
-
|
151
|
-
# wait...
|
152
|
-
|
153
|
-
#=> Boom!
|
154
|
-
#=> INFO (2013-08-02 23:40:49) Foo: execution completed successfully
|
155
|
-
#=> Boom!
|
156
|
-
#=> INFO (2013-08-02 23:40:54) Foo: execution completed successfully
|
157
|
-
#=> Boom!
|
158
|
-
#=> INFO (2013-08-02 23:40:59) Foo: execution completed successfully
|
159
|
-
|
160
|
-
# and the log file contains
|
161
|
-
# INFO (2013-08-02 23:39:52) Foo: execution completed successfully
|
162
|
-
# INFO (2013-08-02 23:39:57) Foo: execution completed successfully
|
163
|
-
# INFO (2013-08-02 23:40:49) Foo: execution completed successfully
|
164
|
-
```
|
165
|
-
|
166
|
-
## Copyright
|
167
|
-
|
168
|
-
*Concurrent Ruby* is Copyright © 2013 [Jerry D'Antonio](https://twitter.com/jerrydantonio).
|
169
|
-
It is free software and may be redistributed under the terms specified in the LICENSE file.
|
170
|
-
|
171
|
-
## License
|
172
|
-
|
173
|
-
Released under the MIT license.
|
174
|
-
|
175
|
-
http://www.opensource.org/licenses/mit-license.php
|
176
|
-
|
177
|
-
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
178
|
-
> of this software and associated documentation files (the "Software"), to deal
|
179
|
-
> in the Software without restriction, including without limitation the rights
|
180
|
-
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
181
|
-
> copies of the Software, and to permit persons to whom the Software is
|
182
|
-
> furnished to do so, subject to the following conditions:
|
183
|
-
>
|
184
|
-
> The above copyright notice and this permission notice shall be included in
|
185
|
-
> all copies or substantial portions of the Software.
|
186
|
-
>
|
187
|
-
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
188
|
-
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
189
|
-
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
190
|
-
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
191
|
-
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
192
|
-
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
193
|
-
> THE SOFTWARE.
|
@@ -1,240 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
require_relative 'runnable_shared'
|
3
|
-
|
4
|
-
module Concurrent
|
5
|
-
|
6
|
-
describe Executor do
|
7
|
-
|
8
|
-
before(:each) do
|
9
|
-
@orig_stdout = $stdout
|
10
|
-
$stdout = StringIO.new
|
11
|
-
end
|
12
|
-
|
13
|
-
after(:each) do
|
14
|
-
$stdout = @orig_stdout
|
15
|
-
end
|
16
|
-
|
17
|
-
after(:each) do
|
18
|
-
@subject = @subject.runner if @subject.respond_to?(:runner)
|
19
|
-
@subject.kill unless @subject.nil?
|
20
|
-
@thread.kill unless @thread.nil?
|
21
|
-
sleep(0.1)
|
22
|
-
end
|
23
|
-
|
24
|
-
context ':runnable' do
|
25
|
-
|
26
|
-
subject { Executor.new(':runnable'){ nil } }
|
27
|
-
|
28
|
-
it_should_behave_like :runnable
|
29
|
-
end
|
30
|
-
|
31
|
-
context 'created with #new' do
|
32
|
-
|
33
|
-
context '#initialize' do
|
34
|
-
|
35
|
-
it 'raises an exception if no block given' do
|
36
|
-
lambda {
|
37
|
-
@subject = Concurrent::Executor.new('Foo')
|
38
|
-
}.should raise_error
|
39
|
-
end
|
40
|
-
|
41
|
-
it 'uses the default execution interval when no interval is given' do
|
42
|
-
@subject = Executor.new('Foo'){ nil }
|
43
|
-
@subject.execution_interval.should eq Executor::EXECUTION_INTERVAL
|
44
|
-
end
|
45
|
-
|
46
|
-
it 'uses the default timeout interval when no interval is given' do
|
47
|
-
@subject = Executor.new('Foo'){ nil }
|
48
|
-
@subject.timeout_interval.should eq Executor::TIMEOUT_INTERVAL
|
49
|
-
end
|
50
|
-
|
51
|
-
it 'uses the given execution interval' do
|
52
|
-
@subject = Executor.new('Foo', execution_interval: 5){ nil }
|
53
|
-
@subject.execution_interval.should eq 5
|
54
|
-
end
|
55
|
-
|
56
|
-
it 'uses the given timeout interval' do
|
57
|
-
@subject = Executor.new('Foo', timeout_interval: 5){ nil }
|
58
|
-
@subject.timeout_interval.should eq 5
|
59
|
-
end
|
60
|
-
|
61
|
-
it 'sets the #name context variable' do
|
62
|
-
@subject = Executor.new('Foo'){ nil }
|
63
|
-
@subject.name.should eq 'Foo'
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
context '#kill' do
|
68
|
-
pending
|
69
|
-
end
|
70
|
-
|
71
|
-
context '#status' do
|
72
|
-
|
73
|
-
subject { Executor.new('Foo'){ nil } }
|
74
|
-
|
75
|
-
it 'returns the status of the executor thread when running' do
|
76
|
-
@thread = Thread.new { subject.run }
|
77
|
-
sleep(0.1)
|
78
|
-
subject.status.should eq 'sleep'
|
79
|
-
end
|
80
|
-
|
81
|
-
it 'returns nil when not running' do
|
82
|
-
subject.status.should be_nil
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
context 'created with Executor.run!' do
|
88
|
-
|
89
|
-
context 'arguments' do
|
90
|
-
|
91
|
-
it 'raises an exception if no block given' do
|
92
|
-
lambda {
|
93
|
-
@subject = Concurrent::Executor.run('Foo')
|
94
|
-
}.should raise_error
|
95
|
-
end
|
96
|
-
|
97
|
-
it 'passes the name to the new Executor' do
|
98
|
-
@subject = Executor.new('Foo'){ nil }
|
99
|
-
Executor.should_receive(:new).with('Foo').and_return(@subject)
|
100
|
-
Concurrent::Executor.run!('Foo')
|
101
|
-
end
|
102
|
-
|
103
|
-
it 'passes the options to the new Executor' do
|
104
|
-
opts = {
|
105
|
-
execution_interval: 100,
|
106
|
-
timeout_interval: 100,
|
107
|
-
run_now: false,
|
108
|
-
logger: proc{ nil },
|
109
|
-
block_args: %w[one two three]
|
110
|
-
}
|
111
|
-
@subject = Executor.new('Foo', opts){ nil }
|
112
|
-
Executor.should_receive(:new).with(anything(), opts).and_return(@subject)
|
113
|
-
Concurrent::Executor.run!('Foo', opts)
|
114
|
-
end
|
115
|
-
|
116
|
-
it 'passes the block to the new Executor' do
|
117
|
-
@expected = false
|
118
|
-
block = proc{ @expected = true }
|
119
|
-
@subject = Executor.run!('Foo', run_now: true, &block)
|
120
|
-
sleep(0.1)
|
121
|
-
@expected.should be_true
|
122
|
-
end
|
123
|
-
|
124
|
-
it 'creates a new thread' do
|
125
|
-
thread = Thread.new{ sleep(1) }
|
126
|
-
Thread.should_receive(:new).with(any_args()).and_return(thread)
|
127
|
-
@subject = Executor.run!('Foo'){ nil }
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
context 'execution' do
|
132
|
-
|
133
|
-
it 'runs the block immediately when the :run_now option is true' do
|
134
|
-
@expected = false
|
135
|
-
@subject = Executor.run!('Foo', execution: 500, now: true){ @expected = true }
|
136
|
-
sleep(0.1)
|
137
|
-
@expected.should be_true
|
138
|
-
end
|
139
|
-
|
140
|
-
it 'waits for :execution_interval seconds when the :run_now option is false' do
|
141
|
-
@expected = false
|
142
|
-
@subject = Executor.run!('Foo', execution: 0.5, now: false){ @expected = true }
|
143
|
-
@expected.should be_false
|
144
|
-
sleep(1)
|
145
|
-
@expected.should be_true
|
146
|
-
end
|
147
|
-
|
148
|
-
it 'waits for :execution_interval seconds when the :run_now option is not given' do
|
149
|
-
@expected = false
|
150
|
-
@subject = Executor.run!('Foo', execution: 0.5){ @expected = true }
|
151
|
-
@expected.should be_false
|
152
|
-
sleep(1)
|
153
|
-
@expected.should be_true
|
154
|
-
end
|
155
|
-
|
156
|
-
it 'yields to the execution block' do
|
157
|
-
@expected = false
|
158
|
-
@subject = Executor.run!('Foo', execution: 1){ @expected = true }
|
159
|
-
sleep(2)
|
160
|
-
@expected.should be_true
|
161
|
-
end
|
162
|
-
|
163
|
-
it 'passes any given arguments to the execution block' do
|
164
|
-
args = [1,2,3,4]
|
165
|
-
@expected = nil
|
166
|
-
@subject = Executor.new('Foo', execution_interval: 0.5, args: args) do |*args|
|
167
|
-
@expected = args
|
168
|
-
end
|
169
|
-
@thread = Thread.new { @subject.run }
|
170
|
-
sleep(1)
|
171
|
-
@expected.should eq args
|
172
|
-
end
|
173
|
-
|
174
|
-
it 'kills the worker thread if the timeout is reached' do
|
175
|
-
# the after(:each) block will trigger this expectation
|
176
|
-
Thread.should_receive(:kill).at_least(1).with(any_args())
|
177
|
-
@subject = Executor.new('Foo', execution_interval: 0.5, timeout_interval: 0.5){ Thread.stop }
|
178
|
-
@thread = Thread.new { @subject.run }
|
179
|
-
sleep(1.5)
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
|
-
context '#status' do
|
184
|
-
|
185
|
-
it 'returns the status of the executor thread when running' do
|
186
|
-
@subject = Executor.run!('Foo'){ nil }
|
187
|
-
sleep(0.1)
|
188
|
-
@subject.runner.status.should eq 'sleep'
|
189
|
-
end
|
190
|
-
|
191
|
-
it 'returns nil when not running' do
|
192
|
-
@subject = Executor.new('Foo'){ nil }
|
193
|
-
sleep(0.1)
|
194
|
-
@subject.kill
|
195
|
-
sleep(0.1)
|
196
|
-
@subject.status.should be_nil
|
197
|
-
end
|
198
|
-
end
|
199
|
-
end
|
200
|
-
|
201
|
-
context 'logging' do
|
202
|
-
|
203
|
-
before(:each) do
|
204
|
-
@name = nil
|
205
|
-
@level = nil
|
206
|
-
@msg = nil
|
207
|
-
|
208
|
-
@logger = proc do |name, level, msg|
|
209
|
-
@name = name
|
210
|
-
@level = level
|
211
|
-
@msg = msg
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
it 'uses a custom logger when given' do
|
216
|
-
@subject = Executor.run!('Foo', execution_interval: 0.1, logger: @logger){ nil }
|
217
|
-
sleep(0.5)
|
218
|
-
@name.should eq 'Foo'
|
219
|
-
end
|
220
|
-
|
221
|
-
it 'logs :info when execution is successful' do
|
222
|
-
@subject = Executor.run!('Foo', execution_interval: 0.1, logger: @logger){ nil }
|
223
|
-
sleep(0.5)
|
224
|
-
@level.should eq :info
|
225
|
-
end
|
226
|
-
|
227
|
-
it 'logs :warn when execution times out' do
|
228
|
-
@subject = Executor.run!('Foo', execution_interval: 0.1, timeout_interval: 0.1, logger: @logger){ Thread.stop }
|
229
|
-
sleep(0.5)
|
230
|
-
@level.should eq :warn
|
231
|
-
end
|
232
|
-
|
233
|
-
it 'logs :error when execution is fails' do
|
234
|
-
@subject = Executor.run!('Foo', execution_interval: 0.1, logger: @logger){ raise StandardError }
|
235
|
-
sleep(0.5)
|
236
|
-
@level.should eq :error
|
237
|
-
end
|
238
|
-
end
|
239
|
-
end
|
240
|
-
end
|