Module | Transaction::Simple |
In: |
lib/transaction/simple.rb
|
Simple object transaction support for Ruby
TRANSACTION_SIMPLE_VERSION | = | '1.4.0' |
SKIP_TRANSACTION_VARS | = | %w(@__transaction_checkpoint__ @__transaction_level__) |
Returns the Transaction::Simple debug object. It must respond to #<<.
# File lib/transaction/simple.rb, line 75 75: def debug_io 76: @tdi ||= "" 77: @tdi 78: end
Sets the Transaction::Simple debug object. It must respond to #<<. Debugging will be performed automatically if there‘s a debug object.
# File lib/transaction/simple.rb, line 58 58: def debug_io=(io) 59: if io.nil? 60: @tdi = nil 61: @debugging = false 62: else 63: raise Transaction::TransactionError, Transaction::Messages[:bad_debug_object] unless io.respond_to?(:<<) 64: @tdi = io 65: @debugging = true 66: end 67: end
Returns true if we are debugging.
# File lib/transaction/simple.rb, line 70 70: def debugging? 71: defined? @debugging and @debugging 72: end
Start a named transaction in a block. The transaction will auto-commit when the block finishes.
# File lib/transaction/simple.rb, line 409 409: def start(*vars, &block) 410: __common_start(nil, vars, &block) 411: end
Start a named transaction in a block. The transaction will auto-commit when the block finishes.
# File lib/transaction/simple.rb, line 403 403: def start_named(name, *vars, &block) 404: __common_start(name, vars, &block) 405: end
# File lib/transaction/simple.rb, line 346 346: def __common_start(name, vars, &block) 347: raise Transaction::TransactionError, Transaction::Messages[:cannot_start_empty_block_transaction] if vars.empty? 348: 349: if block 350: begin 351: vlevel = {} 352: 353: vars.each do |vv| 354: vv.extend(Transaction::Simple) 355: vv.start_transaction(name) 356: vlevel[vv.__id__] = vv.instance_variable_get(:@__transaction_level__) 357: vv.instance_variable_set(:@__transaction_block__, vlevel[vv.__id__]) 358: end 359: 360: yield(*vars) 361: rescue Transaction::TransactionAborted 362: vars.each do |vv| 363: if name.nil? and vv.transaction_open? 364: loop do 365: tlevel = vv.instance_variable_get(:@__transaction_level__) || -1 366: vv.instance_variable_set(:@__transaction_block__, -1) 367: break if tlevel < vlevel[vv.__id__] 368: vv.abort_transaction if vv.transaction_open? 369: end 370: elsif vv.transaction_open?(name) 371: vv.instance_variable_set(:@__transaction_block__, -1) 372: vv.abort_transaction(name) 373: end 374: end 375: rescue Transaction::TransactionCommitted 376: nil 377: ensure 378: vars.each do |vv| 379: if name.nil? and vv.transaction_open? 380: loop do 381: tlevel = vv.instance_variable_get(:@__transaction_level__) || -1 382: break if tlevel < vlevel[vv.__id__] 383: vv.instance_variable_set(:@__transaction_block__, -1) 384: vv.commit_transaction if vv.transaction_open? 385: end 386: elsif vv.transaction_open?(name) 387: vv.instance_variable_set(:@__transaction_block__, -1) 388: vv.commit_transaction(name) 389: end 390: end 391: end 392: else 393: vars.each do |vv| 394: vv.extend(Transaction::Simple) 395: vv.start_transaction(name) 396: end 397: end 398: end
Aborts the transaction. Rewinds the object state to what it was before the transaction was started and closes the transaction. If name is specified, then the intervening transactions and the named transaction will be aborted. Otherwise, only the current transaction is aborted.
See rewind_transaction for information about dealing with complex self-referential object graphs.
If the current or named transaction has been started by a block (Transaction::Simple.start), then the execution of the block will be halted with break self.
# File lib/transaction/simple.rb, line 232 232: def abort_transaction(name = nil) 233: raise Transaction::TransactionError, Transaction::Messages[:cannot_abort_no_transaction] if @__transaction_checkpoint__.nil? 234: 235: # Check to see if we are trying to abort a transaction that is outside 236: # of the current transaction block. Otherwise, raise TransactionAborted 237: # if they are the same. 238: defined? @__transaction_block__ or @__transaction_block__ = nil 239: if @__transaction_block__ and name 240: nix = @__transaction_names__.index(name) + 1 241: raise Transaction::TransactionError, Transaction::Messages[:cannot_abort_transaction_before_block] if nix < @__transaction_block__ 242: 243: raise Transaction::TransactionAborted if @__transaction_block__ == nix 244: end 245: 246: raise Transaction::TransactionAborted if @__transaction_block__ == @__transaction_level__ 247: 248: if name.nil? 249: __abort_transaction(name) 250: else 251: raise Transaction::TransactionError, Transaction::Messages[:cannot_abort_named_transaction] % name.inspect unless @__transaction_names__.include?(name) 252: __abort_transaction(name) while @__transaction_names__.include?(name) 253: end 254: 255: self 256: end
If name is nil (default), the current transaction level is closed out and the changes are committed.
If name is specified and name is in the list of named transactions, then all transactions are closed and committed until the named transaction is reached.
# File lib/transaction/simple.rb, line 264 264: def commit_transaction(name = nil) 265: raise Transaction::TransactionError, Transaction::Messages[:cannot_commit_no_transaction] if @__transaction_checkpoint__.nil? 266: @__transaction_block__ ||= nil 267: 268: # Check to see if we are trying to commit a transaction that is outside 269: # of the current transaction block. Otherwise, raise 270: # TransactionCommitted if they are the same. 271: if @__transaction_block__ and name 272: nix = @__transaction_names__.index(name) + 1 273: raise Transaction::TransactionError, Transaction::Messages[:cannot_commit_transaction_before_block] if nix < @__transaction_block__ 274: 275: raise Transaction::TransactionCommitted if @__transaction_block__ == nix 276: end 277: 278: raise Transaction::TransactionCommitted if @__transaction_block__ == @__transaction_level__ 279: 280: if name.nil? 281: ss = "" if Transaction::Simple.debugging? 282: __commit_transaction 283: Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " << "Commit Transaction#{ss}\n" if Transaction::Simple.debugging? 284: else 285: raise Transaction::TransactionError, Transaction::Messages[:cannot_commit_named_transaction] % name.inspect unless @__transaction_names__.include?(name) 286: ss = "(#{name})" if Transaction::Simple.debugging? 287: 288: while @__transaction_names__[-1] != name 289: Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " << "Commit Transaction#{ss}\n" if Transaction::Simple.debugging? 290: __commit_transaction 291: end 292: Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " << "Commit Transaction#{ss}\n" if Transaction::Simple.debugging? 293: __commit_transaction 294: end 295: 296: self 297: end
Rewinds the transaction. If name is specified, then the intervening transactions will be aborted and the named transaction will be rewound. Otherwise, only the current transaction is rewound.
After each level of transaction is rewound, if the callback method _post_transaction_rewind is defined, it will be called. It is intended to allow a complex self-referential graph to fix itself. The simplest way to explain this is with an example.
class Child attr_accessor :parent end class Parent include Transaction::Simple attr_reader :children def initialize @children = [] end def << child child.parent = self @children << child end def valid? @children.all? { |child| child.parent == self } end end parent = Parent.new parent << Child.new parent.start_transaction parent << Child.new parent.abort_transaction puts parent.valid? # => false
This problem can be fixed by modifying the Parent class to include the _post_transaction_rewind callback.
class Parent # Reconnect the restored children to me, instead of to the bogus me # that was restored to them by Marshal::load. def _post_transaction_rewind @children.each { |child| child.parent = self } end end parent = Parent.new parent << Child.new parent.start_transaction parent << Child.new parent.abort_transaction puts parent.valid? # => true
# File lib/transaction/simple.rb, line 187 187: def rewind_transaction(name = nil) 188: raise Transaction::TransactionError, Transaction::Messages[:cannot_rewind_no_transaction] if @__transaction_checkpoint__.nil? 189: 190: # Check to see if we are trying to rewind a transaction that is 191: # outside of the current transaction block. 192: defined? @__transaction_block__ or @__transaction_block__ = nil 193: if @__transaction_block__ and name 194: nix = @__transaction_names__.index(name) + 1 195: raise Transaction::TransactionError, Transaction::Messages[:cannot_rewind_transaction_before_block] if nix < @__transaction_block__ 196: end 197: 198: if name.nil? 199: checkpoint = @__transaction_checkpoint__ 200: __rewind_this_transaction 201: @__transaction_checkpoint__ = checkpoint 202: ss = "" if Transaction::Simple.debugging? 203: else 204: raise Transaction::TransactionError, Transaction::Messages[:cannot_rewind_named_transaction] % name.inspect unless @__transaction_names__.include?(name) 205: ss = "(#{name})" if Transaction::Simple.debugging? 206: 207: while @__transaction_names__[-1] != name 208: @__transaction_checkpoint__ = __rewind_this_transaction 209: Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} " << "Rewind Transaction#{ss}\n" if Transaction::Simple.debugging? 210: @__transaction_level__ -= 1 211: @__transaction_names__.pop 212: end 213: checkpoint = @__transaction_checkpoint__ 214: __rewind_this_transaction 215: @__transaction_checkpoint__ = checkpoint 216: end 217: Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} " << "Rewind Transaction#{ss}\n" if Transaction::Simple.debugging? 218: self 219: end
Starts a transaction. Stores the current object state. If a transaction name is specified, the transaction will be named. Transaction names must be unique. Transaction names of nil will be treated as unnamed transactions.
# File lib/transaction/simple.rb, line 111 111: def start_transaction(name = nil) 112: @__transaction_level__ ||= 0 113: @__transaction_names__ ||= [] 114: 115: name = name.dup.freeze if name.kind_of?(String) 116: 117: raise Transaction::TransactionError, Transaction::Messages[:unique_names] if name and @__transaction_names__.include?(name) 118: 119: @__transaction_names__ << name 120: @__transaction_level__ += 1 121: 122: if Transaction::Simple.debugging? 123: ss = "(#{name.inspect})" 124: ss = "" unless ss 125: 126: Transaction::Simple.debug_io << "#{'>' * @__transaction_level__} " << "Start Transaction#{ss}\n" 127: end 128: 129: @__transaction_checkpoint__ = Marshal.dump(self) 130: end
Alternative method for calling the transaction methods. An optional name can be specified for named transaction support. This method is deprecated and will be removed in Transaction::Simple 2.0.
transaction(:start): | start_transaction |
transaction(:rewind): | rewind_transaction |
transaction(:abort): | abort_transaction |
transaction(:commit): | commit_transaction |
transaction(:name): | transaction_name |
transaction: | transaction_open? |
# File lib/transaction/simple.rb, line 309 309: def transaction(action = nil, name = nil) 310: _method = case action 311: when :start then :start_transaction 312: when :rewind then :rewind_transaction 313: when :abort then :abort_transaction 314: when :commit then :commit_transaction 315: when :name then :transaction_name 316: when nil then :transaction_open? 317: else nil 318: end 319: 320: if method 321: warn "The #transaction method has been deprecated. Use #{method} instead." 322: else 323: warn "The #transaction method has been deprecated." 324: end 325: 326: case method 327: when :transaction_name 328: __send__ method 329: when nil 330: nil 331: else 332: __send__ method, name 333: end 334: end
Allows specific variables to be excluded from transaction support. Must be done after extending the object but before starting the first transaction on the object.
vv.transaction_exclusions << "@io"
# File lib/transaction/simple.rb, line 341 341: def transaction_exclusions 342: @transaction_exclusions ||= [] 343: end
Returns the current name of the transaction. Transactions not explicitly named are named nil.
# File lib/transaction/simple.rb, line 97 97: def transaction_name 98: raise Transaction::TransactionError, Transaction::Messages[:no_transaction_open] if @__transaction_checkpoint__.nil? 99: Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} " << "Transaction Name: #{@__transaction_names__[-1].inspect}\n" if Transaction::Simple.debugging? 100: if @__transaction_names__[-1].kind_of?(String) 101: @__transaction_names__[-1].dup 102: else 103: @__transaction_names__[-1] 104: end 105: end
If name is nil (default), then returns true if there is currently a transaction open. If name is specified, then returns true if there is currently a transaction known as name open.
# File lib/transaction/simple.rb, line 84 84: def transaction_open?(name = nil) 85: defined? @__transaction_checkpoint__ or @__transaction_checkpoint__ = nil 86: if name.nil? 87: Transaction::Simple.debug_io << "Transaction " << "[#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n" if Transaction::Simple.debugging? 88: return (not @__transaction_checkpoint__.nil?) 89: else 90: Transaction::Simple.debug_io << "Transaction(#{name.inspect}) " << "[#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n" if Transaction::Simple.debugging? 91: return ((not @__transaction_checkpoint__.nil?) and @__transaction_names__.include?(name)) 92: end 93: end