Skip to content

Instantly share code, notes, and snippets.

@ccooke
Created May 20, 2010 19:04
Show Gist options
  • Save ccooke/407948 to your computer and use it in GitHub Desktop.
Save ccooke/407948 to your computer and use it in GitHub Desktop.
# 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