Skip to content

Instantly share code, notes, and snippets.

@ccooke
Created May 19, 2010 20:03
Show Gist options
  • Save ccooke/406774 to your computer and use it in GitHub Desktop.
Save ccooke/406774 to your computer and use it in GitHub Desktop.
require 'thread'
require 'marshall'
class Commit < Exception
end
class Rollback < Exception
end
class AbortTransaction < Rollback
end
module Transactions
attr_writer :autocommit, :dry_run
def autocommit
@autocommit ||= false
end
def dry_run
@dry_run ||= false
end
def commit!
commit false
end
def commit merge=true
if transaction? then
original = @__original__
_transaction_merge merge
_cleanup_transaction_copy
original
else
raise Commit.new
end
end
def rollback
if transaction? then
original = @__original__
rollback_cleanup Marshal.load( @__transaction__ )
_cleanup_transaction_copy
original
else
raise Rollback.new
end
end
def _transaction_merge merge
lock do
copy = Marshal.load( @__transaction__ )
copy_vars = {}
copy.instance_variables.each do |var|
copy_vars[ var ] = copy.instance_variable_get var
end
@__original__.lock do
original_vars = {}
@__original__.instance_variables.each do |var|
original_vars[ var ] = @__original__.instance_variable_get var
end
instance_variables.each do |var|
next if [
"@__transaction__",
"@__original__",
"@__methodmap__",
"@transaction_copy"
].include? var
if original_vars.include? var then
if copy_vars.include? var then
if original_vars[ var ] == copy_vars[ var ] || ! merge then
# It's not changed, or we're forcing it anyway
@__original__.instance_variable_set( var, instance_variable_get( var ) )
else
# the variable changed, and we're not forcing
end
elsif merge then
# The variable was created since the transaction started
else
# We're not merging, we're replacing.
@__original__.remove_instance_variable var
end
else
# It's new, from us.
@__original__.instance_variable_set( var, instance_variable_get( var ) )
end
end
end
end
end
private :_transaction_merge
def _cleanup_transaction_copy
_replace_public_methods do |realmethod, *args|
raise AbortTransaction.new( "Attempted to re-use a completed transaction object" )
end
end
private :_cleanup_transaction_copy
def _replace_public_methods &block
@__methodmap__ = {}
public_methods.select { |m|
next if self.method( m.to_sym ).owner == Kernel
next if self.method( m.to_sym ).owner == Transactions
true
}.each do |method_name|
meth = method_name.to_sym
method_object = method( meth )
@__methodmap__[ meth ] = method_object
class << self
self
end.instance_eval do
define_method meth, Proc.new do |*args|
puts "Redefine #{ method_name }"
block.call( method_object, *args )
end
end
end
end
private :_replace_public_methods
def _restore_public_methods
@__methodmap__.each do |name, method|
class << self
self
end.instance_eval do
define_method name, method
end
end
end
private :_restore_public_methods
def _transaction_rollback_cleanup copy
copy.instance_variables.each do |var|
instance_variable_set( var, copy.instance_variable_get( var ) )
end
instance_variables.each do |var|
remove_instance_variable var unless copy.instance_variables.include? var
end
end
private :_transaction_rollback_cleanup
# Extend this to clean up any side effects - note that
# this class *cannot* protect from them.
def rollback_cleanup copy
_transaction_rollback_cleanup copy
end
def dry_run &block
result = false
transaction do
begin
dry_run = true
result = block.call
rescue Commit => c
result = true
rollback
end
end
return result
end
def lock *params
@transaction_lock ||= Mutex.new
@transaction_lock.synchronize do
yield *params
end
end
def transaction_state
if transaction? then
if instance_variable_defined? "@__original__" then
:active
else
:complete
end
else
false
end
end
def transaction_id
if transaction? then
@__transaction__.object_id
else
false
end
end
def transaction?
instance_variable_defined? "@__transaction__"
end
def transaction
lock do
returnval = false
@transaction_copy = nil
begin
# Create a deep clone to work with
copied_data = Marshal.dump( self )
@transaction_copy = Marshal.load( copied_data )
if block_given? then
yield
else
@transaction_copy.instance_variable_set "@__transaction__", copied_data
@transaction_copy.instance_variable_set "@__original__", self
return @transaction_copy
end
rollback unless autocommit
commit
rescue Commit => c
returnval = true
rescue Rollback => r
rollback_cleanup Marshal.load( copied_data )
returnval = false
ensure
remove_instance_variable "@transaction_copy"
end
return returnval
end
end
end
class A
include Transactions
attr_accessor :foo, :bar
end
a = A.new
a.transaction do
a.foo = 100
end
# => false
a
# => #<A:0x7fb7449a2448 @transaction_lock=#<Mutex:0x7fb744997458>>
a.transaction do
a.foo = 1000
a.commit
end
# => true
a
# => #<A:0x7fb7449a2448 @foo=1000, @transaction_lock=#<Mutex:0x7fb744997458>>
a.dry_run do
a.bar = 1000
a.commit
end
# => true
a
# => #<A:0x7fb7449a2448 @foo=1000, @transaction_lock=#<Mutex:0x7fb744a19d40>>
b = a.transaction
b.foo = "This is in a transaction?"
b.rollback
b.foo
a
b = a.transaction
b.foo = "Test"
b.commit
a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment