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 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