concurrent-ruby 0.3.0.pre.2 → 0.3.0.pre.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|