concurrent-ruby 0.5.0 → 0.6.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +88 -77
  3. data/lib/concurrent.rb +17 -2
  4. data/lib/concurrent/actor.rb +17 -0
  5. data/lib/concurrent/actor_context.rb +31 -0
  6. data/lib/concurrent/actor_ref.rb +39 -0
  7. data/lib/concurrent/agent.rb +12 -3
  8. data/lib/concurrent/async.rb +290 -0
  9. data/lib/concurrent/atomic.rb +5 -9
  10. data/lib/concurrent/cached_thread_pool.rb +39 -137
  11. data/lib/concurrent/channel/blocking_ring_buffer.rb +60 -0
  12. data/lib/concurrent/channel/buffered_channel.rb +83 -0
  13. data/lib/concurrent/channel/channel.rb +11 -0
  14. data/lib/concurrent/channel/probe.rb +19 -0
  15. data/lib/concurrent/channel/ring_buffer.rb +54 -0
  16. data/lib/concurrent/channel/unbuffered_channel.rb +34 -0
  17. data/lib/concurrent/channel/waitable_list.rb +38 -0
  18. data/lib/concurrent/configuration.rb +92 -0
  19. data/lib/concurrent/dataflow.rb +9 -3
  20. data/lib/concurrent/delay.rb +88 -0
  21. data/lib/concurrent/exchanger.rb +31 -0
  22. data/lib/concurrent/fixed_thread_pool.rb +28 -122
  23. data/lib/concurrent/future.rb +10 -5
  24. data/lib/concurrent/immediate_executor.rb +3 -2
  25. data/lib/concurrent/ivar.rb +2 -1
  26. data/lib/concurrent/java_cached_thread_pool.rb +45 -0
  27. data/lib/concurrent/java_fixed_thread_pool.rb +37 -0
  28. data/lib/concurrent/java_thread_pool_executor.rb +194 -0
  29. data/lib/concurrent/per_thread_executor.rb +23 -0
  30. data/lib/concurrent/postable.rb +2 -0
  31. data/lib/concurrent/processor_count.rb +125 -0
  32. data/lib/concurrent/promise.rb +42 -18
  33. data/lib/concurrent/ruby_cached_thread_pool.rb +37 -0
  34. data/lib/concurrent/ruby_fixed_thread_pool.rb +31 -0
  35. data/lib/concurrent/ruby_thread_pool_executor.rb +268 -0
  36. data/lib/concurrent/ruby_thread_pool_worker.rb +69 -0
  37. data/lib/concurrent/simple_actor_ref.rb +124 -0
  38. data/lib/concurrent/thread_local_var.rb +1 -1
  39. data/lib/concurrent/thread_pool_executor.rb +30 -0
  40. data/lib/concurrent/timer_task.rb +13 -10
  41. data/lib/concurrent/tvar.rb +212 -0
  42. data/lib/concurrent/utilities.rb +1 -0
  43. data/lib/concurrent/version.rb +1 -1
  44. data/spec/concurrent/actor_context_spec.rb +37 -0
  45. data/spec/concurrent/actor_ref_shared.rb +313 -0
  46. data/spec/concurrent/actor_spec.rb +9 -1
  47. data/spec/concurrent/agent_spec.rb +97 -96
  48. data/spec/concurrent/async_spec.rb +320 -0
  49. data/spec/concurrent/cached_thread_pool_shared.rb +137 -0
  50. data/spec/concurrent/channel/blocking_ring_buffer_spec.rb +149 -0
  51. data/spec/concurrent/channel/buffered_channel_spec.rb +151 -0
  52. data/spec/concurrent/channel/channel_spec.rb +37 -0
  53. data/spec/concurrent/channel/probe_spec.rb +49 -0
  54. data/spec/concurrent/channel/ring_buffer_spec.rb +126 -0
  55. data/spec/concurrent/channel/unbuffered_channel_spec.rb +132 -0
  56. data/spec/concurrent/configuration_spec.rb +134 -0
  57. data/spec/concurrent/dataflow_spec.rb +109 -27
  58. data/spec/concurrent/delay_spec.rb +77 -0
  59. data/spec/concurrent/exchanger_spec.rb +66 -0
  60. data/spec/concurrent/fixed_thread_pool_shared.rb +136 -0
  61. data/spec/concurrent/future_spec.rb +60 -51
  62. data/spec/concurrent/global_thread_pool_shared.rb +33 -0
  63. data/spec/concurrent/immediate_executor_spec.rb +4 -25
  64. data/spec/concurrent/ivar_spec.rb +36 -23
  65. data/spec/concurrent/java_cached_thread_pool_spec.rb +64 -0
  66. data/spec/concurrent/java_fixed_thread_pool_spec.rb +64 -0
  67. data/spec/concurrent/java_thread_pool_executor_spec.rb +71 -0
  68. data/spec/concurrent/obligation_shared.rb +32 -20
  69. data/spec/concurrent/{global_thread_pool_spec.rb → per_thread_executor_spec.rb} +9 -13
  70. data/spec/concurrent/processor_count_spec.rb +20 -0
  71. data/spec/concurrent/promise_spec.rb +29 -41
  72. data/spec/concurrent/ruby_cached_thread_pool_spec.rb +69 -0
  73. data/spec/concurrent/ruby_fixed_thread_pool_spec.rb +39 -0
  74. data/spec/concurrent/ruby_thread_pool_executor_spec.rb +183 -0
  75. data/spec/concurrent/simple_actor_ref_spec.rb +219 -0
  76. data/spec/concurrent/thread_pool_class_cast_spec.rb +40 -0
  77. data/spec/concurrent/thread_pool_executor_shared.rb +155 -0
  78. data/spec/concurrent/thread_pool_shared.rb +98 -36
  79. data/spec/concurrent/tvar_spec.rb +137 -0
  80. data/spec/spec_helper.rb +4 -0
  81. data/spec/support/functions.rb +4 -0
  82. metadata +85 -20
  83. data/lib/concurrent/cached_thread_pool/worker.rb +0 -91
  84. data/lib/concurrent/channel.rb +0 -63
  85. data/lib/concurrent/fixed_thread_pool/worker.rb +0 -54
  86. data/lib/concurrent/global_thread_pool.rb +0 -42
  87. data/spec/concurrent/cached_thread_pool_spec.rb +0 -101
  88. data/spec/concurrent/channel_spec.rb +0 -86
  89. data/spec/concurrent/fixed_thread_pool_spec.rb +0 -92
  90. data/spec/concurrent/uses_global_thread_pool_shared.rb +0 -64
