petra_core 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +3 -0
- data/.rubocop.yml +83 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +74 -0
- data/MIT-LICENSE +20 -0
- data/README.md +726 -0
- data/Rakefile +8 -0
- data/bin/console +8 -0
- data/bin/setup +8 -0
- data/examples/continuation_error.rb +125 -0
- data/examples/dining_philosophers.rb +138 -0
- data/examples/showcase.rb +54 -0
- data/lib/petra/components/entries/attribute_change.rb +29 -0
- data/lib/petra/components/entries/attribute_change_veto.rb +37 -0
- data/lib/petra/components/entries/attribute_read.rb +20 -0
- data/lib/petra/components/entries/object_destruction.rb +22 -0
- data/lib/petra/components/entries/object_initialization.rb +19 -0
- data/lib/petra/components/entries/object_persistence.rb +26 -0
- data/lib/petra/components/entries/read_integrity_override.rb +42 -0
- data/lib/petra/components/entry_set.rb +87 -0
- data/lib/petra/components/log_entry.rb +342 -0
- data/lib/petra/components/proxy_cache.rb +209 -0
- data/lib/petra/components/section.rb +543 -0
- data/lib/petra/components/transaction.rb +405 -0
- data/lib/petra/components/transaction_manager.rb +214 -0
- data/lib/petra/configuration/base.rb +132 -0
- data/lib/petra/configuration/class_configurator.rb +309 -0
- data/lib/petra/configuration/configurator.rb +67 -0
- data/lib/petra/core_ext.rb +27 -0
- data/lib/petra/exceptions.rb +181 -0
- data/lib/petra/persistence_adapters/adapter.rb +154 -0
- data/lib/petra/persistence_adapters/file_adapter.rb +239 -0
- data/lib/petra/proxies/abstract_proxy.rb +149 -0
- data/lib/petra/proxies/enumerable_proxy.rb +44 -0
- data/lib/petra/proxies/handlers/attribute_read_handler.rb +45 -0
- data/lib/petra/proxies/handlers/missing_method_handler.rb +47 -0
- data/lib/petra/proxies/method_handlers.rb +213 -0
- data/lib/petra/proxies/module_proxy.rb +12 -0
- data/lib/petra/proxies/object_proxy.rb +310 -0
- data/lib/petra/util/debug.rb +45 -0
- data/lib/petra/util/extended_attribute_accessors.rb +51 -0
- data/lib/petra/util/field_accessors.rb +35 -0
- data/lib/petra/util/registrable.rb +48 -0
- data/lib/petra/util/test_helpers.rb +9 -0
- data/lib/petra/version.rb +5 -0
- data/lib/petra.rb +100 -0
- data/lib/tasks/petra_tasks.rake +5 -0
- data/petra.gemspec +36 -0
- metadata +208 -0
@@ -0,0 +1,405 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'petra/components/entry_set'
|
4
|
+
require 'petra/components/section'
|
5
|
+
require 'petra/components/proxy_cache'
|
6
|
+
require 'continuation'
|
7
|
+
|
8
|
+
module Petra
|
9
|
+
module Components
|
10
|
+
class Transaction
|
11
|
+
include ActiveSupport::Callbacks
|
12
|
+
|
13
|
+
attr_reader :identifier
|
14
|
+
attr_reader :persisted
|
15
|
+
attr_reader :committed
|
16
|
+
attr_reader :reset
|
17
|
+
attr_reader :retry_in_progress
|
18
|
+
|
19
|
+
alias persisted? persisted
|
20
|
+
alias committed? committed
|
21
|
+
alias reset? reset
|
22
|
+
alias retry_in_progress? retry_in_progress
|
23
|
+
|
24
|
+
delegate :log_attribute_change,
|
25
|
+
:log_object_persistence,
|
26
|
+
:log_attribute_read,
|
27
|
+
:log_object_initialization,
|
28
|
+
:log_object_destruction, to: :current_section
|
29
|
+
|
30
|
+
def initialize(identifier:)
|
31
|
+
@identifier = identifier
|
32
|
+
@persisted = false
|
33
|
+
@committed = false
|
34
|
+
@reset = false
|
35
|
+
@retry_in_progress = false
|
36
|
+
end
|
37
|
+
|
38
|
+
def after_initialize
|
39
|
+
# Initialize the current section
|
40
|
+
current_section
|
41
|
+
end
|
42
|
+
|
43
|
+
#----------------------------------------------------------------
|
44
|
+
# Callbacks
|
45
|
+
#----------------------------------------------------------------
|
46
|
+
|
47
|
+
define_callbacks :commit, :rollback, :reset
|
48
|
+
define_callbacks :persist
|
49
|
+
|
50
|
+
#----------------------------------------------------------------
|
51
|
+
# Log Entries
|
52
|
+
#----------------------------------------------------------------
|
53
|
+
|
54
|
+
#
|
55
|
+
# Returns the latest value which was set for a certain object attribute.
|
56
|
+
# This means that all previous sections' write sets are inspected from new to old.
|
57
|
+
#
|
58
|
+
# @see Petra::Components::Section#value_for for more information
|
59
|
+
#
|
60
|
+
def attribute_value(proxy, attribute:)
|
61
|
+
sections.reverse.find { |s| s.value_for?(proxy, attribute: attribute) }.value_for(proxy, attribute: attribute)
|
62
|
+
end
|
63
|
+
|
64
|
+
#
|
65
|
+
# Checks whether the given attribute has been changed during the transaction.
|
66
|
+
# It basically searches for a matching write set entry in all previous (and current) sections.
|
67
|
+
# If such an entry exists AND there hasn't been an attribute change veto which is newer than it,
|
68
|
+
# the attribute counts as "changed within the transaction".
|
69
|
+
#
|
70
|
+
# @return [Boolean] +true+ if there there was a valid attribute change
|
71
|
+
#
|
72
|
+
def attribute_value?(proxy, attribute:)
|
73
|
+
sections.reverse.any? { |s| s.value_for?(proxy, attribute: attribute) } &&
|
74
|
+
!attribute_change_veto?(proxy, attribute: attribute)
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# @return [Boolean] +true+ if the given attribute was read in one of the previous (or the current) sections
|
79
|
+
#
|
80
|
+
def read_attribute_value?(proxy, attribute:)
|
81
|
+
sections.reverse.any? { |s| s.read_value_for?(proxy, attribute: attribute) }
|
82
|
+
end
|
83
|
+
|
84
|
+
#
|
85
|
+
# @return [Object] the last read value for the given attribute
|
86
|
+
#
|
87
|
+
def read_attribute_value(proxy, attribute:)
|
88
|
+
sections
|
89
|
+
.reverse.find { |s| s.read_value_for?(proxy, attribute: attribute) }
|
90
|
+
.read_value_for(proxy, attribute: attribute)
|
91
|
+
end
|
92
|
+
|
93
|
+
alias attribute_changed? attribute_value?
|
94
|
+
alias attribute_read? read_attribute_value?
|
95
|
+
|
96
|
+
#
|
97
|
+
# @return [Petra::Components::EntrySet] the combined log entries of all sections from old to new
|
98
|
+
#
|
99
|
+
# TODO: Cache entries from already persisted sections.
|
100
|
+
#
|
101
|
+
def log_entries
|
102
|
+
sections.each_with_object(EntrySet.new) { |s, es| es.concat(s.log_entries) }
|
103
|
+
end
|
104
|
+
|
105
|
+
#
|
106
|
+
# @param [Petra::Proxies::ObjectProxy] proxy
|
107
|
+
#
|
108
|
+
# @param [String, Symbol] attribute
|
109
|
+
#
|
110
|
+
# @param [Object] external_value
|
111
|
+
# The current external value. It is needed as read integrity overrides
|
112
|
+
# only stay active as long as the external value stays the same.
|
113
|
+
#
|
114
|
+
# @return [Boolean] +true+ if ReadIntegrityErrors should still be suppressed for
|
115
|
+
# the given attribute. This is the case if a ReadIntegrityOverride log entry is still
|
116
|
+
# active
|
117
|
+
#
|
118
|
+
def read_integrity_override?(proxy, attribute:, external_value:)
|
119
|
+
# Step 1: Search for the latest read integrity override entry we have for the given attribute
|
120
|
+
attribute_entries = log_entries.for_attribute_key(proxy.__attribute_key(attribute))
|
121
|
+
rio_entry = attribute_entries.of_kind(:read_integrity_override).latest
|
122
|
+
|
123
|
+
# If there was no override in the past sections, there can't be an active one
|
124
|
+
return false unless rio_entry
|
125
|
+
|
126
|
+
# Step 2: Find the read log entry we previously created for this attribute.
|
127
|
+
# There has to be one as otherwise no read integrity error could have happened.
|
128
|
+
read_entry = attribute_entries.of_kind(:attribute_read).latest
|
129
|
+
|
130
|
+
# Step 3: Test if the read entry is newer than the RIO entry.
|
131
|
+
# If that's the case, the user most likely decided that the new external
|
132
|
+
# value should be displayed inside the transaction.
|
133
|
+
# As we could have only landed here if the external value changed again,
|
134
|
+
# we probably have to re-raise an exception about that.
|
135
|
+
return false if read_entry > rio_entry
|
136
|
+
|
137
|
+
# Step 4: We found ourselves a RIO entry that has not yet been invalidated
|
138
|
+
# by another attribute read, good.
|
139
|
+
# Now we have to check whether the current external value is still
|
140
|
+
# the same as at the time we generated the RIO entry.
|
141
|
+
# If that's the case, we still have an active read integrity override.
|
142
|
+
rio_entry.external_value == external_value
|
143
|
+
end
|
144
|
+
|
145
|
+
#
|
146
|
+
# @param [Petra::Proxies::ObjectProxy] proxy
|
147
|
+
#
|
148
|
+
# @param [String, Symbol] attribute
|
149
|
+
#
|
150
|
+
# @return [Boolean] +true+ if there is an active AttributeChangeVeto
|
151
|
+
# for the given attribute, meaning that all attribute changes
|
152
|
+
# should be discarded.
|
153
|
+
#
|
154
|
+
# TODO: Combine with #read_integrity_override, because DRY
|
155
|
+
#
|
156
|
+
def attribute_change_veto?(proxy, attribute:)
|
157
|
+
# Step 1: Search for the latest attribute change veto entry we have for the given attribute
|
158
|
+
attribute_entries = log_entries.for_attribute_key(proxy.__attribute_key(attribute))
|
159
|
+
acv_entry = attribute_entries.of_kind(:attribute_change_veto).latest
|
160
|
+
|
161
|
+
# If there hasn't been an attribute change veto in the past, there can't be an active one
|
162
|
+
return false unless acv_entry
|
163
|
+
|
164
|
+
# Step 2: Find the latest attribute change entry we have for the given attribute
|
165
|
+
change_entry = attribute_entries.of_kind(:attribute_change).latest
|
166
|
+
|
167
|
+
# Step 3: Check if the change entry is newer than the ACV entry
|
168
|
+
# If so, the ACV entry is no longer valid
|
169
|
+
change_entry < acv_entry
|
170
|
+
end
|
171
|
+
|
172
|
+
#----------------------------------------------------------------
|
173
|
+
# Attribute Helpers
|
174
|
+
#----------------------------------------------------------------
|
175
|
+
|
176
|
+
#
|
177
|
+
# Checks whether the given attribute has been changed since we last read it.
|
178
|
+
# Raises an exception if the attribute was changed externally
|
179
|
+
#
|
180
|
+
# We cannot check here whether the attribute had several different values before
|
181
|
+
# going back to the original one, so we only compare the current and the last read value.
|
182
|
+
#
|
183
|
+
# @param [Boolean] force
|
184
|
+
# If set to +true+, the check is performed even if it was disabled in the
|
185
|
+
# base configuration.
|
186
|
+
#
|
187
|
+
# @raise [Petra::ReadIntegrityError] Raised if an attribute that we previously read,
|
188
|
+
# but NOT changed was changed externally
|
189
|
+
#
|
190
|
+
# @raise [Petra::ReadWriteIntegrityError] Raised if an attribute that we previously read AND
|
191
|
+
# changed was changed externally
|
192
|
+
#
|
193
|
+
def verify_attribute_integrity!(proxy, attribute:, force: false)
|
194
|
+
# If we didn't read the attribute before, we can't search for changes
|
195
|
+
return unless attribute_read?(proxy, attribute: attribute)
|
196
|
+
|
197
|
+
# Don't perform the check if the force flag is not set and
|
198
|
+
# petra is configured to not fail on read integrity errors at all.
|
199
|
+
return if !force && !Petra.configuration.instant_read_integrity_fail
|
200
|
+
|
201
|
+
# New objects won't be changed externally...
|
202
|
+
return if proxy.__new?
|
203
|
+
|
204
|
+
external_value = proxy.unproxied.send(attribute)
|
205
|
+
last_read_value = read_attribute_value(proxy, attribute: attribute)
|
206
|
+
|
207
|
+
# If nothing changed, we're done
|
208
|
+
return if external_value == last_read_value
|
209
|
+
|
210
|
+
# The user has previously chosen to ignore the external changes to this attribute (using ignore!).
|
211
|
+
# Therefore, we do not have to raise another exception
|
212
|
+
# OR
|
213
|
+
# We only read this attribute before.
|
214
|
+
# If the user (/developer) previously placed a read integrity override
|
215
|
+
# for the current external value, we don't have to re-raise an exception about the change
|
216
|
+
return if read_integrity_override?(proxy, attribute: attribute, external_value: external_value)
|
217
|
+
|
218
|
+
if attribute_changed?(proxy, attribute: attribute)
|
219
|
+
# We read AND changed this attribute before
|
220
|
+
|
221
|
+
# If there is already an active attribute change veto (meaning that we didn't change
|
222
|
+
# the attribute again after the last one), we don't have to raise another exception about it.
|
223
|
+
# TODO: This should have already been filtered out by #attribute_changed?
|
224
|
+
# return if attribute_change_veto?(proxy, attribute: attribute)
|
225
|
+
|
226
|
+
callcc do |continuation|
|
227
|
+
exception = Petra::WriteClashError.new(attribute: attribute,
|
228
|
+
object: proxy,
|
229
|
+
our_value: attribute_value(proxy, attribute: attribute),
|
230
|
+
external_value: external_value,
|
231
|
+
continuation: continuation)
|
232
|
+
|
233
|
+
fail exception, "The attribute `#{attribute}` has been changed externally and in the transaction."
|
234
|
+
end
|
235
|
+
else
|
236
|
+
callcc do |continuation|
|
237
|
+
exception = Petra::ReadIntegrityError.new(attribute: attribute,
|
238
|
+
object: proxy,
|
239
|
+
last_read_value: last_read_value,
|
240
|
+
external_value: external_value,
|
241
|
+
continuation: continuation)
|
242
|
+
fail exception, "The attribute `#{attribute}` has been changed externally."
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
#----------------------------------------------------------------
|
248
|
+
# Object Helpers
|
249
|
+
#----------------------------------------------------------------
|
250
|
+
|
251
|
+
def objects
|
252
|
+
@objects ||= ProxyCache.new(self)
|
253
|
+
end
|
254
|
+
|
255
|
+
#----------------------------------------------------------------
|
256
|
+
# Sections
|
257
|
+
#----------------------------------------------------------------
|
258
|
+
|
259
|
+
def current_section
|
260
|
+
@current_section ||= Petra::Components::Section.new(self).tap do |s|
|
261
|
+
sections << s
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def sections
|
266
|
+
# TODO: Acquire the transaction lock once here, otherwise, every section will do it.
|
267
|
+
@sections ||= begin
|
268
|
+
persistence_adapter.with_transaction_lock(self) do
|
269
|
+
persistence_adapter.savepoints(self).map do |savepoint|
|
270
|
+
Petra::Components::Section.new(self, savepoint: savepoint)
|
271
|
+
end.sort_by(&:savepoint_version)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
#----------------------------------------------------------------
|
277
|
+
# Transaction Handling
|
278
|
+
#----------------------------------------------------------------
|
279
|
+
|
280
|
+
#
|
281
|
+
# Tries to commit the current transaction
|
282
|
+
#
|
283
|
+
def commit!
|
284
|
+
run_callbacks :commit do
|
285
|
+
# Step 1: Lock this transaction so no other thread may alter it any more
|
286
|
+
persistence_adapter.with_transaction_lock(identifier) do
|
287
|
+
# Step 2: Try to get the locks for all objects which took part in this transaction
|
288
|
+
# Acquire the locks on a sorted collection to avoid Deadlocks with other transactions
|
289
|
+
# We do not have to lock objects which were created within the transaction
|
290
|
+
# as the cannot be altered outside of it and the transaction itself is locked.
|
291
|
+
with_locked_objects(objects.fateful.sort.reject(&:__new?), suspend: false) do
|
292
|
+
# Step 3: Now that we got locks on all objects used during this transaction,
|
293
|
+
# we can check whether all read attributes still have the same value.
|
294
|
+
# If that's not the case, we may not proceed.
|
295
|
+
objects.verify_read_attributes!(force: true)
|
296
|
+
|
297
|
+
# Step 4: Now that we know that all read values are still valid,
|
298
|
+
# we may actually apply all the changes we previously logged.
|
299
|
+
sections.each(&:apply_log_entries!)
|
300
|
+
|
301
|
+
@committed = true
|
302
|
+
Petra.logger.info "Committed transaction #{@identifier}", :blue, :underline
|
303
|
+
|
304
|
+
# Step 5: Wow, me made it this far!
|
305
|
+
# Now it's time to clean up and remove the data we previously persisted for this
|
306
|
+
# transaction before releasing the lock on all of the objects and the transaction itself.
|
307
|
+
# TODO: See if this causes problems with other threads working on this transactions. Probably keep
|
308
|
+
# the entries around and just mark the transaction as committed?
|
309
|
+
# Idea: keep it and add a last log entry like `transaction_commit` and persist it.
|
310
|
+
persistence_adapter.reset_transaction(self)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
rescue Petra::ReadIntegrityError # One (or more) of the attributes from our read set changed externally
|
314
|
+
raise
|
315
|
+
rescue Petra::LockError # One (or more) of the objects could not be locked.
|
316
|
+
# The object locks are freed by itself, but we have to notify
|
317
|
+
# the outer application about this commit error
|
318
|
+
raise
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
#
|
323
|
+
# Make sure that overrides (ReadIntegrityOverride / AttributeChangeVeto) are persisted
|
324
|
+
# before retrying a section. If we don't persist those, the same error will simply happen again
|
325
|
+
# in the next iteration.
|
326
|
+
#
|
327
|
+
def prepare_for_retry!
|
328
|
+
@retry_in_progress = true
|
329
|
+
current_section.prepare_for_retry!
|
330
|
+
persistence_adapter.persist!
|
331
|
+
@retry_in_progress = false
|
332
|
+
end
|
333
|
+
|
334
|
+
#
|
335
|
+
# Performs a rollback on this transaction, meaning that it will be set
|
336
|
+
# to the state of the latest savepoint.
|
337
|
+
# The current section will be reset, but keep the same savepoint name.
|
338
|
+
#
|
339
|
+
def rollback!
|
340
|
+
run_callbacks :rollback do
|
341
|
+
current_section.reset! unless current_section.persisted?
|
342
|
+
Petra.logger.debug "Rolled back section #{current_section.savepoint}", :yellow
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
#
|
347
|
+
# Persists the current transaction section using the configured persistence adapter
|
348
|
+
#
|
349
|
+
def persist!
|
350
|
+
run_callbacks :persist do
|
351
|
+
current_section.enqueue_for_persisting!
|
352
|
+
persistence_adapter.persist!
|
353
|
+
Petra.logger.debug "Persisted transaction #{@identifier}", :green
|
354
|
+
@persisted = true
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
#
|
359
|
+
# Completely dismisses the current transaction and removes it from the persistence storage
|
360
|
+
#
|
361
|
+
def reset!
|
362
|
+
run_callbacks :reset do
|
363
|
+
persistence_adapter.reset_transaction(self)
|
364
|
+
@sections = []
|
365
|
+
Petra.logger.warn "Reset transaction #{@identifier}", :red
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
private
|
370
|
+
|
371
|
+
#
|
372
|
+
# Tries to acquire locks on all of the given proxies and executes
|
373
|
+
# the given block afterwards.
|
374
|
+
#
|
375
|
+
# Please note that the objects may still be altered outside of transactions.
|
376
|
+
#
|
377
|
+
# This ensures that all object locks are released if an exception occurs
|
378
|
+
#
|
379
|
+
# @param [Array<Petra::Proxies::ObjectProxy>] proxies
|
380
|
+
#
|
381
|
+
# @raise [Petra::LockError] If +suspend+ is set to +false+, a LockError is raised
|
382
|
+
# if one of the object locks could not be acquired
|
383
|
+
#
|
384
|
+
# TODO: Many objects, many SystemStackErrors?
|
385
|
+
#
|
386
|
+
def with_locked_objects(proxies, suspend: true, &block)
|
387
|
+
if proxies.empty?
|
388
|
+
yield
|
389
|
+
else
|
390
|
+
persistence_adapter.with_object_lock(proxies.first, suspend: suspend) do
|
391
|
+
with_locked_objects(proxies[1..-1], suspend: suspend, &block)
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
#
|
397
|
+
# @return [Petra::PersistenceAdapters::Adapter] the current persistence adapter
|
398
|
+
#
|
399
|
+
def persistence_adapter
|
400
|
+
Petra.transaction_manager.persistence_adapter
|
401
|
+
end
|
402
|
+
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'petra/components/transaction'
|
4
|
+
|
5
|
+
module Petra
|
6
|
+
module Components
|
7
|
+
#
|
8
|
+
# A transaction manager handles the transactions in a single petra section,
|
9
|
+
# when speaking in terms of Rails, a new transaction manager is built every request.
|
10
|
+
#
|
11
|
+
# Each TransactionManager has a stack of transactions which are currently active and
|
12
|
+
# is currently stored within the current thread space. Once all active transactions
|
13
|
+
# are either persisted or otherwise finished, it is removed again to (more or less)
|
14
|
+
# ensure thread safety.
|
15
|
+
#
|
16
|
+
class TransactionManager
|
17
|
+
|
18
|
+
#
|
19
|
+
# Performs the given block on the current instance of TransactionManager.
|
20
|
+
# For nested transactions, the outer transaction manager is re-used,
|
21
|
+
# for outer transactions, a new manager is created.
|
22
|
+
#
|
23
|
+
# Once all running transactions either failed or were committed/persisted,
|
24
|
+
# the transaction manager instance is removed from the thread local space again.
|
25
|
+
#
|
26
|
+
# @todo: See if that is still a good practise when it comes to
|
27
|
+
# offering further actions through exception callbacks
|
28
|
+
#
|
29
|
+
def self.within_instance(&block)
|
30
|
+
instance = Thread.current[:__petra_transaction_manager] ||= TransactionManager.new
|
31
|
+
begin
|
32
|
+
instance.instance_eval(&block)
|
33
|
+
ensure
|
34
|
+
Thread.current[:__petra_transaction_manager] = nil if instance&.transaction_count&.zero?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# @return [Petra::Components::TransactionManager, NilClass] the currently active TransactionManager
|
40
|
+
# if there is at least one running transaction.
|
41
|
+
#
|
42
|
+
def self.instance
|
43
|
+
Thread.current[:__petra_transaction_manager] || fail(Petra::PetraError, 'There are no running transactions')
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# @return [Boolean] +true+ if at least one transaction is currently running.
|
48
|
+
#
|
49
|
+
# Please note that a transaction is only considered running if the process is currently
|
50
|
+
# in its execution block, even if it has uncommitted changes.
|
51
|
+
#
|
52
|
+
def self.instance?
|
53
|
+
!!Thread.current[:__petra_transaction_manager]
|
54
|
+
end
|
55
|
+
|
56
|
+
def initialize
|
57
|
+
@stack = []
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# Resets the currently innermost transaction.
|
62
|
+
# This means that everything this transaction has done so far will be
|
63
|
+
# discarded and the identifier freed again.
|
64
|
+
# TODO: Nested transactions again, what would happen?
|
65
|
+
#
|
66
|
+
def reset_transaction
|
67
|
+
@stack.last.reset!
|
68
|
+
fail Petra::AbortTransaction
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# Performs a rollback on the currently innermost transaction.
|
73
|
+
# This means that everything up until the transaction's latest
|
74
|
+
# savepoint will be discarded.
|
75
|
+
# TODO: Can we jump to a custom savepoint? What would happen if we were using the outer transaction's data?
|
76
|
+
#
|
77
|
+
def rollback_transaction
|
78
|
+
@stack.last.rollback!
|
79
|
+
fail Petra::AbortTransaction
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Commits the currently innermost transaction
|
84
|
+
#
|
85
|
+
def commit_transaction
|
86
|
+
@stack.last.commit!
|
87
|
+
fail Petra::AbortTransaction
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# Persists the currently innermost transaction, meaning that its actions will be
|
92
|
+
# written to storage using the chosen persistence adapter.
|
93
|
+
# This usually happens when a #with_transaction block ends and no commit flag
|
94
|
+
# was set using the corresponding exception class
|
95
|
+
#
|
96
|
+
def persist_transaction
|
97
|
+
@stack.last.persist!
|
98
|
+
@stack.pop
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# Wraps the given block in a petra transaction (section)
|
103
|
+
#
|
104
|
+
# @param [String] identifier
|
105
|
+
# The transaction's identifier. For continued transaction it has to be
|
106
|
+
# the same in each request, otherwise, a new transaction is started instead.
|
107
|
+
#
|
108
|
+
# @return [String] the transaction's identifier
|
109
|
+
#
|
110
|
+
def self.with_transaction(identifier: SecureRandom.uuid, &block)
|
111
|
+
within_instance do
|
112
|
+
Petra.logger.info "Starting transaction #{identifier}", :green
|
113
|
+
|
114
|
+
begin
|
115
|
+
transaction = begin_transaction(identifier)
|
116
|
+
yield
|
117
|
+
rescue Petra::Retry
|
118
|
+
Petra.logger.debug "Re-trying transaction #{identifier}", :blue
|
119
|
+
# We have to persist certain log entries before triggering a rollback
|
120
|
+
# as we'd lose read / write overrides otherwise
|
121
|
+
transaction.prepare_for_retry!
|
122
|
+
@stack.pop
|
123
|
+
retry
|
124
|
+
rescue Exception => error
|
125
|
+
handle_exception(error, transaction: transaction, &block)
|
126
|
+
ensure
|
127
|
+
# If we made it through the transaction section without raising
|
128
|
+
# any exception, we simply want to persist the performed transaction steps.
|
129
|
+
# If an exception happens during this persistence,
|
130
|
+
# a simple rollback is triggered as long as the transaction wasn't already persisted.
|
131
|
+
# TODO: See if this behaviour could cause trouble
|
132
|
+
unless error
|
133
|
+
begin
|
134
|
+
persist_transaction unless transaction.committed?
|
135
|
+
rescue Exception
|
136
|
+
transaction.rollback! unless transaction.persisted?
|
137
|
+
raise
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Remove the current transaction from the stack
|
142
|
+
@stack.pop
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
identifier
|
147
|
+
end
|
148
|
+
|
149
|
+
def persistence_adapter
|
150
|
+
@persistence_adapter ||= Petra.configuration.persistence_adapter.new
|
151
|
+
end
|
152
|
+
|
153
|
+
#
|
154
|
+
# @return [Fixnum] the number of currently active transactions
|
155
|
+
#
|
156
|
+
def transaction_count
|
157
|
+
@stack.size
|
158
|
+
end
|
159
|
+
|
160
|
+
def current_transaction
|
161
|
+
@stack.last
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
#
|
167
|
+
# Handles various exceptions which may occur during transaction execution
|
168
|
+
#
|
169
|
+
def handle_exception(error, transaction:)
|
170
|
+
case error
|
171
|
+
when Petra::Rollback
|
172
|
+
transaction.rollback!
|
173
|
+
when Petra::Reset
|
174
|
+
transaction.reset!
|
175
|
+
when Petra::ReadIntegrityError, Petra::WriteClashError
|
176
|
+
transaction.reset!
|
177
|
+
# TODO: Remove a possible continuation, we are outside of the transaction!
|
178
|
+
raise
|
179
|
+
when Petra::AbortTransaction
|
180
|
+
nil
|
181
|
+
# ActionView wraps errors inside an own error class. Therefore,
|
182
|
+
# we have to extract the actual exception first.
|
183
|
+
# TODO: Allow the registration of error handlers for certain exceptions to get rid of
|
184
|
+
# this very specific behaviour in petra core
|
185
|
+
# TODO: There is a mechanism in petra-rails' `petra_transaction` to extract
|
186
|
+
# the original exceptions. May we get rid of this now?
|
187
|
+
when ->(_) { Petra.rails? && error.is_a?(ActionView::Template::Error) }
|
188
|
+
handle_exception(error.original_exception, transaction: transaction)
|
189
|
+
else
|
190
|
+
# If another exception happened, we forward it to the actual application
|
191
|
+
transaction.reset!
|
192
|
+
raise
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
#
|
197
|
+
# Starts a new transaction and pushes it to the transaction stack.
|
198
|
+
# If one or more transactions are already running, a sub-transaction with a section
|
199
|
+
# savepoint name is started which can be rolled back individually
|
200
|
+
#
|
201
|
+
def begin_transaction(identifier)
|
202
|
+
Transaction.new(identifier: identifier).tap do |t|
|
203
|
+
@stack.push(t)
|
204
|
+
|
205
|
+
# It is important that the after_initialize method is called **after** the
|
206
|
+
# transaction was pushed to the transaction stack.
|
207
|
+
# Otherwise, +current_transaction+ might not be available for exception handling
|
208
|
+
# during the initialization phase.
|
209
|
+
t.after_initialize
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|