petra_core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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