@@ -0,0 +1,69 @@
1
+ require 'thread'
2
+
3
+ module Concurrent
4
+
5
+ # @!visibility private
6
+ class RubyThreadPoolWorker # :nodoc:
7
+
8
+ # @!visibility private
9
+ def initialize(queue, parent) # :nodoc:
10
+ @queue = queue
11
+ @parent = parent
12
+ @mutex = Mutex.new
13
+ @last_activity = Time.now.to_f
14
+ end
15
+
16
+ # @!visibility private
17
+ def dead? # :nodoc:
18
+ return @mutex.synchronize do
19
+ @thread.nil? ? false : ! @thread.alive?
20
+ end
21
+ end
22
+
23
+ # @!visibility private
24
+ def last_activity # :nodoc:
25
+ @mutex.synchronize { @last_activity }
26
+ end
27
+
28
+ def status
29
+ @mutex.synchronize do
30
+ return 'not running' if @thread.nil?
31
+ @thread.status
32
+ end
33
+ end
34
+
35
+ # @!visibility private
36
+ def kill # :nodoc:
37
+ @mutex.synchronize do
38
+ Thread.kill(@thread) unless @thread.nil?
39
+ @thread = nil
40
+ end
41
+ end
42
+
43
+ # @!visibility private
44
+ def run(thread = Thread.current) # :nodoc:
45
+ @mutex.synchronize do
46
+ raise StandardError.new('already running') unless @thread.nil?
47
+ @thread = thread
48
+ end
49
+
50
+ loop do
51
+ task = @queue.pop
52
+ if task == :stop
53
+ @thread = nil
54
+ @parent.on_worker_exit(self)
55
+ break
56
+ end
57
+
58
+ begin
59
+ task.last.call(*task.first)
60
+ rescue => ex
61
+ # let it fail
62
+ ensure
63
+ @last_activity = Time.now.to_f
64
+ @parent.on_end_task
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,124 @@
1
+ require 'thread'
2
+
3
+ require 'concurrent/actor_ref'
4
+ require 'concurrent/event'
5
+ require 'concurrent/ivar'
6
+
7
+ module Concurrent
8
+
9
+ class SimpleActorRef
10
+ include ActorRef
11
+
12
+ def initialize(actor, opts = {})
13
+ @actor = actor
14
+ @mutex = Mutex.new
15
+ @queue = Queue.new
16
+ @thread = nil
17
+ @stop_event = Event.new
18
+ @abort_on_exception = opts.fetch(:abort_on_exception, true)
19
+ @reset_on_error = opts.fetch(:reset_on_error, true)
20
+ @exception_class = opts.fetch(:rescue_exception, false) ? Exception : StandardError
21
+ @observers = CopyOnNotifyObserverSet.new
22
+
23
+ @actor.define_singleton_method(:shutdown, &method(:set_stop_event))
24
+ end
25
+
26
+ def running?
27
+ ! @stop_event.set?
28
+ end
29
+
30
+ def shutdown?
31
+ @stop_event.set?
32
+ end
33
+
34
+ def post(*msg, &block)
35
+ raise ArgumentError.new('message cannot be empty') if msg.empty?
36
+ @mutex.synchronize do
37
+ supervise unless shutdown?
38
+ end
39
+ ivar = IVar.new
40
+ @queue.push(Message.new(msg, ivar, block))
41
+ ivar
42
+ end
43
+
44
+ def post!(seconds, *msg)
45
+ raise Concurrent::TimeoutError if seconds == 0
46
+ ivar = self.post(*msg)
47
+ ivar.value(seconds)
48
+ if ivar.incomplete?
49
+ raise Concurrent::TimeoutError
50
+ elsif ivar.reason
51
+ raise ivar.reason
52
+ end
53
+ ivar.value
54
+ end
55
+
56
+ def shutdown
57
+ @mutex.synchronize do
58
+ return if shutdown?
59
+ if @thread && @thread.alive?
60
+ @thread.kill
61
+ @actor.on_shutdown
62
+ end
63
+ @stop_event.set
64
+ end
65
+ end
66
+
67
+ def join(timeout = nil)
68
+ @stop_event.wait(timeout)
69
+ end
70
+
71
+ private
72
+
73
+ Message = Struct.new(:payload, :ivar, :callback)
74
+
75
+ def set_stop_event
76
+ @stop_event.set
77
+ end
78
+
79
+ def supervise
80
+ if @thread.nil?
81
+ @actor.on_start
82
+ @thread = new_worker_thread
83
+ elsif ! @thread.alive?
84
+ @actor.on_reset
85
+ @thread = new_worker_thread
86
+ end
87
+ end
88
+
89
+ def new_worker_thread
90
+ Thread.new do
91
+ Thread.current.abort_on_exception = @abort_on_exception
92
+ run_message_loop
93
+ end
94
+ end
95
+
96
+ def run_message_loop
97
+ loop do
98
+ message = @queue.pop
99
+ result = ex = nil
100
+
101
+ begin
102
+ result = @actor.receive(*message.payload)
103
+ rescue @exception_class => ex
104
+ @actor.on_error(Time.now, message.payload, ex)
105
+ @actor.on_reset if @reset_on_error
106
+ ensure
107
+ now = Time.now
108
+ message.ivar.complete(ex.nil?, result, ex)
109
+
110
+ begin
111
+ message.callback.call(now, result, ex) if message.callback
112
+ rescue @exception_class => ex
113
+ # suppress
114
+ end
115
+
116
+ observers.notify_observers(now, message.payload, result, ex)
117
+ end
118
+
119
+ break if @stop_event.set?
120
+ end
121
+ @actor.on_shutdown
122
+ end
123
+ end
124
+ end
@@ -75,7 +75,7 @@ module Concurrent
75
75
 
