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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +83 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +5 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +13 -0
  9. data/Gemfile.lock +74 -0
  10. data/MIT-LICENSE +20 -0
  11. data/README.md +726 -0
  12. data/Rakefile +8 -0
  13. data/bin/console +8 -0
  14. data/bin/setup +8 -0
  15. data/examples/continuation_error.rb +125 -0
  16. data/examples/dining_philosophers.rb +138 -0
  17. data/examples/showcase.rb +54 -0
  18. data/lib/petra/components/entries/attribute_change.rb +29 -0
  19. data/lib/petra/components/entries/attribute_change_veto.rb +37 -0
  20. data/lib/petra/components/entries/attribute_read.rb +20 -0
  21. data/lib/petra/components/entries/object_destruction.rb +22 -0
  22. data/lib/petra/components/entries/object_initialization.rb +19 -0
  23. data/lib/petra/components/entries/object_persistence.rb +26 -0
  24. data/lib/petra/components/entries/read_integrity_override.rb +42 -0
  25. data/lib/petra/components/entry_set.rb +87 -0
  26. data/lib/petra/components/log_entry.rb +342 -0
  27. data/lib/petra/components/proxy_cache.rb +209 -0
  28. data/lib/petra/components/section.rb +543 -0
  29. data/lib/petra/components/transaction.rb +405 -0
  30. data/lib/petra/components/transaction_manager.rb +214 -0
  31. data/lib/petra/configuration/base.rb +132 -0
  32. data/lib/petra/configuration/class_configurator.rb +309 -0
  33. data/lib/petra/configuration/configurator.rb +67 -0
  34. data/lib/petra/core_ext.rb +27 -0
  35. data/lib/petra/exceptions.rb +181 -0
  36. data/lib/petra/persistence_adapters/adapter.rb +154 -0
  37. data/lib/petra/persistence_adapters/file_adapter.rb +239 -0
  38. data/lib/petra/proxies/abstract_proxy.rb +149 -0
  39. data/lib/petra/proxies/enumerable_proxy.rb +44 -0
  40. data/lib/petra/proxies/handlers/attribute_read_handler.rb +45 -0
  41. data/lib/petra/proxies/handlers/missing_method_handler.rb +47 -0
  42. data/lib/petra/proxies/method_handlers.rb +213 -0
  43. data/lib/petra/proxies/module_proxy.rb +12 -0
  44. data/lib/petra/proxies/object_proxy.rb +310 -0
  45. data/lib/petra/util/debug.rb +45 -0
  46. data/lib/petra/util/extended_attribute_accessors.rb +51 -0
  47. data/lib/petra/util/field_accessors.rb +35 -0
  48. data/lib/petra/util/registrable.rb +48 -0
  49. data/lib/petra/util/test_helpers.rb +9 -0
  50. data/lib/petra/version.rb +5 -0
  51. data/lib/petra.rb +100 -0
  52. data/lib/tasks/petra_tasks.rake +5 -0
  53. data/petra.gemspec +36 -0
  54. 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