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.
- checksums.yaml +4 -4
- data/README.md +88 -77
- data/lib/concurrent.rb +17 -2
- data/lib/concurrent/actor.rb +17 -0
- data/lib/concurrent/actor_context.rb +31 -0
- data/lib/concurrent/actor_ref.rb +39 -0
- data/lib/concurrent/agent.rb +12 -3
- data/lib/concurrent/async.rb +290 -0
- data/lib/concurrent/atomic.rb +5 -9
- data/lib/concurrent/cached_thread_pool.rb +39 -137
- data/lib/concurrent/channel/blocking_ring_buffer.rb +60 -0
- data/lib/concurrent/channel/buffered_channel.rb +83 -0
- data/lib/concurrent/channel/channel.rb +11 -0
- data/lib/concurrent/channel/probe.rb +19 -0
- data/lib/concurrent/channel/ring_buffer.rb +54 -0
- data/lib/concurrent/channel/unbuffered_channel.rb +34 -0
- data/lib/concurrent/channel/waitable_list.rb +38 -0
- data/lib/concurrent/configuration.rb +92 -0
- data/lib/concurrent/dataflow.rb +9 -3
- data/lib/concurrent/delay.rb +88 -0
- data/lib/concurrent/exchanger.rb +31 -0
- data/lib/concurrent/fixed_thread_pool.rb +28 -122
- data/lib/concurrent/future.rb +10 -5
- data/lib/concurrent/immediate_executor.rb +3 -2
- data/lib/concurrent/ivar.rb +2 -1
- data/lib/concurrent/java_cached_thread_pool.rb +45 -0
- data/lib/concurrent/java_fixed_thread_pool.rb +37 -0
- data/lib/concurrent/java_thread_pool_executor.rb +194 -0
- data/lib/concurrent/per_thread_executor.rb +23 -0
- data/lib/concurrent/postable.rb +2 -0
- data/lib/concurrent/processor_count.rb +125 -0
- data/lib/concurrent/promise.rb +42 -18
- data/lib/concurrent/ruby_cached_thread_pool.rb +37 -0
- data/lib/concurrent/ruby_fixed_thread_pool.rb +31 -0
- data/lib/concurrent/ruby_thread_pool_executor.rb +268 -0
- data/lib/concurrent/ruby_thread_pool_worker.rb +69 -0
- data/lib/concurrent/simple_actor_ref.rb +124 -0
- data/lib/concurrent/thread_local_var.rb +1 -1
- data/lib/concurrent/thread_pool_executor.rb +30 -0
- data/lib/concurrent/timer_task.rb +13 -10
- data/lib/concurrent/tvar.rb +212 -0
- data/lib/concurrent/utilities.rb +1 -0
- data/lib/concurrent/version.rb +1 -1
- data/spec/concurrent/actor_context_spec.rb +37 -0
- data/spec/concurrent/actor_ref_shared.rb +313 -0
- data/spec/concurrent/actor_spec.rb +9 -1
- data/spec/concurrent/agent_spec.rb +97 -96
- data/spec/concurrent/async_spec.rb +320 -0
- data/spec/concurrent/cached_thread_pool_shared.rb +137 -0
- data/spec/concurrent/channel/blocking_ring_buffer_spec.rb +149 -0
- data/spec/concurrent/channel/buffered_channel_spec.rb +151 -0
- data/spec/concurrent/channel/channel_spec.rb +37 -0
- data/spec/concurrent/channel/probe_spec.rb +49 -0
- data/spec/concurrent/channel/ring_buffer_spec.rb +126 -0
- data/spec/concurrent/channel/unbuffered_channel_spec.rb +132 -0
- data/spec/concurrent/configuration_spec.rb +134 -0
- data/spec/concurrent/dataflow_spec.rb +109 -27
- data/spec/concurrent/delay_spec.rb +77 -0
- data/spec/concurrent/exchanger_spec.rb +66 -0
- data/spec/concurrent/fixed_thread_pool_shared.rb +136 -0
- data/spec/concurrent/future_spec.rb +60 -51
- data/spec/concurrent/global_thread_pool_shared.rb +33 -0
- data/spec/concurrent/immediate_executor_spec.rb +4 -25
- data/spec/concurrent/ivar_spec.rb +36 -23
- data/spec/concurrent/java_cached_thread_pool_spec.rb +64 -0
- data/spec/concurrent/java_fixed_thread_pool_spec.rb +64 -0
- data/spec/concurrent/java_thread_pool_executor_spec.rb +71 -0
- data/spec/concurrent/obligation_shared.rb +32 -20
- data/spec/concurrent/{global_thread_pool_spec.rb → per_thread_executor_spec.rb} +9 -13
- data/spec/concurrent/processor_count_spec.rb +20 -0
- data/spec/concurrent/promise_spec.rb +29 -41
- data/spec/concurrent/ruby_cached_thread_pool_spec.rb +69 -0
- data/spec/concurrent/ruby_fixed_thread_pool_spec.rb +39 -0
- data/spec/concurrent/ruby_thread_pool_executor_spec.rb +183 -0
- data/spec/concurrent/simple_actor_ref_spec.rb +219 -0
- data/spec/concurrent/thread_pool_class_cast_spec.rb +40 -0
- data/spec/concurrent/thread_pool_executor_shared.rb +155 -0
- data/spec/concurrent/thread_pool_shared.rb +98 -36
- data/spec/concurrent/tvar_spec.rb +137 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/support/functions.rb +4 -0
- metadata +85 -20
- data/lib/concurrent/cached_thread_pool/worker.rb +0 -91
- data/lib/concurrent/channel.rb +0 -63
- data/lib/concurrent/fixed_thread_pool/worker.rb +0 -54
- data/lib/concurrent/global_thread_pool.rb +0 -42
- data/spec/concurrent/cached_thread_pool_spec.rb +0 -101
- data/spec/concurrent/channel_spec.rb +0 -86
- data/spec/concurrent/fixed_thread_pool_spec.rb +0 -92
- 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
|
@@ -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
|
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
|
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
|
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
|
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]
|
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 =>
|
282
|
-
|
285
|
+
rescue Exception => e
|
286
|
+
ex = e
|
283
287
|
ensure
|
284
|
-
|
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
|