76
76
  NIL_SENTINEL = Object.new
77
77
 
78
- if defined? java.lang
78
+ if RUBY_PLATFORM == 'java'
79
79
  include ThreadLocalJavaStorage
80
80
  elsif Thread.current.respond_to?(:thread_variable_set)
81
81
  include ThreadLocalNewStorage
@@ -0,0 +1,30 @@
1
+ require 'concurrent/ruby_thread_pool_executor'
2
+
3
+ module Concurrent
4
+
5
+ if RUBY_PLATFORM == 'java'
6
+ require 'concurrent/java_thread_pool_executor'
7
+ # @!macro [attach] thread_pool_executor
8
+ #
9
+ # A thread pool...
10
+ #
11
+ # The API and behavior of this class are based on Java's +ThreadPoolExecutor+
12
+ #
13
+ # @note When running on the JVM (JRuby) this class will inherit from +JavaThreadPoolExecutor+.
14
+ # On all other platforms it will inherit from +RubyThreadPoolExecutor+.
15
+ #
16
+ # @see Concurrent::RubyThreadPoolExecutor
17
+ # @see Concurrent::JavaThreadPoolExecutor
18
+ #
19
+ # @see http://docs.oracle.com/javase/tutorial/essential/concurrency/pools.html
20
+ # @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Executors.html
21
+ # @see http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html
22
+ # @see http://stackoverflow.com/questions/17957382/fixedthreadpool-vs-fixedthreadpool-the-lesser-of-two-evils
23
+ class ThreadPoolExecutor < JavaThreadPoolExecutor
24
+ end
25
+ else
26
+ # @!macro thread_pool_executor
27
+ class ThreadPoolExecutor < RubyThreadPoolExecutor
28
+ end
29
+ end
30
+ end
@@ -9,9 +9,9 @@ require 'concurrent/utilities'
9
9
  module Concurrent
