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.
- 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
|