Created
May 20, 2010 19:04
-
-
Save ccooke/407948 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
# This is UNRELEASED code. | |
# There is currently no license to use it; that will come later | |
# This code was written while working for Betfair and will be released publicly later | |
require 'thread' | |
module MethodInterception | |
private | |
def _replace_public_methods methods_to_replace, &block | |
@__methodinterception_map ||= {} | |
methods_to_replace.each do |meth| | |
method_object = method( meth ) | |
@__methodinterception_map[ meth ] = method_object | |
class << self | |
self | |
end.instance_eval do | |
define_method meth, Proc.new do |*args| | |
block.call( meth, method_object, *args ) | |
end | |
end | |
end | |
end | |
def _restore_public_methods | |
@__methodinterception_map ||= {} | |
__methodinterception_map.each do |name, method| | |
class << self | |
self | |
end.instance_eval do | |
define_method name, method | |
end | |
end | |
end | |
end | |
module Transactions | |
class Commit < Exception | |
end | |
class Rollback < Exception | |
end | |
class Abort < Rollback | |
end | |
module Querying | |
attr_reader :autocommit, :transaction_state, :dry_run, :strict_merge, :proxy | |
def transaction_id | |
if transaction? then | |
@__transaction__.object_id | |
else | |
false | |
end | |
end | |
def transaction? | |
instance_variable_defined? "@__transaction__" | |
end | |
def locked? | |
@transaction_lock.locked? | |
end | |
end | |
module Merge | |
private | |
def _transaction_test_copy_vars from, to, orig | |
values = {} | |
from.instance_variables.each do |var| | |
next if @__transactions_ignore_variables.include? var | |
values[ var ] = from.instance_variable_get var | |
if to.instance_variable_defined? var then | |
to_value = to.instance_variable_get var | |
if orig.instance_variable_defined? var then | |
orig_value = orig.instance_variable_get var | |
if orig_value != to_value then | |
values[ var ] = to.transaction_merge_var var, values[ var ] | |
end | |
else | |
values[ var ] = to.transaction_merge_var var, values[ var ] | |
end | |
end | |
end | |
values | |
end | |
def _transaction_test_delete_vars from, to, orig | |
delete_vars = [] | |
to.instance_variables.each do |var| | |
next if @__transactions_ignore_variables.include? var | |
next if from.instance_variable_defined? var | |
unless to.transaction_delete_allow? var then | |
delete_vars.push var | |
end | |
end | |
delete_vars | |
end | |
def _transaction_test_merge from, to, orig | |
# Test the merge | |
return { | |
:copy => _transaction_test_copy_vars( from, to, orig ), | |
:delete => _transaction_test_delete_vars( from, to, orig ), | |
} | |
end | |
def _transaction_merge_variables from, to, orig | |
vars = _transaction_test_merge from, to, orig | |
vars[:copy].each do |var, value| | |
to.instance_variable_set var, value | |
end | |
vars[:delete].each do |var| | |
to.remove_instance_variable var | |
end | |
end | |
def _transaction_merge merge | |
lock do | |
copy = Marshal.load( @__transaction__ ) | |
@__original__.lock do | |
_transaction_merge_variables self, @__original__, copy | |
end | |
end | |
end | |
def _cleanup_transaction_copy | |
_replace_public_methods( | |
self.public_methods.select do |meth| | |
next if method( meth.to_sym ).owner == Kernel | |
next if method( meth.to_sym ).owner == Transactions::Querying | |
true | |
end | |
) do |realmethod, *args| | |
raise Transactions::Abort.new( "Attempted to re-use a completed transaction object" ) | |
end | |
end | |
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 | |
end | |
module Commands | |
def commit! | |
commit false | |
end | |
def commit merge=true | |
if transaction? then | |
original = @__original__ | |
_transaction_merge merge | |
_cleanup_transaction_copy | |
original | |
else | |
raise Transactions::Commit.new | |
end | |
end | |
def rollback | |
if transaction? then | |
original = @__original__ | |
rollback_cleanup Marshal.load( @__transaction__ ) | |
_cleanup_transaction_copy | |
original | |
else | |
raise Transactions::Rollback.new | |
end | |
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.synchronize do | |
yield *params | |
end | |
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 | |
original = Marshal.load( copied_data ) | |
_transaction_merge_variables original, self, original | |
returnval = false | |
ensure | |
remove_instance_variable "@transaction_copy" | |
end | |
return returnval | |
end | |
end | |
end | |
module Overridables | |
# 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 | |
# This will be called whenever a clash happens: that is, | |
# when a variable in the to-be-merged object existed when | |
# the transaction began and has changed since. | |
def transaction_merge_var variable, new_value | |
case @transaction_merge_policy | |
when :replace then new_value | |
when :strict then | |
raise Transactions::Abort.new( "Merge conflict setting #{ variable }" ) | |
#when :keep then instance_variable_get( variable ) | |
else | |
instance_variable_get( variable ) | |
end | |
end | |
# This will be called whenever a variable exists in the | |
# merge target object but not in the merge source. if | |
# this function returns true, the variable will be deleted | |
# from the merge target object during the merge. | |
def transaction_delete_allow? variable | |
case @transaction_merge_policy | |
when :replace then true | |
when :strict then | |
raise Transactions::Abort.new( "Merge conflict deleting #{ variable }" ) | |
else | |
#when :keep then false | |
end | |
end | |
end | |
module LockedMethods | |
def initialize *args | |
@_transaction_lock_timeout = 60 | |
super | |
end | |
def add_transaction_lock_to methods | |
_replace_public_methods( methods ) do |name, callable, *args| | |
@_transaction_lock_timeout.downto( 0 ) do | |
if @transaction_lock.try_lock then | |
begin | |
return callable.call( *args ) | |
rescue | |
@transaction_lock.unlock | |
raise $! | |
end | |
end | |
sleep 1 | |
end | |
raise Transactions::Abort.new( "Failed to get lock for method #{ name }" ) | |
end | |
end | |
end | |
include MethodInterception | |
include Transactions::Querying | |
include Transactions::Merge | |
include Transactions::Commands | |
include Transactions::Overridables | |
include Transactions::LockedMethods | |
attr_writer :autocommit, :dry_run, :transaction_merge_policy, :transaction_cascade | |
def initialize *attr | |
@transaction_cascade = true | |
@autocommit = @dry_run = false | |
@transaction_merge_policy = :replace | |
@transaction_state = :none | |
@__transactions_ignore_variables = [ | |
"@__transaction__", | |
"@__original__", | |
"@__methodinterception_map", | |
"@transaction_copy" | |
] | |
@transaction_lock = Mutex.new | |
super *attr | |
end | |
end | |
class A | |
include Transactions | |
attr_accessor :foo, :bar | |
end | |
a = A.new | |
a.foo = "Unmodified" | |
a.bar = "Not changed" | |
a.transaction do | |
a.foo = 100 | |
end | |
# => false | |
a.foo | |
# => "Unmodified" | |
a.transaction do | |
a.foo = 1000 | |
a.commit | |
end | |
# => true | |
a.foo | |
# => 1000 | |
a.dry_run do | |
a.bar = 1000 | |
a.commit | |
end | |
# => true | |
a.bar | |
# => "Not changed" | |
b = a.transaction | |
b.foo = "This is in a transaction?" | |
b.rollback | |
b.foo | |
a.foo | |
# => 1000 | |
b = a.transaction | |
b.foo = "Test" | |
b.commit | |
a.foo | |
# => "Test" | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment