Created
May 19, 2010 20:03
-
-
Save ccooke/406774 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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