|
4 | 4 |
|
5 | 5 | module Concurrent
|
6 | 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 |
| - |
| 7 | + # A `TVar` is a transactional variable - a single-element container that |
| 8 | + # is used as part of a transaction - see `Concurrent::atomically`. |
14 | 9 | class TVar
|
15 | 10 |
|
| 11 | + # Create a new `TVar` with an initial value. |
16 | 12 | def initialize(value)
|
17 | 13 | @value = value
|
18 | 14 | @version = 0
|
19 | 15 | @lock = Mutex.new
|
20 | 16 | end
|
21 | 17 |
|
| 18 | + # Get the value of a `TVar`. |
22 | 19 | def value
|
23 | 20 | Concurrent::atomically do
|
24 | 21 | Transaction::current.read(self)
|
25 | 22 | end
|
26 | 23 | end
|
27 | 24 |
|
| 25 | + # Set the value of a `TVar`. |
28 | 26 | def value=(value)
|
29 | 27 | Concurrent::atomically do
|
30 | 28 | Transaction::current.write(self, value)
|
31 | 29 | end
|
32 | 30 | end
|
33 | 31 |
|
34 |
| - def unsafe_value |
| 32 | + # @!visibility private |
| 33 | + def unsafe_value # :nodoc: |
35 | 34 | @value
|
36 | 35 | end
|
37 | 36 |
|
38 |
| - def unsafe_value=(value) |
| 37 | + # @!visibility private |
| 38 | + def unsafe_value=(value) # :nodoc: |
39 | 39 | @value = value
|
40 | 40 | end
|
41 | 41 |
|
42 |
| - def unsafe_version |
| 42 | + # @!visibility private |
| 43 | + def unsafe_version # :nodoc: |
43 | 44 | @version
|
44 | 45 | end
|
45 | 46 |
|
46 |
| - def unsafe_increment_version |
| 47 | + # @!visibility private |
| 48 | + def unsafe_increment_version # :nodoc: |
47 | 49 | @version += 1
|
48 | 50 | end
|
49 | 51 |
|
50 |
| - def unsafe_lock |
| 52 | + # @!visibility private |
| 53 | + def unsafe_lock # :nodoc: |
51 | 54 | @lock
|
52 | 55 | end
|
53 | 56 |
|
54 | 57 | end
|
55 | 58 |
|
| 59 | + # Run a block that reads and writes `TVar`s as a single atomic transaction. |
| 60 | + # With respect to the value of `TVar` objects, the transaction is atomic, |
| 61 | + # in that it either happens or it does not, consistent, in that the `TVar` |
| 62 | + # objects involved will never enter an illegal state, and isolated, in that |
| 63 | + # transactions never interfere with each other. You may recognise these |
| 64 | + # properties from database transactions. |
| 65 | + # |
| 66 | + # There are some very important and unusual semantics that you must be aware of: |
| 67 | + # |
| 68 | + # * Most importantly, the block that you pass to atomically may be executed more than once. In most cases your code should be free of side-effects, except for via TVar. |
| 69 | + # |
| 70 | + # * If an exception escapes an atomically block it will abort the transaction. |
| 71 | + # |
| 72 | + # * It is undefined behaviour to use callcc or Fiber with atomically. |
| 73 | + # |
| 74 | + # * If you create a new thread within an atomically, it will not be part of the transaction. Creating a thread counts as a side-effect. |
| 75 | + # |
| 76 | + # Transactions within transactions are flattened to a single transaction. |
| 77 | + # |
| 78 | + # @example |
| 79 | + # a = new TVar(100_000) |
| 80 | + # b = new TVar(100) |
| 81 | + # |
| 82 | + # Concurrent::atomically do |
| 83 | + # a.value -= 10 |
| 84 | + # b.value += 10 |
| 85 | + # end |
| 86 | + def atomically |
| 87 | + raise ArgumentError.new('no block given') unless block_given? |
| 88 | + |
| 89 | + # Get the current transaction |
| 90 | + |
| 91 | + transaction = Transaction::current |
| 92 | + |
| 93 | + # Are we not already in a transaction (not nested)? |
| 94 | + |
| 95 | + if transaction.nil? |
| 96 | + # New transaction |
| 97 | + |
| 98 | + begin |
| 99 | + # Retry loop |
| 100 | + |
| 101 | + loop do |
| 102 | + |
| 103 | + # Create a new transaction |
| 104 | + |
| 105 | + transaction = Transaction.new |
| 106 | + Transaction::current = transaction |
| 107 | + |
| 108 | + # Run the block, aborting on exceptions |
| 109 | + |
| 110 | + begin |
| 111 | + result = yield |
| 112 | + rescue Transaction::AbortError => e |
| 113 | + transaction.abort |
| 114 | + result = Transaction::ABORTED |
| 115 | + rescue => e |
| 116 | + transaction.abort |
| 117 | + throw e |
| 118 | + end |
| 119 | + # If we can commit, break out of the loop |
| 120 | + |
| 121 | + if result != Transaction::ABORTED |
| 122 | + if transaction.commit |
| 123 | + break result |
| 124 | + end |
| 125 | + end |
| 126 | + end |
| 127 | + ensure |
| 128 | + # Clear the current transaction |
| 129 | + |
| 130 | + Transaction::current = nil |
| 131 | + end |
| 132 | + else |
| 133 | + # Nested transaction - flatten it and just run the block |
| 134 | + |
| 135 | + yield |
| 136 | + end |
| 137 | + end |
| 138 | + |
| 139 | + # Abort a currently running transaction - see `Concurrent::atomically`. |
| 140 | + def abort_transaction |
| 141 | + raise Transaction::AbortError.new |
| 142 | + end |
| 143 | + |
| 144 | + module_function :atomically, :abort_transaction |
| 145 | + |
| 146 | + private |
| 147 | + |
56 | 148 | class Transaction
|
57 | 149 |
|
| 150 | + ABORTED = Object.new |
| 151 | + |
| 152 | + CURRENT_TRANSACTION = ThreadLocalVar.new(nil) |
| 153 | + |
| 154 | + ReadLogEntry = Struct.new(:tvar, :version) |
| 155 | + UndoLogEntry = Struct.new(:tvar, :value) |
| 156 | + |
| 157 | + AbortError = Class.new(StandardError) |
| 158 | + |
58 | 159 | def initialize
|
59 | 160 | @write_set = Set.new
|
60 | 161 | @read_log = []
|
@@ -148,65 +249,4 @@ def self.current=(transaction)
|
148 | 249 |
|
149 | 250 | end
|
150 | 251 |
|
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 | 252 | end
|
0 commit comments