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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +48 -1
  3. data/lib/concurrent.rb +8 -1
  4. data/lib/concurrent/agent.rb +19 -40
  5. data/lib/concurrent/cached_thread_pool.rb +10 -11
  6. data/lib/concurrent/defer.rb +8 -12
  7. data/lib/concurrent/executor.rb +95 -0
  8. data/lib/concurrent/fixed_thread_pool.rb +12 -6
  9. data/lib/concurrent/functions.rb +120 -0
  10. data/lib/concurrent/future.rb +8 -20
  11. data/lib/concurrent/global_thread_pool.rb +13 -0
  12. data/lib/concurrent/goroutine.rb +5 -1
  13. data/lib/concurrent/null_thread_pool.rb +22 -0
  14. data/lib/concurrent/obligation.rb +10 -64
  15. data/lib/concurrent/promise.rb +38 -60
  16. data/lib/concurrent/reactor.rb +166 -0
  17. data/lib/concurrent/reactor/drb_async_demux.rb +83 -0
  18. data/lib/concurrent/reactor/tcp_sync_demux.rb +131 -0
  19. data/lib/concurrent/supervisor.rb +100 -0
  20. data/lib/concurrent/thread_pool.rb +16 -5
  21. data/lib/concurrent/utilities.rb +8 -0
  22. data/lib/concurrent/version.rb +1 -1
  23. data/md/defer.md +4 -4
  24. data/md/executor.md +187 -0
  25. data/md/promise.md +2 -0
  26. data/md/thread_pool.md +27 -0
  27. data/spec/concurrent/agent_spec.rb +8 -27
  28. data/spec/concurrent/cached_thread_pool_spec.rb +14 -1
  29. data/spec/concurrent/defer_spec.rb +17 -21
  30. data/spec/concurrent/event_machine_defer_proxy_spec.rb +159 -149
  31. data/spec/concurrent/executor_spec.rb +200 -0
  32. data/spec/concurrent/fixed_thread_pool_spec.rb +2 -3
  33. data/spec/concurrent/functions_spec.rb +217 -0
  34. data/spec/concurrent/future_spec.rb +4 -11
  35. data/spec/concurrent/global_thread_pool_spec.rb +38 -0
  36. data/spec/concurrent/goroutine_spec.rb +15 -0
  37. data/spec/concurrent/null_thread_pool_spec.rb +54 -0
  38. data/spec/concurrent/obligation_shared.rb +127 -116
  39. data/spec/concurrent/promise_spec.rb +16 -14
  40. data/spec/concurrent/reactor/drb_async_demux_spec.rb +196 -0
  41. data/spec/concurrent/reactor/tcp_sync_demux_spec.rb +410 -0
  42. data/spec/concurrent/reactor_spec.rb +364 -0
  43. data/spec/concurrent/supervisor_spec.rb +258 -0
  44. data/spec/concurrent/thread_pool_shared.rb +156 -161
  45. data/spec/concurrent/utilities_spec.rb +30 -1
  46. data/spec/spec_helper.rb +13 -0
  47. metadata +38 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ace46fc451714785036604e4174b57f81ff2f3c6
4
- data.tar.gz: f5278235efdd0cc59ae6645aace41c35ef61c8ef
3
+ metadata.gz: 336769e53b0dcf7aa0aa3952ca396deaabfd2886
4
+ data.tar.gz: a8f2de814918d3ab9849636741674d9731246c45
5
5
  SHA512:
6
- metadata.gz: 48569321a92ec1304c60d425af98412f56dad24ce4fa6c17b149fa18cf40221df320a95ed806196a017a8f521c3d935433b02b626901ec28de5bac5ddcf92332
7
- data.tar.gz: f5731e01e2a559c25d92d11f9c9693fafd8219aa681711962dc8d777a46198fb3e3c0d97e26461d4e4d05e3aad8c8cf8b6aee17fbb31441c83fb36bcc91da920
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
- Optimized for and tested under MRI Ruby 1.9.x and 2.0.
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
@@ -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
 
@@ -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
- $GLOBAL_THREAD_POOL << proc{ work }
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
- @rescuers << Rescuer.new(clazz, block) if block_given?
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 atomic {
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 = @rescuers.find{|r| ex.is_a?(r.clazz) }
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
- atomic {
102
+ @mutex.synchronize do
96
103
  @value = result
97
104
  changed
98
- }
99
- notify_observers(Time.now, @value)
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
- @mutex.synchronize do
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
- @mutex.synchronize do
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
- @mutex.synchronize do
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
- atomic {
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
- atomic {
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
- atomic {
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
- @mutex.synchronize do
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))
@@ -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
- $GLOBAL_THREAD_POOL.post { Thread.pass; fulfill }
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