10
10
 
11
11
  # A very common currency pattern is to run a thread that performs a task at regular
12
- # intervals. The thread that peforms the task sleeps for the given interval then
12
+ # intervals. The thread that performs the task sleeps for the given interval then
13
13
  # wakes up and performs the task. Lather, rinse, repeat... This pattern causes two
14
- # problems. First, it is difficult to test the business logic of the task becuse the
14
+ # problems. First, it is difficult to test the business logic of the task because the
15
15
  # task itself is tightly coupled with the concurrency logic. Second, an exception in
16
16
  # raised while performing the task can cause the entire thread to abend. In a
17
17
  # long-running application where the task thread is intended to run for days/weeks/years
@@ -25,7 +25,7 @@ module Concurrent
25
25
  # performing logging or ancillary operations. +TimerTask+ can also be configured with a
26
26
  # timeout value allowing it to kill a task that runs too long.
27
27
  #
28
- # One other advantage of +TimerTask+ is it forces the bsiness logic to be completely decoupled
28
+ # One other advantage of +TimerTask+ is it forces the business logic to be completely decoupled
29
29
  # from the concurrency logic. The business logic can be tested separately then passed to the
30
30
  # +TimerTask+ for scheduling and running.
31
31
  #
@@ -147,7 +147,6 @@ module Concurrent
147
147
  include Dereferenceable
148
148
  include Runnable
