# :title: Transaction::Simple -- Active Object Transaction Support for Ruby
# :main: Transaction::Simple
#
# == Licence
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#--
# Transaction::Simple
# Simple object transaction support for Ruby
# Version 1.3.0
#
# Copyright (c) 2003 - 2005 Austin Ziegler
#
# $Id: simple.rb,v 1.5 2005/05/05 16:16:49 austin Exp $
#++
# The "Transaction" namespace can be used for additional transaction
# support objects and modules.
module Transaction
# A standard exception for transaction errors.
class TransactionError < StandardError; end
# The TransactionAborted exception is used to indicate when a
# transaction has been aborted in the block form.
class TransactionAborted < Exception; end
# The TransactionCommitted exception is used to indicate when a
# transaction has been committed in the block form.
class TransactionCommitted < Exception; end
te = "Transaction Error: %s"
Messages = {
:bad_debug_object =>
te % "the transaction debug object must respond to #<<.",
:unique_names =>
te % "named transactions must be unique.",
:no_transaction_open =>
te % "no transaction open.",
:cannot_rewind_no_transaction =>
te % "cannot rewind; there is no current transaction.",
:cannot_rewind_named_transaction =>
te % "cannot rewind to transaction %s because it does not exist.",
:cannot_rewind_transaction_before_block =>
te % "cannot rewind a transaction started before the execution block.",
:cannot_abort_no_transaction =>
te % "cannot abort; there is no current transaction.",
:cannot_abort_transaction_before_block =>
te % "cannot abort a transaction started before the execution block.",
:cannot_abort_named_transaction =>
te % "cannot abort nonexistant transaction %s.",
:cannot_commit_no_transaction =>
te % "cannot commit; there is no current transaction.",
:cannot_commit_transaction_before_block =>
te % "cannot commit a transaction started before the execution block.",
:cannot_commit_named_transaction =>
te % "cannot commit nonexistant transaction %s.",
:cannot_start_empty_block_transaction =>
te % "cannot start a block transaction with no objects.",
:cannot_obtain_transaction_lock =>
te % "cannot obtain transaction lock for #%s.",
}
# = Transaction::Simple for Ruby
# Simple object transaction support for Ruby
#
# == Introduction
# Transaction::Simple provides a generic way to add active transaction
# support to objects. The transaction methods added by this module will
# work with most objects, excluding those that cannot be
# Marshaled (bindings, procedure objects, IO instances, or
# singleton objects).
#
# The transactions supported by Transaction::Simple are not backed
# transactions; they are not associated with any sort of data store.
# They are "live" transactions occurring in memory and in the object
# itself. This is to allow "test" changes to be made to an object
# before making the changes permanent.
#
# Transaction::Simple can handle an "infinite" number of transaction
# levels (limited only by memory). If I open two transactions, commit
# the second, but abort the first, the object will revert to the
# original version.
#
# Transaction::Simple supports "named" transactions, so that multiple
# levels of transactions can be committed, aborted, or rewound by
# referring to the appropriate name of the transaction. Names may be any
# object *except* +nil+. As with Hash keys, String names will be
# duplicated and frozen before using.
#
# Copyright:: Copyright © 2003 - 2005 by Austin Ziegler
# Version:: 1.3.0
# Licence:: MIT-Style
#
# Thanks to David Black for help with the initial concept that led to
# this library.
#
# == Usage
# include 'transaction/simple'
#
# v = "Hello, you." # -> "Hello, you."
# v.extend(Transaction::Simple) # -> "Hello, you."
#
# v.start_transaction # -> ... (a Marshal string)
# v.transaction_open? # -> true
# v.gsub!(/you/, "world") # -> "Hello, world."
#
# v.rewind_transaction # -> "Hello, you."
# v.transaction_open? # -> true
#
# v.gsub!(/you/, "HAL") # -> "Hello, HAL."
# v.abort_transaction # -> "Hello, you."
# v.transaction_open? # -> false
#
# v.start_transaction # -> ... (a Marshal string)
# v.start_transaction # -> ... (a Marshal string)
#
# v.transaction_open? # -> true
# v.gsub!(/you/, "HAL") # -> "Hello, HAL."
#
# v.commit_transaction # -> "Hello, HAL."
# v.transaction_open? # -> true
# v.abort_transaction # -> "Hello, you."
# v.transaction_open? # -> false
#
# == Named Transaction Usage
# v = "Hello, you." # -> "Hello, you."
# v.extend(Transaction::Simple) # -> "Hello, you."
#
# v.start_transaction(:first) # -> ... (a Marshal string)
# v.transaction_open? # -> true
# v.transaction_open?(:first) # -> true
# v.transaction_open?(:second) # -> false
# v.gsub!(/you/, "world") # -> "Hello, world."
#
# v.start_transaction(:second) # -> ... (a Marshal string)
# v.gsub!(/world/, "HAL") # -> "Hello, HAL."
# v.rewind_transaction(:first) # -> "Hello, you."
# v.transaction_open? # -> true
# v.transaction_open?(:first) # -> true
# v.transaction_open?(:second) # -> false
#
# v.gsub!(/you/, "world") # -> "Hello, world."
# v.start_transaction(:second) # -> ... (a Marshal string)
# v.gsub!(/world/, "HAL") # -> "Hello, HAL."
# v.transaction_name # -> :second
# v.abort_transaction(:first) # -> "Hello, you."
# v.transaction_open? # -> false
#
# v.start_transaction(:first) # -> ... (a Marshal string)
# v.gsub!(/you/, "world") # -> "Hello, world."
# v.start_transaction(:second) # -> ... (a Marshal string)
# v.gsub!(/world/, "HAL") # -> "Hello, HAL."
#
# v.commit_transaction(:first) # -> "Hello, HAL."
# v.transaction_open? # -> false
#
# == Block Usage
# v = "Hello, you." # -> "Hello, you."
# Transaction::Simple.start(v) do |tv|
# # v has been extended with Transaction::Simple and an unnamed
# # transaction has been started.
# tv.transaction_open? # -> true
# tv.gsub!(/you/, "world") # -> "Hello, world."
#
# tv.rewind_transaction # -> "Hello, you."
# tv.transaction_open? # -> true
#
# tv.gsub!(/you/, "HAL") # -> "Hello, HAL."
# # The following breaks out of the transaction block after
# # aborting the transaction.
# tv.abort_transaction # -> "Hello, you."
# end
# # v still has Transaction::Simple applied from here on out.
# v.transaction_open? # -> false
#
# Transaction::Simple.start(v) do |tv|
# tv.start_transaction # -> ... (a Marshal string)
#
# tv.transaction_open? # -> true
# tv.gsub!(/you/, "HAL") # -> "Hello, HAL."
#
# # If #commit_transaction were called without having started a
# # second transaction, then it would break out of the transaction
# # block after committing the transaction.
# tv.commit_transaction # -> "Hello, HAL."
# tv.transaction_open? # -> true
# tv.abort_transaction # -> "Hello, you."
# end
# v.transaction_open? # -> false
#
# == Named Transaction Usage
# v = "Hello, you." # -> "Hello, you."
# v.extend(Transaction::Simple) # -> "Hello, you."
#
# v.start_transaction(:first) # -> ... (a Marshal string)
# v.transaction_open? # -> true
# v.transaction_open?(:first) # -> true
# v.transaction_open?(:second) # -> false
# v.gsub!(/you/, "world") # -> "Hello, world."
#
# v.start_transaction(:second) # -> ... (a Marshal string)
# v.gsub!(/world/, "HAL") # -> "Hello, HAL."
# v.rewind_transaction(:first) # -> "Hello, you."
# v.transaction_open? # -> true
# v.transaction_open?(:first) # -> true
# v.transaction_open?(:second) # -> false
#
# v.gsub!(/you/, "world") # -> "Hello, world."
# v.start_transaction(:second) # -> ... (a Marshal string)
# v.gsub!(/world/, "HAL") # -> "Hello, HAL."
# v.transaction_name # -> :second
# v.abort_transaction(:first) # -> "Hello, you."
# v.transaction_open? # -> false
#
# v.start_transaction(:first) # -> ... (a Marshal string)
# v.gsub!(/you/, "world") # -> "Hello, world."
# v.start_transaction(:second) # -> ... (a Marshal string)
# v.gsub!(/world/, "HAL") # -> "Hello, HAL."
#
# v.commit_transaction(:first) # -> "Hello, HAL."
# v.transaction_open? # -> false
#
# == Thread Safety
# Threadsafe version of Transaction::Simple and
# Transaction::Simple::Group exist; these are loaded from
# 'transaction/simple/threadsafe' and
# 'transaction/simple/threadsafe/group', respectively, and are
# represented in Ruby code as Transaction::Simple::ThreadSafe and
# Transaction::Simple::ThreadSafe::Group, respectively.
#
# == Contraindications
# While Transaction::Simple is very useful, it has some severe
# limitations that must be understood. Transaction::Simple:
#
# * uses Marshal. Thus, any object which cannot be Marshaled
# cannot use Transaction::Simple. In my experience, this affects
# singleton objects more often than any other object. It may be that
# Ruby 2.0 will solve this problem.
# * does not manage resources. Resources external to the object and its
# instance variables are not managed at all. However, all instance
# variables and objects "belonging" to those instance variables are
# managed. If there are object reference counts to be handled,
# Transaction::Simple will probably cause problems.
# * is not inherently thread-safe. In the ACID ("atomic, consistent,
# isolated, durable") test, Transaction::Simple provides CD, but it is
# up to the user of Transaction::Simple to provide isolation and
# atomicity. Transactions should be considered "critical sections" in
# multi-threaded applications. If thread safety and atomicity is
# absolutely required, use Transaction::Simple::ThreadSafe, which uses
# a Mutex object to synchronize the accesses on the object during the
# transaction operations.
# * does not necessarily maintain Object#__id__ values on rewind or
# abort. This may change for future versions that will be Ruby 1.8 or
# better *only*. Certain objects that support #replace will maintain
# Object#__id__.
# * Can be a memory hog if you use many levels of transactions on many
# objects.
#
module Simple
TRANSACTION_SIMPLE_VERSION = '1.3.0'
# Sets the Transaction::Simple debug object. It must respond to #<<.
# Sets the transaction debug object. Debugging will be performed
# automatically if there's a debug object. The generic transaction
# error class.
def self.debug_io=(io)
if io.nil?
@tdi = nil
@debugging = false
else
unless io.respond_to?(:<<)
raise TransactionError, Messages[:bad_debug_object]
end
@tdi = io
@debugging = true
end
end
# Returns +true+ if we are debugging.
def self.debugging?
@debugging
end
# Returns the Transaction::Simple debug object. It must respond to
# #<<.
def self.debug_io
@tdi ||= ""
@tdi
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 that responds to +name+ open.
def transaction_open?(name = nil)
if name.nil?
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "Transaction " <<
"[#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n"
end
return (not @__transaction_checkpoint__.nil?)
else
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "Transaction(#{name.inspect}) " <<
"[#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n"
end
return ((not @__transaction_checkpoint__.nil?) and @__transaction_names__.include?(name))
end
end
# Returns the current name of the transaction. Transactions not
# explicitly named are named +nil+.
def transaction_name
if @__transaction_checkpoint__.nil?
raise TransactionError, Messages[:no_transaction_open]
end
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} " <<
"Transaction Name: #{@__transaction_names__[-1].inspect}\n"
end
if @__transaction_names__[-1].kind_of?(String)
@__transaction_names__[-1].dup
else
@__transaction_names__[-1]
end
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.
def start_transaction(name = nil)
@__transaction_level__ ||= 0
@__transaction_names__ ||= []
if name.nil?
@__transaction_names__ << nil
ss = "" if Transaction::Simple.debugging?
else
if @__transaction_names__.include?(name)
raise TransactionError, Messages[:unique_names]
end
name = name.dup.freeze if name.kind_of?(String)
@__transaction_names__ << name
ss = "(#{name.inspect})" if Transaction::Simple.debugging?
end
@__transaction_level__ += 1
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "#{'>' * @__transaction_level__} " <<
"Start Transaction#{ss}\n"
end
@__transaction_checkpoint__ = Marshal.dump(self)
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.
def rewind_transaction(name = nil)
if @__transaction_checkpoint__.nil?
raise TransactionError, Messages[:cannot_rewind_no_transaction]
end
# Check to see if we are trying to rewind a transaction that is
# outside of the current transaction block.
if @__transaction_block__ and name
nix = @__transaction_names__.index(name) + 1
if nix < @__transaction_block__
raise TransactionError, Messages[:cannot_rewind_transaction_before_block]
end
end
if name.nil?
__rewind_this_transaction
ss = "" if Transaction::Simple.debugging?
else
unless @__transaction_names__.include?(name)
raise TransactionError, Messages[:cannot_rewind_named_transaction] % name.inspect
end
ss = "(#{name})" if Transaction::Simple.debugging?
while @__transaction_names__[-1] != name
@__transaction_checkpoint__ = __rewind_this_transaction
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} " <<
"Rewind Transaction#{ss}\n"
end
@__transaction_level__ -= 1
@__transaction_names__.pop
end
__rewind_this_transaction
end
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} " <<
"Rewind Transaction#{ss}\n"
end
self
end
# Aborts the transaction. Resets 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.
#
# 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+.
def abort_transaction(name = nil)
if @__transaction_checkpoint__.nil?
raise TransactionError, Messages[:cannot_abort_no_transaction]
end
# Check to see if we are trying to abort a transaction that is
# outside of the current transaction block. Otherwise, raise
# TransactionAborted if they are the same.
if @__transaction_block__ and name
nix = @__transaction_names__.index(name) + 1
if nix < @__transaction_block__
raise TransactionError, Messages[:cannot_abort_transaction_before_block]
end
raise TransactionAborted if @__transaction_block__ == nix
end
raise TransactionAborted if @__transaction_block__ == @__transaction_level__
if name.nil?
__abort_transaction(name)
else
unless @__transaction_names__.include?(name)
raise TransactionError, Messages[:cannot_abort_named_transaction] % name.inspect
end
__abort_transaction(name) while @__transaction_names__.include?(name)
end
self
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.
def commit_transaction(name = nil)
if @__transaction_checkpoint__.nil?
raise TransactionError, Messages[:cannot_commit_no_transaction]
end
@__transaction_block__ ||= nil
# Check to see if we are trying to commit a transaction that is
# outside of the current transaction block. Otherwise, raise
# TransactionCommitted if they are the same.
if @__transaction_block__ and name
nix = @__transaction_names__.index(name) + 1
if nix < @__transaction_block__
raise TransactionError, Messages[:cannot_commit_transaction_before_block]
end
raise TransactionCommitted if @__transaction_block__ == nix
end
raise TransactionCommitted if @__transaction_block__ == @__transaction_level__
if name.nil?
ss = "" if Transaction::Simple.debugging?
__commit_transaction
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " <<
"Commit Transaction#{ss}\n"
end
else
unless @__transaction_names__.include?(name)
raise TransactionError, Messages[:cannot_commit_named_transaction] % name.inspect
end
ss = "(#{name})" if Transaction::Simple.debugging?
while @__transaction_names__[-1] != name
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " <<
"Commit Transaction#{ss}\n"
end
__commit_transaction
end
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " <<
"Commit Transaction#{ss}\n"
end
__commit_transaction
end
self
end
# Alternative method for calling the transaction methods. An optional
# name can be specified for named transaction support.
#
# #transaction(:start):: #start_transaction
# #transaction(:rewind):: #rewind_transaction
# #transaction(:abort):: #abort_transaction
# #transaction(:commit):: #commit_transaction
# #transaction(:name):: #transaction_name
# #transaction:: #transaction_open?
def transaction(action = nil, name = nil)
case action
when :start
start_transaction(name)
when :rewind
rewind_transaction(name)
when :abort
abort_transaction(name)
when :commit
commit_transaction(name)
when :name
transaction_name
when nil
transaction_open?(name)
end
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"
def transaction_exclusions
@transaction_exclusions ||= []
end
class << self
def __common_start(name, vars, &block)
if vars.empty?
raise TransactionError, Messages[:cannot_start_empty_block_transaction]
end
if block
begin
vlevel = {}
vars.each do |vv|
vv.extend(Transaction::Simple)
vv.start_transaction(name)
vlevel[vv.__id__] = vv.instance_variable_get(:@__transaction_level__)
vv.instance_variable_set(:@__transaction_block__, vlevel[vv.__id__])
end
yield(*vars)
rescue TransactionAborted
vars.each do |vv|
if name.nil? and vv.transaction_open?
loop do
tlevel = vv.instance_variable_get(:@__transaction_level__) || -1
vv.instance_variable_set(:@__transaction_block__, -1)
break if tlevel < vlevel[vv.__id__]
vv.abort_transaction if vv.transaction_open?
end
elsif vv.transaction_open?(name)
vv.instance_variable_set(:@__transaction_block__, -1)
vv.abort_transaction(name)
end
end
rescue TransactionCommitted
nil
ensure
vars.each do |vv|
if name.nil? and vv.transaction_open?
loop do
tlevel = vv.instance_variable_get(:@__transaction_level__) || -1
break if tlevel < vlevel[vv.__id__]
vv.instance_variable_set(:@__transaction_block__, -1)
vv.commit_transaction if vv.transaction_open?
end
elsif vv.transaction_open?(name)
vv.instance_variable_set(:@__transaction_block__, -1)
vv.commit_transaction(name)
end
end
end
else
vars.each do |vv|
vv.extend(Transaction::Simple)
vv.start_transaction(name)
end
end
end
private :__common_start
def start_named(name, *vars, &block)
__common_start(name, vars, &block)
end
def start(*vars, &block)
__common_start(nil, vars, &block)
end
end
def __abort_transaction(name = nil) #:nodoc:
@__transaction_checkpoint__ = __rewind_this_transaction
if name.nil?
ss = "" if Transaction::Simple.debugging?
else
ss = "(#{name.inspect})" if Transaction::Simple.debugging?
end
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " <<
"Abort Transaction#{ss}\n"
end
@__transaction_level__ -= 1
@__transaction_names__.pop
if @__transaction_level__ < 1
@__transaction_level__ = 0
@__transaction_names__ = []
end
end
TRANSACTION_CHECKPOINT = "@__transaction_checkpoint__" #:nodoc:
SKIP_TRANSACTION_VARS = [TRANSACTION_CHECKPOINT, "@__transaction_level__"] #:nodoc:
def __rewind_this_transaction #:nodoc:
rr = Marshal.restore(@__transaction_checkpoint__)
begin
self.replace(rr) if respond_to?(:replace)
rescue
nil
end
rr.instance_variables.each do |vv|
next if SKIP_TRANSACTION_VARS.include?(vv)
next if self.transaction_exclusions.include?(vv)
if respond_to?(:instance_variable_get)
instance_variable_set(vv, rr.instance_variable_get(vv))
else
instance_eval(%q|#{vv} = rr.instance_eval("#{vv}")|)
end
end
new_ivar = instance_variables - rr.instance_variables - SKIP_TRANSACTION_VARS
new_ivar.each do |vv|
if respond_to?(:instance_variable_set)
instance_variable_set(vv, nil)
else
instance_eval(%q|#{vv} = nil|)
end
end
if respond_to?(:instance_variable_get)
rr.instance_variable_get(TRANSACTION_CHECKPOINT)
else
rr.instance_eval(TRANSACTION_CHECKPOINT)
end
end
def __commit_transaction #:nodoc:
if respond_to?(:instance_variable_get)
@__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_variable_get(TRANSACTION_CHECKPOINT)
else
@__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_eval(TRANSACTION_CHECKPOINT)
end
@__transaction_level__ -= 1
@__transaction_names__.pop
if @__transaction_level__ < 1
@__transaction_level__ = 0
@__transaction_names__ = []
end
end
private :__abort_transaction
private :__rewind_this_transaction
private :__commit_transaction
end
end