concurrent-ruby 0.2.2 → 0.3.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -42
  3. data/lib/concurrent.rb +5 -6
  4. data/lib/concurrent/agent.rb +29 -33
  5. data/lib/concurrent/cached_thread_pool.rb +26 -105
  6. data/lib/concurrent/channel.rb +94 -0
  7. data/lib/concurrent/event.rb +8 -17
  8. data/lib/concurrent/executor.rb +68 -72
  9. data/lib/concurrent/fixed_thread_pool.rb +15 -83
  10. data/lib/concurrent/functions.rb +7 -22
  11. data/lib/concurrent/future.rb +29 -9
  12. data/lib/concurrent/null_thread_pool.rb +5 -2
  13. data/lib/concurrent/obligation.rb +6 -16
  14. data/lib/concurrent/promise.rb +9 -10
  15. data/lib/concurrent/runnable.rb +103 -0
  16. data/lib/concurrent/supervisor.rb +271 -44
  17. data/lib/concurrent/thread_pool.rb +112 -39
  18. data/lib/concurrent/version.rb +1 -1
  19. data/md/executor.md +9 -3
  20. data/md/goroutine.md +11 -9
  21. data/md/reactor.md +32 -0
  22. data/md/supervisor.md +43 -0
  23. data/spec/concurrent/agent_spec.rb +128 -51
  24. data/spec/concurrent/cached_thread_pool_spec.rb +33 -47
  25. data/spec/concurrent/channel_spec.rb +446 -0
  26. data/spec/concurrent/event_machine_defer_proxy_spec.rb +3 -1
  27. data/spec/concurrent/event_spec.rb +0 -19
  28. data/spec/concurrent/executor_spec.rb +167 -119
  29. data/spec/concurrent/fixed_thread_pool_spec.rb +40 -30
  30. data/spec/concurrent/functions_spec.rb +0 -20
  31. data/spec/concurrent/future_spec.rb +88 -0
  32. data/spec/concurrent/null_thread_pool_spec.rb +23 -2
  33. data/spec/concurrent/obligation_shared.rb +0 -5
  34. data/spec/concurrent/promise_spec.rb +9 -10
  35. data/spec/concurrent/runnable_shared.rb +62 -0
  36. data/spec/concurrent/runnable_spec.rb +233 -0
  37. data/spec/concurrent/supervisor_spec.rb +912 -47
  38. data/spec/concurrent/thread_pool_shared.rb +18 -31
  39. data/spec/spec_helper.rb +10 -3
  40. metadata +17 -23
  41. data/lib/concurrent/defer.rb +0 -65
  42. data/lib/concurrent/reactor.rb +0 -166
  43. data/lib/concurrent/reactor/drb_async_demux.rb +0 -83
  44. data/lib/concurrent/reactor/tcp_sync_demux.rb +0 -131
  45. data/lib/concurrent/utilities.rb +0 -32
  46. data/md/defer.md +0 -174
  47. data/spec/concurrent/defer_spec.rb +0 -199
  48. data/spec/concurrent/reactor/drb_async_demux_spec.rb +0 -196
  49. data/spec/concurrent/reactor/tcp_sync_demux_spec.rb +0 -410
  50. data/spec/concurrent/reactor_spec.rb +0 -364
  51. data/spec/concurrent/utilities_spec.rb +0 -74
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b0cd24fc7583a5575559eacbeeddc5d23fdae43f
4
- data.tar.gz: addd7382eda8182216f5d86b0c53a75030c49439
3
+ metadata.gz: ed299ebc57fa2caa62d5538e28f6506d175be4f0
4
+ data.tar.gz: 4e160d0efeefab15be0c1767679079f9d07b5b59
5
5
  SHA512:
6
- metadata.gz: 658d79b611fd7f572beeb73096b80addf9fab04ee8a06f75f914a3dd780dad86038519c5338d73fb540dd62d0683dce26c5c25b5419978bef79c5e72171e9052
7
- data.tar.gz: fa5977a3f4bb3d1195f0aa4a00fc3fea84f86ee4d9d751d2c3d1916e6e94fe09daa74885016769e41d5640d7567cdc6d47e0bbd32cbdb8bf5546041a1bf9d3aa
6
+ metadata.gz: 5687e0000024468f66f42cfd4742ff672502ed6f9cde7a9579a84acfdcdfde8af33b2df28a0b387f36767bfefe3d70411b8efa7a5ba8cb3f7a684112db5a11ec
7
+ data.tar.gz: d8e45ab66ecb79ad64310e8a31f604c146840d9abf5bd51e9dcbf48279767dd0c9a02fad2f37e5bef25f9ec5ca656d92bf87ccbcced9e2aa7ff9bc8e9e47fca5
data/README.md CHANGED
@@ -1,8 +1,11 @@
1
- # Concurrent Ruby [![Build Status](https://secure.travis-ci.org/jdantonio/concurrent-ruby.png)](https://travis-ci.org/jdantonio/concurrent-ruby?branch=master) [![Dependency Status](https://gemnasium.com/jdantonio/concurrent-ruby.png)](https://gemnasium.com/jdantonio/concurrent-ruby)
1
+ # Concurrent Ruby [![Build Status](https://secure.travis-ci.org/jdantonio/concurrent-ruby.png)](https://travis-ci.org/jdantonio/concurrent-ruby?branch=master) [![Coverage Status](https://coveralls.io/repos/jdantonio/concurrent-ruby/badge.png)](https://coveralls.io/r/jdantonio/concurrent-ruby) [![Dependency Status](https://gemnasium.com/jdantonio/concurrent-ruby.png)](https://gemnasium.com/jdantonio/concurrent-ruby)
2
2
 
3
- Modern concurrency tools including agents, futures, promises, thread pools, reactors, supervisors, and more.
3
+ Modern concurrency tools including agents, futures, promises, thread pools, supervisors, and more.
4
4
  Inspired by Erlang, Clojure, Go, JavaScript, actors, and classic concurrency patterns.
5
5
 