149
149
  include Stoppable
150
- include Observable
151
150
 
152
151
  # Default +:execution_interval+
153
152
  EXECUTION_INTERVAL = 60
@@ -171,7 +170,7 @@ module Concurrent
171
170
  # @option opts [Integer] :timeout_interval number of seconds a task can
172
171
  # run before it is considered to have failed (default: TIMEOUT_INTERVAL)
173
172
  # @option opts [Boolean] :run_now Whether to run the task immediately
174
- # upon instanciation or to wait until the first #execution_interval
173
+ # upon instantiation or to wait until the first #execution_interval
175
174
  # has passed (default: false)
176
175
  #
177
176
  # @raise ArgumentError when no block is given.
@@ -193,9 +192,10 @@ module Concurrent
193
192
 
194
193
  self.execution_interval = opts[:execution] || opts[:execution_interval] || EXECUTION_INTERVAL
195
194
  self.timeout_interval = opts[:timeout] || opts[:timeout_interval] || TIMEOUT_INTERVAL
196
- @run_now = opts[:now] || opts[:run_now] || false
195
+ @run_now = opts[:now] || opts[:run_now]
197
196
 
198
197
  @task = block
198
+ @observers = CopyOnWriteObserverSet.new
199
199
  init_mutex
200
200
  set_deref_options(opts)
201
201
  end
@@ -226,6 +226,10 @@ module Concurrent
226
226
  @timeout_interval = value
227
227
  end
228
228
 
229
+ def add_observer(observer, func = :update)
230
+ @observers.add_observer(observer, func)
231
+ end
232
+
229
233
  # Terminate with extreme prejudice. Useful in cases where +#stop+ doesn't
230
234
  # work because one of the threads becomes unresponsive.
231
235
  #
@@ -278,11 +282,10 @@ module Concurrent
278
282
  end
279
283
  raise TimeoutError if @worker.join(@timeout_interval).nil?
280
284
  mutex.synchronize { @value = @worker[:result] }
281
- rescue Exception => ex
282
- # suppress
285
+ rescue Exception => e
286
+ ex = e
283
287
  ensure
284
- changed
285
- notify_observers(Time.now, self.value, ex)
288
+ @observers.notify_observers(Time.now, self.value, ex)
286
289
  unless @worker.nil?
287
290
  Thread.kill(@worker)
288
291
  @worker = nil
