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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2e3a37abe10cd8c068be7f5f34ec87ea665c4d8c
4
- data.tar.gz: bbb6cf6902175de9995656c9ded8e611940edfb7
3
+ metadata.gz: 0823ac7a2cf530803f04b071254f156c32a840a7
4
+ data.tar.gz: 74a9edb10d10210746b53cb87dec1bdff8c1a971
5
5
  SHA512:
6
- metadata.gz: 69f7670740cfb70d858462864993d38209050a435435cbd4a9202dda5146f3ab32629bc6e2206b016c2ec0d9a95b5ff1c7751a2ee8278ba6e6400fafb478d23f
7
- data.tar.gz: 84ce8fdad2b1ccc72f93e6a5b08889937b9c27d2d4aea0560d191ac4e9338e5909fedbd1a9a128c228a36eb7f6fbcc6f3206130308658122d23d98e89a2a4239
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(v=vs.85).aspx) from back in my Visual C++ days
63
- * Scheduled task execution with the [Executor](https://github.com/jdantonio/concurrent-ruby/blob/master/md/executor.md) service
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
- #### Executor
202
+ #### TimerTask
203
203
 
204
204
  ```ruby
205
205
  require 'concurrent'
206
206
 
207
- ec = Concurrent::Executor.run('Foo'){ puts 'Boom!' }
207
+ ec = Concurrent::TimerTask.run{ puts 'Boom!' }
208
208
 
209
- ec.name #=> "Foo"
210
- ec.execution_interval #=> 60 == Concurrent::Executor::EXECUTION_INTERVAL
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
@@ -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'
@@ -2,7 +2,9 @@ require 'thread'
2
2
 
3
3
  module Concurrent
4
4
 
5
- module Running
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 self.included(base)
54
- base.send(:include, Running)
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 = delta(@restart_times.first, @restart_times.last)
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 Executor
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(name, opts = {}, &block)
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
- def status
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
- @logger.call(@name, :error, "execution failed with error '#{ex}'")
76
+ changed
77
+ notify_observers(Time.now, nil, ex)
84
78
  ensure
85
79
  unless @worker.nil?
86
80
  Thread.kill(@worker)
@@ -1,3 +1,3 @@
1
1
  module Concurrent
2
- VERSION = '0.3.0.pre.2'
2
+ VERSION = '0.3.0.pre.3'
3
3
  end
@@ -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 &copy; 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{ sleep(0.1); pool << proc{ sleep } }
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{ sleep(0.01); pool << proc{ sleep } }
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
- let(:clazz) do
145
- Class.new { include Runnable }
144
+ after(:each) do
145
+ @thread.kill if @thread
146
146
  end
147
147
 
148
- subject { clazz.new }
149
-
150
- after(:each) do
151
- @context.runner.stop if @context && @context.runner
152
- @context.thread.kill if @context && @context.thread
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(any_args()).and_return(nil)
157
- @context = subject.run!
158
- sleep(0.1)
158
+ Thread.should_receive(:new).with(no_args())
159
+ @thread = subject.run!
159
160
  end
160
161
 
161
- it 'runs the runner on the new thread' do
162
- @context = subject.run!
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 a context object on success' do
168
- @context = subject.run!
168
+ it 'returns the new thread' do
169
+ @thread = subject.run!
169
170
  sleep(0.1)
170
- @context.should be_a(Running::Context)
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(Running::Context)
223
+ @context.should be_a(Runnable::Context)
223
224
  end
224
225
 
225
226
  it 'returns nil on failure' do
@@ -34,6 +34,7 @@ share_examples_for :thread_pool do
34
34
 
35
35
  it 'stops accepting new tasks' do
36
36
  subject.post{ sleep(1) }
37
+ sleep(0.1)
37
38
  subject.shutdown
38
39
  @expected = false
39
40
  subject.post{ @expected = true }.should be_false
@@ -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.2
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-13 00:00:00.000000000 Z
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
@@ -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 &copy; 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