concurrent-ruby 0.2.2 → 0.3.0.pre.1

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