@@ -0,0 +1,212 @@
1
+ require 'set'
2
+
3
+ require 'concurrent/thread_local_var'
4
+
5
+ module Concurrent
6
+
7
+ ABORTED = Object.new
8
+
9
+ CURRENT_TRANSACTION = ThreadLocalVar.new(nil)
10
+
11
+ ReadLogEntry = Struct.new(:tvar, :version)
12
+ UndoLogEntry = Struct.new(:tvar, :value)
13
+
14
+ class TVar
15
+
16
+ def initialize(value)
17
+ @value = value
18
+ @version = 0
19
+ @lock = Mutex.new
20
+ end
21
+
22
+ def value
23
+ Concurrent::atomically do
24
+ Transaction::current.read(self)
25
+ end
26
+ end
27
+
28
+ def value=(value)
29
+ Concurrent::atomically do
30
+ Transaction::current.write(self, value)
31
+ end
32
+ end
33
+
34
+ def unsafe_value
35
+ @value
36
+ end
37
+
38
+ def unsafe_value=(value)
39
+ @value = value
40
+ end
41
+
42
+ def unsafe_version
43
+ @version
44
+ end
45
+
46
+ def unsafe_increment_version
47
+ @version += 1
48
+ end
49
+
50
+ def unsafe_lock
51
+ @lock
52
+ end
53
+
54
+ end
55
+
56
+ class Transaction
57
+
58
+ def initialize
59
+ @write_set = Set.new
60
+ @read_log = []
61
+ @undo_log = []
62
+ end
63
+
64
+ def read(tvar)
65
+ Concurrent::abort_transaction unless valid?
66
+ @read_log.push(ReadLogEntry.new(tvar, tvar.unsafe_version))
67
+ tvar.unsafe_value
68
+ end
69
+
70
+ def write(tvar, value)
71
+ # Have we already written to this TVar?
72
+
73
+ unless @write_set.include? tvar
74
+ # Try to lock the TVar
75
+
76
+ unless tvar.unsafe_lock.try_lock
77
+ # Someone else is writing to this TVar - abort
78
+ Concurrent::abort_transaction
79
+ end
80
+
81
+ # We've locked it - add it to the write set
82
+
83
+ @write_set.add(tvar)
84
+
85
+ # If we previously wrote to it, check the version hasn't changed
86
+
87
+ @read_log.each do |log_entry|
88
+ if log_entry.tvar == tvar and tvar.unsafe_version > log_entry.version
89
+ Concurrent::abort_transaction
90
+ end
91
+ end
92
+ end
93
+
94
+ # Record the current value of the TVar so we can undo it later
95
+
96
+ @undo_log.push(UndoLogEntry.new(tvar, tvar.unsafe_value))
97
+
98
+ # Write the new value to the TVar
99
+
100
+ tvar.unsafe_value = value
101
+ end
102
+
103
+ def abort
104
+ @undo_log.each do |entry|
105
+ entry.tvar.unsafe_value = entry.value
106
+ end
107
+
108
+ unlock
109
+ end
110
+
111
+ def commit
112
+ return false unless valid?
113
+
114
+ @write_set.each do |tvar|
115
+ tvar.unsafe_increment_version
116
+ end
117
+
118
+ unlock
119
+
120
+ true
121
+ end
122
+
123
+ def valid?
124
+ @read_log.each do |log_entry|
125
+ unless @write_set.include? log_entry.tvar
126
+ if log_entry.tvar.unsafe_version > log_entry.version
127
+ return false
128
+ end
129
+ end
130
+ end
131
+
132
+ true
133
+ end
134
+
135
+ def unlock
136
+ @write_set.each do |tvar|
137
+ tvar.unsafe_lock.unlock
138
+ end
139
+ end
140
+
141
+ def self.current
142
+ CURRENT_TRANSACTION.value
143
+ end
144
+
145
+ def self.current=(transaction)
146
+ CURRENT_TRANSACTION.value = transaction
147
+ end
148
+
149
+ end
150
+
151
+ AbortError = Class.new(StandardError)
152
+
153
+ def atomically
154
+ raise ArgumentError.new('no block given') unless block_given?
155
+
156
+ # Get the current transaction
157
+
158
+ transaction = Transaction::current
159
+
160
+ # Are we not already in a transaction (not nested)?
161
+
162
+ if transaction.nil?
163
+ # New transaction
164
+
165
+ begin
166
+ # Retry loop
167
+
168
+ loop do
169
+
170
+ # Create a new transaction
171
+
172
+ transaction = Transaction.new
173
+ Transaction::current = transaction
174
+
175
+ # Run the block, aborting on exceptions
176
+
177
+ begin
178
+ result = yield
179
+ rescue AbortError => e
180
+ transaction.abort
181
+ result = ABORTED
182
+ rescue => e
183
+ transaction.abort
184
+ throw e
185
+ end
186
+ # If we can commit, break out of the loop
187
+
188
+ if result != ABORTED
189
+ if transaction.commit
190
+ break result
191
+ end
192
+ end
193
+ end
194
+ ensure
195
+ # Clear the current transaction
196
+
197
+ Transaction::current = nil
198
+ end
199
+ else
200
+ # Nested transaction - flatten it and just run the block
201
+
202
+ yield
203
+ end
204
+ end
205
+
206
+ def abort_transaction
207
+ raise AbortError.new
208
+ end
209
+
210
+ module_function :atomically, :abort_transaction
211
+
212
+ end