concurrent-ruby 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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