petra_core 0.0.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 +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
|