Class: Petra::Components::TransactionManager

Inherits:
Object
  • Object
show all
Defined in:
lib/petra/components/transaction_manager.rb

Overview

A transaction manager handles the transactions in a single petra section, when speaking in terms of Rails, a new transaction manager is built every request.

Each TransactionManager has a stack of transactions which are currently active and is currently stored within the current thread space. Once all active transactions are either persisted or otherwise finished, it is removed again to (more or less) ensure thread safety.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeTransactionManager

Returns a new instance of TransactionManager.



56
57
58
# File 'lib/petra/components/transaction_manager.rb', line 56

def initialize
  @stack = []
end

Class Method Details

.instancePetra::Components::TransactionManager, NilClass

Returns the currently active TransactionManager if there is at least one running transaction.

Returns:



42
43
44
# File 'lib/petra/components/transaction_manager.rb', line 42

def self.instance
  Thread.current[:__petra_transaction_manager] || fail(Petra::PetraError, 'There are no running transactions')
end

.instance?Boolean

Please note that a transaction is only considered running if the process is currently in its execution block, even if it has uncommitted changes.

Returns:

  • (Boolean)

    true if at least one transaction is currently running.



52
53
54
# File 'lib/petra/components/transaction_manager.rb', line 52

def self.instance?
  !!Thread.current[:__petra_transaction_manager]
end

.with_transaction(identifier: SecureRandom.uuid, &block) ⇒ String

Wraps the given block in a petra transaction (section)

Parameters:

  • identifier (String) (defaults to: SecureRandom.uuid)

    The transaction’s identifier. For continued transaction it has to be the same in each request, otherwise, a new transaction is started instead.

Returns:

  • (String)

    the transaction’s identifier



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/petra/components/transaction_manager.rb', line 110

def self.with_transaction(identifier: SecureRandom.uuid, &block)
  within_instance do
    Petra.logger.info "Starting transaction #{identifier}", :green

    begin
      transaction = begin_transaction(identifier)
      yield
    rescue Petra::Retry
      Petra.logger.debug "Re-trying transaction #{identifier}", :blue
      # We have to persist certain log entries before triggering a rollback
      # as we'd lose read / write overrides otherwise
      transaction.prepare_for_retry!
      @stack.pop
      retry
    rescue Exception => error
      handle_exception(error, transaction: transaction, &block)
    ensure
      # If we made it through the transaction section without raising
      # any exception, we simply want to persist the performed transaction steps.
      # If an exception happens during this persistence,
      # a simple rollback is triggered as long as the transaction wasn't already persisted.
      # TODO: See if this behaviour could cause trouble
      unless error
        begin
          persist_transaction unless transaction.committed?
        rescue Exception
          transaction.rollback! unless transaction.persisted?
          raise
        end
      end

      # Remove the current transaction from the stack
      @stack.pop
    end
  end

  identifier
end

.within_instance(&block) ⇒ Object

Performs the given block on the current instance of TransactionManager. For nested transactions, the outer transaction manager is re-used, for outer transactions, a new manager is created.

Once all running transactions either failed or were committed/persisted, the transaction manager instance is removed from the thread local space again.

@todo: See if that is still a good practise when it comes to

offering further actions through exception callbacks


29
30
31
32
33
34
35
36
# File 'lib/petra/components/transaction_manager.rb', line 29

def self.within_instance(&block)
  instance = Thread.current[:__petra_transaction_manager] ||= TransactionManager.new
  begin
    instance.instance_eval(&block)
  ensure
    Thread.current[:__petra_transaction_manager] = nil if instance&.transaction_count&.zero?
  end
end

Instance Method Details

#commit_transactionObject

Commits the currently innermost transaction



85
86
87
88
# File 'lib/petra/components/transaction_manager.rb', line 85

def commit_transaction
  @stack.last.commit!
  fail Petra::AbortTransaction
end

#current_transactionObject



160
161
162
# File 'lib/petra/components/transaction_manager.rb', line 160

def current_transaction
  @stack.last
end

#persist_transactionObject

Persists the currently innermost transaction, meaning that its actions will be written to storage using the chosen persistence adapter. This usually happens when a #with_transaction block ends and no commit flag was set using the corresponding exception class



96
97
98
99
# File 'lib/petra/components/transaction_manager.rb', line 96

def persist_transaction
  @stack.last.persist!
  @stack.pop
end

#persistence_adapterObject



149
150
151
# File 'lib/petra/components/transaction_manager.rb', line 149

def persistence_adapter
  @persistence_adapter ||= Petra.configuration.persistence_adapter.new
end

#reset_transactionObject

Resets the currently innermost transaction. This means that everything this transaction has done so far will be discarded and the identifier freed again. TODO: Nested transactions again, what would happen?



66
67
68
69
# File 'lib/petra/components/transaction_manager.rb', line 66

def reset_transaction
  @stack.last.reset!
  fail Petra::AbortTransaction
end

#rollback_transactionObject

Performs a rollback on the currently innermost transaction. This means that everything up until the transaction’s latest savepoint will be discarded. TODO: Can we jump to a custom savepoint? What would happen if we were using the outer transaction’s data?



77
78
79
80
# File 'lib/petra/components/transaction_manager.rb', line 77

def rollback_transaction
  @stack.last.rollback!
  fail Petra::AbortTransaction
end

#transaction_countFixnum

Returns the number of currently active transactions.

Returns:

  • (Fixnum)

    the number of currently active transactions



156
157
158
# File 'lib/petra/components/transaction_manager.rb', line 156

def transaction_count
  @stack.size
end