6
+ If you find this gem useful you should check out my [functional-ruby](https://github.com/jdantonio/functional-ruby)
7
+ gem, too. This gem uses several of the tools in that gem.
8
+
6
9
  ## Introduction
7
10
 
8
11
  The old-school "lock and synchronize" approach to concurrency is dead. The future of concurrency
@@ -51,12 +54,13 @@ Specifically:
51
54
  Several features from Erlang, Go, Clojure, Java, and JavaScript have been implemented thus far:
52
55
 
53
56
  * Clojure inspired [Agent](https://github.com/jdantonio/concurrent-ruby/blob/master/md/agent.md)
54
- * EventMachine inspired [Defer](https://github.com/jdantonio/concurrent-ruby/blob/master/md/defer.md)
55
57
  * Clojure inspired [Future](https://github.com/jdantonio/concurrent-ruby/blob/master/md/future.md)
56
58
  * Go inspired [Goroutine](https://github.com/jdantonio/concurrent-ruby/blob/master/md/goroutine.md)
57
59
  * JavaScript inspired [Promise](https://github.com/jdantonio/concurrent-ruby/blob/master/md/promise.md)
58
60
  * Java inspired [Thread Pools](https://github.com/jdantonio/concurrent-ruby/blob/master/md/thread_pool.md)
61
+ * Old school [events](http://msdn.microsoft.com/en-us/library/windows/desktop/ms682655(v=vs.85).aspx) from back in my Visual C++ days
59
62
  * Scheduled task execution with the [Executor](https://github.com/jdantonio/concurrent-ruby/blob/master/md/executor.md) service
63
+ * Erlang-inspired [Supervisor](https://github.com/jdantonio/concurrent-ruby/blob/master/md/supervisor.md) for managing long-running threads
60
64
 
61
65
  ### Is it any good?
62
66
 
@@ -118,10 +122,17 @@ For complete examples, see the specific documentation linked above. Below are a
118
122
  ```ruby
119
123
  require 'concurrent'
120
124
 
121
- @expected = nil
122
- go(1, 2, 3){|a, b, c| @expected = [c, b, a] }
123
- sleep(0.1)
124
- @expected #=> [3, 2, 1]
125
+ go('foo'){|echo| sleep(0.1); print "#{echo}\n"; sleep(0.1); print "Boom!\n" }
126
+ go('bar'){|echo| sleep(0.1); print "#{echo}\n"; sleep(0.1); print "Pow!\n" }
127
+ go('baz'){|echo| sleep(0.1); print "#{echo}\n"; sleep(0.1); print "Zap!\n" }
128
+ sleep(0.5)
129
+
130
+ #=> foo
131
+ #=> bar
132
+ #=> baz
133
+ #=> Boom!
134
+ #=> Pow!
135
+ #=> Zap!
125
136
  ```
126
137
 
127
138
  #### Agent (Clojure)
@@ -146,28 +157,6 @@ sleep(0.1)
146
157
  score.value #=> 170
147
158
  ```
148
159
 
149
- #### Defer (EventMachine)
150
-
151
- ```ruby
152
- require 'concurrent'
153
- require 'concurrent/functions'
154
-
155
- Concurrent::Defer.new{ "Jerry D'Antonio" }.
156
- then{|result| puts "Hello, #{result}!" }.
157
- rescue{|ex| puts ex.message }.
158
- go
159
-
160
- #=> Hello, Jerry D'Antonio!
161
-
162
- operation = proc{ raise StandardError.new('Boom!') }
163
- callback = proc{|result| puts result }
164
- errorback = proc{|ex| puts ex.message }
165
- defer(operation, callback, errorback)
166
- sleep(0.1)
167
-
168
- #=> "Boom!"
169
- ```
170
-
171
160
  #### Future (Clojure)
172
161
 
173
162
  ```ruby
@@ -200,23 +189,37 @@ p.value #=> "Hello Jerry D'Antonio. Would you like to play a game?"
200
189
  ```ruby
201
190
  require 'concurrent'
202
191
 
203
- pool = Concurrent::FixedThreadPool.new(10)
204
- @expected = 0
205
- pool.post{ sleep(0.5); @expected += 100 }
206
- pool.post{ sleep(0.5); @expected += 100 }
207
- pool.post{ sleep(0.5); @expected += 100 }
208
- @expected #=> nil
192
+ pool = Concurrent::FixedThreadPool.new(2)
193
+ pool.size #=> 2
194
+
195
+ pool.post{ sleep(0.5); print "Boom!\n" }
196
+ pool.size #=> 2
197
+ pool.post{ sleep(0.5); print "Pow!\n" }
198
+ pool.size #=> 2
199
+ pool.post{ sleep(0.5); print "Zap!\n" }
200
+ pool.size #=> 2
201
+
209
202
  sleep(1)
210
- @expected #=> 300
203
+
204
+ #=> Boom!
205
+ #=> Pow!
206
+ #=> Zap!
211
207
 
212
208
  pool = Concurrent::CachedThreadPool.new
213
- @expected = 0
214
- pool << proc{ sleep(0.5); @expected += 10 }
215
- pool << proc{ sleep(0.5); @expected += 10 }
216
- pool << proc{ sleep(0.5); @expected += 10 }
217
- @expected #=> 0
209
+ pool.size #=> 0
210
+
211
+ pool << proc{ sleep(0.5); print "Boom!\n" }
212
+ pool.size #=> 1
213
+ pool << proc{ sleep(0.5); print "Pow!\n" }
214
+ pool.size #=> 2
215
+ pool << proc{ sleep(0.5); print "Zap!\n" }
216
+ pool.size #=> 3
217
+
218
218
  sleep(1)
219
- @expected #=> 30
219
+
220
+ #=> Boom!
221
+ #=> Pow!
222
+ #=> Zap!
220
223
  ```
221
224
 
222
225
  #### Executor
@@ -1,22 +1,21 @@
1
1
  require 'thread'
2
2
 
3
+ $ENABLE_BEHAVIOR_CHECK_ON_CONSTRUCTION ||= ( $DEGUG ? true : false )
4
+ require 'functional'
5
+
3
6
  require 'concurrent/version'
4
7
 
5
8
  require 'concurrent/event'
6
9
 
7
10
  require 'concurrent/agent'
8
- require 'concurrent/defer'
11
+ require 'concurrent/channel'
9
12
  require 'concurrent/executor'
10
13
  require 'concurrent/future'
11
14
  require 'concurrent/goroutine'
12
15
  require 'concurrent/obligation'
13
16
  require 'concurrent/promise'
17
+ require 'concurrent/runnable'
14
18
  require 'concurrent/supervisor'
15
- require 'concurrent/utilities'
16
-
17
- require 'concurrent/reactor'
18
- require 'concurrent/reactor/drb_async_demux'
19
- require 'concurrent/reactor/tcp_sync_demux'
20
19
 
21
20
  require 'concurrent/thread_pool'
22
21
  require 'concurrent/cached_thread_pool'
@@ -1,8 +1,7 @@
1
- require 'observer'
2
1
  require 'thread'
2
+ require 'observer'
3
3
 
4
4
  require 'concurrent/global_thread_pool'
5
- require 'concurrent/utilities'
6
5
 
7
6
  module Concurrent
8
7
 
@@ -21,21 +20,31 @@ module Concurrent
21
20
  attr_reader :initial
22
21
  attr_reader :timeout
23
22
 
24
- def initialize(initial, timeout = TIMEOUT)
23
+ def initialize(initial, opts = {})
25
24
  @value = initial
26
- @timeout = timeout
27
25
  @rescuers = []
28
26
  @validator = nil
29
- @queue = Queue.new
30
- @mutex = Mutex.new
31
27
 
32
- Agent.thread_pool.post{ work }
28
+ @timeout = opts[:timeout] || TIMEOUT
29
+ @dup_on_deref = opts[:dup_on_deref] || opts[:dup] || false
30
+ @freeze_on_deref = opts[:freeze_on_deref] || opts[:freeze] || false
31
+ @copy_on_deref = opts[:copy_on_deref] || opts[:copy]
32
+
33
+ @mutex = Mutex.new
33
34
  end
34
35
 
35
- def value(timeout = 0) return @value; end
36
+ def value(timeout = 0)
37
+ return @mutex.synchronize do
38
+ value = @value
39
+ value = @copy_on_deref.call(value) if @copy_on_deref
40
+ value = value.dup if @dup_on_deref
41
+ value = value.freeze if @freeze_on_deref
42
+ value
43
+ end
44
+ end
36
45
  alias_method :deref, :value
37
46
 
38
- def rescue(clazz = Exception, &block)
47
+ def rescue(clazz = nil, &block)
39
48
  if block_given?
40
49
  @mutex.synchronize do
41
50
  @rescuers << Rescuer.new(clazz, block)
@@ -55,11 +64,7 @@ module Concurrent
55
64
  alias_method :validates_with, :validate
56
65
 
57
66
  def post(&block)
58
- return @queue.length unless block_given?
59
- return @mutex.synchronize do
60
- @queue << block
61
- @queue.length
62
- end
67
+ Agent.thread_pool.post{ work(&block) } if block_given?
63
68
  end
64
69
 
65
70
  def <<(block)
@@ -67,12 +72,6 @@ module Concurrent
67
72
  return self
68
73
  end
69
74
 
70
- def length
71
- return @queue.length
72
- end
73
- alias_method :size, :length
74
- alias_method :count, :length
75
-
76
75
  alias_method :add_watch, :add_observer
77
76
 
78
77
  private
@@ -83,31 +82,28 @@ module Concurrent
83
82
  # @private
84
83
  def try_rescue(ex) # :nodoc:
85
84
  rescuer = @mutex.synchronize do
86
- @rescuers.find{|r| ex.is_a?(r.clazz) }
85
+ @rescuers.find{|r| r.clazz.nil? || ex.is_a?(r.clazz) }
87
86
  end
88
87
  rescuer.block.call(ex) if rescuer
89
- rescue Exception => e
88
+ rescue Exception => ex
90
89
  # supress
91
90
  end
92
91
 
93
92
  # @private
94
- def work # :nodoc:
95
- loop do
96
- handler = @queue.pop
97
- begin
93
+ def work(&handler) # :nodoc:
94
+ begin
95
+ @mutex.synchronize do
98
96
  result = Timeout.timeout(@timeout) do
99
97
  handler.call(@value)
100
98
  end
101
99
  if @validator.nil? || @validator.call(result)
102
- @mutex.synchronize do
103
- @value = result
104
- changed
105
- notify_observers(Time.now, @value)
106
- end
100
+ @value = result
101
+ changed
102
+ notify_observers(Time.now, @value)
107
103
  end
108
- rescue Exception => ex
109
- try_rescue(ex)
110
104
  end
105
+ rescue Exception => ex
106
+ try_rescue(ex)
111
107
  end
112
108
  end
113
109
  end
@@ -1,131 +1,52 @@
1
- require 'thread'
2
-
3
1
  require 'concurrent/thread_pool'
4
- require 'concurrent/utilities'
5
-
6
- require 'functional/utilities'
7
2
 
8
3
  module Concurrent
9
4
 
10
- def self.new_cached_thread_pool
11
- return CachedThreadPool.new
12
- end
5
+ class CachedThreadPool < AbstractThreadPool
6
+ behavior(:global_thread_pool)
13
7
 
14
- class CachedThreadPool < ThreadPool
15
- behavior(:thread_pool)
16
-
17
- DEFAULT_GC_INTERVAL = 60
18
8
  DEFAULT_THREAD_IDLETIME = 60
19
9
 
20
- attr_reader :working
21
-
22
10
  def initialize(opts = {})
23
- @gc_interval = (opts[:gc_interval] || DEFAULT_GC_INTERVAL).freeze
24
- @thread_idletime = (opts[:thread_idletime] || DEFAULT_THREAD_IDLETIME).freeze
25
- super()
26
- @working = 0
27
- end
28
-
29
- def kill
30
- @status = :killed
31
- mutex.synchronize do
32
- @pool.each{|t| Thread.kill(t.thread) }
33
- end
34
- end
35
-
36
- def size
37
- return @pool.length
11
+ @idletime = (opts[:idletime] || DEFAULT_THREAD_IDLETIME).to_i
12
+ super(opts)
38
13
  end
39
14
 
40
15
  def post(*args, &block)
41
16
  raise ArgumentError.new('no block given') unless block_given?
42
- if running?
43
- collect_garbage if @pool.empty?
44
- mutex.synchronize do
45
- if @working >= @pool.length
46
- create_worker_thread
47
- end
17
+ return @mutex.synchronize do
18
+ if @state == :running
48
19
  @queue << [args, block]
49
- end
50
- return true
51
- else
52
- return false
53
- end
54
- end
55
-
56
- # @private
57
- def status # :nodoc:
58
- mutex.synchronize do
59
- @pool.collect do |worker|
60
- [
61
- worker.status,
62
- worker.status == :idle ? delta(worker.idletime, timestamp) : nil,
63
- worker.thread.status
64
- ]
20
+ at_capacity = @pool.empty? || ! @queue.empty? || @working >= @pool.size
21
+ if at_capacity && @pool.length < @max_threads
22
+ @pool << create_worker_thread
23
+ end
24
+ true
25
+ else
26
+ false
65
27
  end
66
28
  end
67
29
  end
68
30
 
69
- private
70
-
71
- Worker = Struct.new(:status, :idletime, :thread)
72
-
73
- # @private
74
- def create_worker_thread # :nodoc:
75
- worker = Worker.new(:idle, timestamp, nil)
76
-
77
- worker.thread = Thread.new(worker) do |me|
78
-
79
- loop do
80
- task = @queue.pop
31
+ protected
81
32
 
82
- mutex.synchronize do
83
- @working += 1
84
- me.status = :working
85
- end
86
-
87
- if task == :stop
88
- me.status = :stopping
89
- break
90
- else
91
- task.last.call(*task.first)
92
- mutex.synchronize do
93
- @working -= 1
94
- me.status = :idle
95
- me.idletime = timestamp
96
- end
97
- end
98
- end
33
+ def dead_worker?(context)
34
+ return context.thread.nil? || context.thread.status == 'aborting' || ! context.thread.status
35
+ end
99
36
 
100
- mutex.synchronize do
101
- @pool.delete(me)
102
- if @pool.empty?
103
- @termination.set
104
- @status = :shutdown unless killed?
105
- end
106
- end
37
+ def stale_worker?(context)
38
+ if context.status == :idle && @idletime <= (timestamp - context.idletime)
39
+ context.thread.kill
40
+ return true
41
+ else
42
+ return false
107
43
  end
108
-
109
- worker.thread.abort_on_exception = false
110
- @pool << worker
111
44
  end
112
45
 
113
- # @private
114
- def collect_garbage # :nodoc:
115
- @collector = Thread.new do
116
- loop do
117
- sleep(@gc_interval)
118
- mutex.synchronize do
119
- @pool.reject! do |worker|
120
- worker.thread.status.nil? ||
121
- (worker.status == :idle && @thread_idletime >= delta(worker.idletime, timestamp))
122
- end
123
- end
124
- @working = @pool.count{|worker| worker.status == :working}
125
- break if @pool.empty?
126
- end
46
+ def collect_garbage
47
+ @pool.reject! do |context|
48
+ dead_worker?(context) || stale_worker?(context)
127
49
  end
128
- @collector.abort_on_exception = false
129
50
  end
130
51
  end
131
52
  end
@@ -0,0 +1,94 @@
1
+ require 'thread'
2
+ require 'observer'
3
+
4
+ require 'concurrent/runnable'
5
+
6
+ module Concurrent
7
+
8
+ class Channel
9
+ include Observable
10
+ include Runnable
11
+ behavior(:runnable)
12
+
13
+ def initialize(errorback = nil, &block)
14
+ @queue = Queue.new
15
+ @task = block
16
+ @errorback = errorback
17
+ end
18
+
19
+ def post(*message)
20
+ return false unless running?
21
+ @queue.push(message)
22
+ return @queue.length
23
+ end
24
+
25
+ def <<(message)
26
+ post(*message)
27
+ return self
28
+ end
29
+
30
+ def self.pool(count, errorback = nil, &block)
31
+ raise ArgumentError.new('count must be greater than zero') unless count > 0
32
+ mailbox = Queue.new
33
+ channels = count.times.collect do
34
+ channel = self.new(errorback, &block)
35
+ channel.instance_variable_set(:@queue, mailbox)
36
+ channel
37
+ end
38
+ return Poolbox.new(mailbox), channels
39
+ end
40
+
41
+ protected
42
+
43
+ class Poolbox
44
+
45
+ def initialize(queue)
46
+ @queue = queue
47
+ end
48
+
49
+ def post(*message)
50
+ @queue.push(message)
51
+ return @queue.length
52
+ end
53
+
54
+ def <<(message)
55
+ post(*message)
56
+ return self
57
+ end
58
+ end
59
+
60
+ # @private
61
+ def on_run # :nodoc:
62
+ @queue.clear
63
+ end
64
+
65
+ # @private
66
+ def on_stop # :nodoc:
67
+ @queue.clear
68
+ @queue.push(:stop)
69
+ end
70
+
71
+ # @private
72
+ def on_task # :nodoc:
73
+ message = @queue.pop
74
+ return if message == :stop
75
+ begin
76
+ result = receive(*message)
77
+ changed
78
+ notify_observers(Time.now, message, result)
79
+ rescue => ex
80
+ on_error(Time.now, message, ex)
81
+ end
82
+ end
83
+
84
+ # @private
85
+ def on_error(time, msg, ex) # :nodoc:
86
+ @errorback.call(time, msg, ex) if @errorback
87
+ end
88
+
89
+ # @private
90
+ def receive(*message) # :nodoc:
91
+ @task.call(*message) unless @task.nil?
92
+ end
93
+ end
94
+ end