concurrent-ruby 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +48 -1
- data/lib/concurrent.rb +8 -1
- data/lib/concurrent/agent.rb +19 -40
- data/lib/concurrent/cached_thread_pool.rb +10 -11
- data/lib/concurrent/defer.rb +8 -12
- data/lib/concurrent/executor.rb +95 -0
- data/lib/concurrent/fixed_thread_pool.rb +12 -6
- data/lib/concurrent/functions.rb +120 -0
- data/lib/concurrent/future.rb +8 -20
- data/lib/concurrent/global_thread_pool.rb +13 -0
- data/lib/concurrent/goroutine.rb +5 -1
- data/lib/concurrent/null_thread_pool.rb +22 -0
- data/lib/concurrent/obligation.rb +10 -64
- data/lib/concurrent/promise.rb +38 -60
- data/lib/concurrent/reactor.rb +166 -0
- data/lib/concurrent/reactor/drb_async_demux.rb +83 -0
- data/lib/concurrent/reactor/tcp_sync_demux.rb +131 -0
- data/lib/concurrent/supervisor.rb +100 -0
- data/lib/concurrent/thread_pool.rb +16 -5
- data/lib/concurrent/utilities.rb +8 -0
- data/lib/concurrent/version.rb +1 -1
- data/md/defer.md +4 -4
- data/md/executor.md +187 -0
- data/md/promise.md +2 -0
- data/md/thread_pool.md +27 -0
- data/spec/concurrent/agent_spec.rb +8 -27
- data/spec/concurrent/cached_thread_pool_spec.rb +14 -1
- data/spec/concurrent/defer_spec.rb +17 -21
- data/spec/concurrent/event_machine_defer_proxy_spec.rb +159 -149
- data/spec/concurrent/executor_spec.rb +200 -0
- data/spec/concurrent/fixed_thread_pool_spec.rb +2 -3
- data/spec/concurrent/functions_spec.rb +217 -0
- data/spec/concurrent/future_spec.rb +4 -11
- data/spec/concurrent/global_thread_pool_spec.rb +38 -0
- data/spec/concurrent/goroutine_spec.rb +15 -0
- data/spec/concurrent/null_thread_pool_spec.rb +54 -0
- data/spec/concurrent/obligation_shared.rb +127 -116
- data/spec/concurrent/promise_spec.rb +16 -14
- data/spec/concurrent/reactor/drb_async_demux_spec.rb +196 -0
- data/spec/concurrent/reactor/tcp_sync_demux_spec.rb +410 -0
- data/spec/concurrent/reactor_spec.rb +364 -0
- data/spec/concurrent/supervisor_spec.rb +258 -0
- data/spec/concurrent/thread_pool_shared.rb +156 -161
- data/spec/concurrent/utilities_spec.rb +30 -1
- data/spec/spec_helper.rb +13 -0
- metadata +38 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 336769e53b0dcf7aa0aa3952ca396deaabfd2886
|
4
|
+
data.tar.gz: a8f2de814918d3ab9849636741674d9731246c45
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f6696e6d00cc9126d247673a1f4cd90ec1ef822cd55c2fd30d45bf7b48f766fd6f0a712c884e68c9fe7e29dff005e501ed1299b9903502215f09db1a77dfd83
|
7
|
+
data.tar.gz: 5ad2e8367f7bca913db38ecfca31c77288e0029b624587c7196d0658e6d32fe8bb3709b33c7791c95e5c5b5dd494261e0b4130030718e8dbaf2fea9dc65fc5a8
|
data/README.md
CHANGED
@@ -55,6 +55,7 @@ Several features from Erlang, Go, Clojure, Java, and JavaScript have been implem
|
|
55
55
|
* Go inspired [Goroutine](https://github.com/jdantonio/concurrent-ruby/blob/master/md/goroutine.md)
|
56
56
|
* JavaScript inspired [Promise](https://github.com/jdantonio/concurrent-ruby/blob/master/md/promise.md)
|
57
57
|
* Java inspired [Thread Pools](https://github.com/jdantonio/concurrent-ruby/blob/master/md/thread_pool.md)
|
58
|
+
* Scheduled task execution with the [Executor](https://github.com/jdantonio/concurrent-ruby/blob/master/md/executor.md) service
|
58
59
|
|
59
60
|
### Is it any good?
|
60
61
|
|
@@ -62,7 +63,11 @@ Several features from Erlang, Go, Clojure, Java, and JavaScript have been implem
|
|
62
63
|
|
63
64
|
### Supported Ruby versions
|
64
65
|
|
65
|
-
|
66
|
+
MRI 1.9.2, 1.9.3, and 2.0. This library is pure Ruby and has minimal gem dependencies. It should be
|
67
|
+
fully compatible with any Ruby interpreter that is 1.9.x compliant. I simply don't know enough
|
68
|
+
about JRuby, Rubinius, or the others to fully support them. I can promise good karma and
|
69
|
+
attribution on this page to anyone wishing to take responsibility for verifying compaitibility
|
70
|
+
with any Ruby other than MRI.
|
66
71
|
|
67
72
|
### Install
|
68
73
|
|
@@ -84,6 +89,25 @@ Once you've installed the gem you must `require` it in your project:
|
|
84
89
|
require 'concurrent'
|
85
90
|
```
|
86
91
|
|
92
|
+
### Kernel Methods
|
93
|
+
|
94
|
+
Many Ruby developers consider it bad form to add function to the global (Kernel) namespace.
|
95
|
+
I don't necessarily agree. If the function acts like a low-level feature of the language
|
96
|
+
I think it is OK to add the method to the `Kernel` module. To support my personal programming
|
97
|
+
style I have chosen to implement `Kernel` methods to instance many of the objects in this
|
98
|
+
library. Out of respect for the larger Ruby community I have made these methods optional.
|
99
|
+
They are not imported with the normal `require 'concurrent'` directive. To import these
|
100
|
+
functions you must import the `concurrent/functions` library.
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
require 'concurrent'
|
104
|
+
score = agent(10) #=> NoMethodError: undefined method `agent' for main:Object
|
105
|
+
|
106
|
+
require 'concurrent/functions'
|
107
|
+
score = agent(10) #=> #<Concurrent::Agent:0x35b2b28 ...
|
108
|
+
score.value #=> 10
|
109
|
+
```
|
110
|
+
|
87
111
|
### Examples
|
88
112
|
|
89
113
|
For complete examples, see the specific documentation linked above. Below are a few examples to whet your appetite.
|
@@ -103,6 +127,7 @@ sleep(0.1)
|
|
103
127
|
|
104
128
|
```ruby
|
105
129
|
require 'concurrent'
|
130
|
+
require 'concurrent/functions'
|
106
131
|
|
107
132
|
score = agent(10)
|
108
133
|
score.value #=> 10
|
@@ -124,6 +149,7 @@ score.value #=> 170
|
|
124
149
|
|
125
150
|
```ruby
|
126
151
|
require 'concurrent'
|
152
|
+
require 'concurrent/functions'
|
127
153
|
|
128
154
|
Concurrent::Defer.new{ "Jerry D'Antonio" }.
|
129
155
|
then{|result| puts "Hello, #{result}!" }.
|
@@ -145,6 +171,7 @@ sleep(0.1)
|
|
145
171
|
|
146
172
|
```ruby
|
147
173
|
require 'concurrent'
|
174
|
+
require 'concurrent/functions'
|
148
175
|
|
149
176
|
count = future{ sleep(1); 10 }
|
150
177
|
count.state #=> :pending
|
@@ -157,6 +184,7 @@ deref count #=> 10
|
|
157
184
|
|
158
185
|
```ruby
|
159
186
|
require 'concurrent'
|
187
|
+
require 'concurrent/functions'
|
160
188
|
|
161
189
|
p = promise("Jerry", "D'Antonio"){|a, b| "#{a} #{b}" }.
|
162
190
|
then{|result| "Hello #{result}." }.
|
@@ -190,6 +218,25 @@ sleep(1)
|
|
190
218
|
@expected #=> 30
|
191
219
|
```
|
192
220
|
|
221
|
+
#### Executor
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
require 'concurrent'
|
225
|
+
|
226
|
+
ec = Concurrent::Executor.run('Foo'){ puts 'Boom!' }
|
227
|
+
|
228
|
+
ec.name #=> "Foo"
|
229
|
+
ec.execution_interval #=> 60 == Concurrent::Executor::EXECUTION_INTERVAL
|
230
|
+
ec.timeout_interval #=> 30 == Concurrent::Executor::TIMEOUT_INTERVAL
|
231
|
+
ec.status #=> "sleep"
|
232
|
+
|
233
|
+
# wait 60 seconds...
|
234
|
+
#=> 'Boom!'
|
235
|
+
#=> ' INFO (2013-08-02 23:20:15) Foo: execution completed successfully'
|
236
|
+
|
237
|
+
ec.kill #=> true
|
238
|
+
```
|
239
|
+
|
193
240
|
## Contributing
|
194
241
|
|
195
242
|
1. Fork it
|
data/lib/concurrent.rb
CHANGED
@@ -6,15 +6,22 @@ require 'concurrent/event'
|
|
6
6
|
|
7
7
|
require 'concurrent/agent'
|
8
8
|
require 'concurrent/defer'
|
9
|
+
require 'concurrent/executor'
|
9
10
|
require 'concurrent/future'
|
10
11
|
require 'concurrent/goroutine'
|
11
|
-
require 'concurrent/promise'
|
12
12
|
require 'concurrent/obligation'
|
13
|
+
require 'concurrent/promise'
|
14
|
+
require 'concurrent/supervisor'
|
13
15
|
require 'concurrent/utilities'
|
14
16
|
|
17
|
+
require 'concurrent/reactor'
|
18
|
+
require 'concurrent/reactor/drb_async_demux'
|
19
|
+
require 'concurrent/reactor/tcp_sync_demux'
|
20
|
+
|
15
21
|
require 'concurrent/thread_pool'
|
16
22
|
require 'concurrent/cached_thread_pool'
|
17
23
|
require 'concurrent/fixed_thread_pool'
|
24
|
+
require 'concurrent/null_thread_pool'
|
18
25
|
|
19
26
|
require 'concurrent/global_thread_pool'
|
20
27
|
|
data/lib/concurrent/agent.rb
CHANGED
@@ -14,6 +14,7 @@ module Concurrent
|
|
14
14
|
# A good example of an agent is a shared incrementing counter, such as the score in a video game.
|
15
15
|
class Agent
|
16
16
|
include Observable
|
17
|
+
include UsesGlobalThreadPool
|
17
18
|
|
18
19
|
TIMEOUT = 5
|
19
20
|
|
@@ -26,15 +27,20 @@ module Concurrent
|
|
26
27
|
@rescuers = []
|
27
28
|
@validator = nil
|
28
29
|
@queue = Queue.new
|
30
|
+
@mutex = Mutex.new
|
29
31
|
|
30
|
-
|
32
|
+
Agent.thread_pool.post{ work }
|
31
33
|
end
|
32
34
|
|
33
35
|
def value(timeout = 0) return @value; end
|
34
36
|
alias_method :deref, :value
|
35
37
|
|
36
38
|
def rescue(clazz = Exception, &block)
|
37
|
-
|
39
|
+
if block_given?
|
40
|
+
@mutex.synchronize do
|
41
|
+
@rescuers << Rescuer.new(clazz, block)
|
42
|
+
end
|
43
|
+
end
|
38
44
|
return self
|
39
45
|
end
|
40
46
|
alias_method :catch, :rescue
|
@@ -50,10 +56,10 @@ module Concurrent
|
|
50
56
|
|
51
57
|
def post(&block)
|
52
58
|
return @queue.length unless block_given?
|
53
|
-
return
|
59
|
+
return @mutex.synchronize do
|
54
60
|
@queue << block
|
55
61
|
@queue.length
|
56
|
-
|
62
|
+
end
|
57
63
|
end
|
58
64
|
|
59
65
|
def <<(block)
|
@@ -62,7 +68,7 @@ module Concurrent
|
|
62
68
|
end
|
63
69
|
|
64
70
|
def length
|
65
|
-
@queue.length
|
71
|
+
return @queue.length
|
66
72
|
end
|
67
73
|
alias_method :size, :length
|
68
74
|
alias_method :count, :length
|
@@ -76,7 +82,9 @@ module Concurrent
|
|
76
82
|
|
77
83
|
# @private
|
78
84
|
def try_rescue(ex) # :nodoc:
|
79
|
-
rescuer = @
|
85
|
+
rescuer = @mutex.synchronize do
|
86
|
+
@rescuers.find{|r| ex.is_a?(r.clazz) }
|
87
|
+
end
|
80
88
|
rescuer.block.call(ex) if rescuer
|
81
89
|
rescue Exception => e
|
82
90
|
# supress
|
@@ -85,18 +93,17 @@ module Concurrent
|
|
85
93
|
# @private
|
86
94
|
def work # :nodoc:
|
87
95
|
loop do
|
88
|
-
Thread.pass
|
89
96
|
handler = @queue.pop
|
90
97
|
begin
|
91
|
-
result = Timeout.timeout(@timeout)
|
98
|
+
result = Timeout.timeout(@timeout) do
|
92
99
|
handler.call(@value)
|
93
|
-
|
100
|
+
end
|
94
101
|
if @validator.nil? || @validator.call(result)
|
95
|
-
|
102
|
+
@mutex.synchronize do
|
96
103
|
@value = result
|
97
104
|
changed
|
98
|
-
|
99
|
-
|
105
|
+
notify_observers(Time.now, @value)
|
106
|
+
end
|
100
107
|
end
|
101
108
|
rescue Exception => ex
|
102
109
|
try_rescue(ex)
|
@@ -105,31 +112,3 @@ module Concurrent
|
|
105
112
|
end
|
106
113
|
end
|
107
114
|
end
|
108
|
-
|
109
|
-
module Kernel
|
110
|
-
|
111
|
-
def agent(initial, timeout = Concurrent::Agent::TIMEOUT)
|
112
|
-
return Concurrent::Agent.new(initial, timeout)
|
113
|
-
end
|
114
|
-
module_function :agent
|
115
|
-
|
116
|
-
def deref(agent, timeout = nil)
|
117
|
-
if agent.respond_to?(:deref)
|
118
|
-
return agent.deref(timeout)
|
119
|
-
elsif agent.respond_to?(:value)
|
120
|
-
return agent.deref(timeout)
|
121
|
-
else
|
122
|
-
return nil
|
123
|
-
end
|
124
|
-
end
|
125
|
-
module_function :deref
|
126
|
-
|
127
|
-
def post(agent, &block)
|
128
|
-
if agent.respond_to?(:post)
|
129
|
-
return agent.post(&block)
|
130
|
-
else
|
131
|
-
return nil
|
132
|
-
end
|
133
|
-
end
|
134
|
-
module_function :deref
|
135
|
-
end
|
@@ -24,12 +24,11 @@ module Concurrent
|
|
24
24
|
@thread_idletime = (opts[:thread_idletime] || DEFAULT_THREAD_IDLETIME).freeze
|
25
25
|
super()
|
26
26
|
@working = 0
|
27
|
-
@mutex = Mutex.new
|
28
27
|
end
|
29
28
|
|
30
29
|
def kill
|
31
30
|
@status = :killed
|
32
|
-
|
31
|
+
mutex.synchronize do
|
33
32
|
@pool.each{|t| Thread.kill(t.thread) }
|
34
33
|
end
|
35
34
|
end
|
@@ -42,7 +41,7 @@ module Concurrent
|
|
42
41
|
raise ArgumentError.new('no block given') unless block_given?
|
43
42
|
if running?
|
44
43
|
collect_garbage if @pool.empty?
|
45
|
-
|
44
|
+
mutex.synchronize do
|
46
45
|
if @working >= @pool.length
|
47
46
|
create_worker_thread
|
48
47
|
end
|
@@ -56,7 +55,7 @@ module Concurrent
|
|
56
55
|
|
57
56
|
# @private
|
58
57
|
def status # :nodoc:
|
59
|
-
|
58
|
+
mutex.synchronize do
|
60
59
|
@pool.collect do |worker|
|
61
60
|
[
|
62
61
|
worker.status,
|
@@ -80,31 +79,31 @@ module Concurrent
|
|
80
79
|
loop do
|
81
80
|
task = @queue.pop
|
82
81
|
|
83
|
-
|
82
|
+
mutex.synchronize do
|
84
83
|
@working += 1
|
85
84
|
me.status = :working
|
86
|
-
|
85
|
+
end
|
87
86
|
|
88
87
|
if task == :stop
|
89
88
|
me.status = :stopping
|
90
89
|
break
|
91
90
|
else
|
92
91
|
task.last.call(*task.first)
|
93
|
-
|
92
|
+
mutex.synchronize do
|
94
93
|
@working -= 1
|
95
94
|
me.status = :idle
|
96
95
|
me.idletime = timestamp
|
97
|
-
|
96
|
+
end
|
98
97
|
end
|
99
98
|
end
|
100
99
|
|
101
|
-
|
100
|
+
mutex.synchronize do
|
102
101
|
@pool.delete(me)
|
103
102
|
if @pool.empty?
|
104
103
|
@termination.set
|
105
104
|
@status = :shutdown unless killed?
|
106
105
|
end
|
107
|
-
|
106
|
+
end
|
108
107
|
end
|
109
108
|
|
110
109
|
@pool << worker
|
@@ -115,7 +114,7 @@ module Concurrent
|
|
115
114
|
@collector = Thread.new do
|
116
115
|
loop do
|
117
116
|
sleep(@gc_interval)
|
118
|
-
|
117
|
+
mutex.synchronize do
|
119
118
|
@pool.reject! do |worker|
|
120
119
|
worker.thread.status.nil? ||
|
121
120
|
(worker.status == :idle && @thread_idletime >= delta(worker.idletime, timestamp))
|
data/lib/concurrent/defer.rb
CHANGED
@@ -7,14 +7,18 @@ module Concurrent
|
|
7
7
|
IllegalMethodCallError = Class.new(StandardError)
|
8
8
|
|
9
9
|
class Defer
|
10
|
+
include UsesGlobalThreadPool
|
11
|
+
|
12
|
+
def initialize(opts = {}, &block)
|
13
|
+
operation = opts[:op] || opts[:operation]
|
14
|
+
@callback = opts[:cback] || opts[:callback]
|
15
|
+
@errorback = opts[:eback] || opts[:error] || opts[:errorback]
|
16
|
+
thread_pool = opts[:pool] || opts[:thread_pool]
|
10
17
|
|
11
|
-
def initialize(operation = nil, callback = nil, errorback = nil, &block)
|
12
18
|
raise ArgumentError.new('no operation given') if operation.nil? && ! block_given?
|
13
19
|
raise ArgumentError.new('two operations given') if ! operation.nil? && block_given?
|
14
20
|
|
15
21
|
@operation = operation || block
|
16
|
-
@callback = callback
|
17
|
-
@errorback = errorback
|
18
22
|
|
19
23
|
if operation.nil?
|
20
24
|
@running = false
|
@@ -44,7 +48,7 @@ module Concurrent
|
|
44
48
|
def go
|
45
49
|
return nil if @running
|
46
50
|
@running = true
|
47
|
-
|
51
|
+
Defer.thread_pool.post { fulfill }
|
48
52
|
return nil
|
49
53
|
end
|
50
54
|
|
@@ -59,11 +63,3 @@ module Concurrent
|
|
59
63
|
end
|
60
64
|
end
|
61
65
|
end
|
62
|
-
|
63
|
-
module Kernel
|
64
|
-
|
65
|
-
def defer(*args, &block)
|
66
|
-
return Concurrent::Defer.new(*args, &block)
|
67
|
-
end
|
68
|
-
module_function :defer
|
69
|
-
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Concurrent
|
4
|
+
|
5
|
+
module Executor
|
6
|
+
extend self
|
7
|
+
|
8
|
+
class ExecutionContext
|
9
|
+
attr_reader :name
|
10
|
+
attr_reader :execution_interval
|
11
|
+
attr_reader :timeout_interval
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def initialize(name, execution_interval, timeout_interval, thread)
|
16
|
+
@name = name
|
17
|
+
@execution_interval = execution_interval
|
18
|
+
@timeout_interval = timeout_interval
|
19
|
+
@thread = thread
|
20
|
+
@thread[:stop] = false
|
21
|
+
end
|
22
|
+
|
23
|
+
public
|
24
|
+
|
25
|
+
def status
|
26
|
+
return @thread.status unless @thread.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
def join(limit = nil)
|
30
|
+
if @thread.nil?
|
31
|
+
return nil
|
32
|
+
elsif limit.nil?
|
33
|
+
return @thread.join
|
34
|
+
else
|
35
|
+
return @thread.join(limit)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def stop
|
40
|
+
@thread[:stop] = true
|
41
|
+
end
|
42
|
+
|
43
|
+
def kill
|
44
|
+
unless @thread.nil?
|
45
|
+
stop
|
46
|
+
Thread.kill(@thread)
|
47
|
+
@thread = nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
alias_method :terminate, :kill
|
51
|
+
end
|
52
|
+
|
53
|
+
EXECUTION_INTERVAL = 60
|
54
|
+
TIMEOUT_INTERVAL = 30
|
55
|
+
|
56
|
+
STDOUT_LOGGER = proc do |name, level, msg|
|
57
|
+
print "%5s (%s) %s: %s\n" % [level.upcase, Time.now.strftime("%F %T"), name, msg]
|
58
|
+
end
|
59
|
+
|
60
|
+
def run(name, opts = {})
|
61
|
+
raise ArgumentError.new('no block given') unless block_given?
|
62
|
+
|
63
|
+
execution_interval = opts[:execution] || opts[:execution_interval] || EXECUTION_INTERVAL
|
64
|
+
timeout_interval = opts[:timeout] || opts[:timeout_interval] || TIMEOUT_INTERVAL
|
65
|
+
run_now = opts[:now] || opts[:run_now] || false
|
66
|
+
logger = opts[:logger] || STDOUT_LOGGER
|
67
|
+
block_args = opts[:args] || opts [:arguments] || []
|
68
|
+
|
69
|
+
executor = Thread.new(*block_args) do |*args|
|
70
|
+
sleep(execution_interval) unless run_now == true
|
71
|
+
loop do
|
72
|
+
break if Thread.current[:stop]
|
73
|
+
begin
|
74
|
+
worker = Thread.new{ yield(*args) }
|
75
|
+
worker.abort_on_exception = false
|
76
|
+
if worker.join(timeout_interval).nil?
|
77
|
+
logger.call(name, :warn, "execution timed out after #{timeout_interval} seconds")
|
78
|
+
else
|
79
|
+
logger.call(name, :info, 'execution completed successfully')
|
80
|
+
end
|
81
|
+
rescue Exception => ex
|
82
|
+
logger.call(name, :error, "execution failed with error '#{ex}'")
|
83
|
+
ensure
|
84
|
+
Thread.kill(worker)
|
85
|
+
worker = nil
|
86
|
+
end
|
87
|
+
break if Thread.current[:stop]
|
88
|
+
sleep(execution_interval)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
return ExecutionContext.new(name, execution_interval, timeout_interval, executor)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|