concurrent-ruby 0.5.0 → 0.6.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 (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