Skip to content

Commit

Permalink
initial version; work in progress; mysql2 should mostly work; simple …
Browse files Browse the repository at this point in the history
…smoke test
  • Loading branch information
qertoip committed Feb 5, 2012
0 parents commit 01ea3f9
Show file tree
Hide file tree
Showing 29 changed files with 512 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
@@ -0,0 +1,6 @@
*.gem
.bundle
Gemfile.lock
pkg/*
.idea

31 changes: 31 additions & 0 deletions Gemfile
@@ -0,0 +1,31 @@
source "http://rubygems.org"

# Specify your gem's dependencies in kontolib.gemspec
gemspec

group :test do
# Use the gem instead of a dated version bundled with Ruby
gem 'minitest', '2.8.1'

# Allows to test for method invocations. Used sparingly but indispensable
gem 'mocha', :require => false

gem 'activerecord', "3.1.3"

gem 'simplecov', :require => false

# TDD (watch for file changes and fire the test)
gem 'watchr'
gem 'rev'

gem 'mysql2'
gem 'pg'
gem 'sqlite3'
end

group :development do
gem 'rake'
# enhance irb
gem 'awesome_print', :require => false
gem 'pry', :require => false
end
11 changes: 11 additions & 0 deletions Rakefile
@@ -0,0 +1,11 @@
require "bundler/gem_tasks"

require 'rake/testtask'

Rake::TestTask.new do |t|
t.libs += ["test", "lib"]
t.pattern = 'test/smoke/**/*_test.rb'
t.verbose = true
end

task :default => [:test]
1 change: 1 addition & 0 deletions d
@@ -0,0 +1 @@
bundle exec ruby test/test_console.rb
26 changes: 26 additions & 0 deletions lib/transaction_isolation.rb
@@ -0,0 +1,26 @@
# Require version statement

require_relative 'transaction_isolation/version'

# Require modules with ActiveRecord enhancements

def apply_transaction_isolation_patch
require 'active_record'
require_relative 'transaction_isolation/active_record/errors'
require_relative 'transaction_isolation/active_record/base'
require_relative 'transaction_isolation/active_record/connection_adapters/abstract_adapter'
require_relative 'transaction_isolation/active_record/connection_adapters/mysql2_adapter'
require_relative 'transaction_isolation/active_record/connection_adapters/postgresql_adapter'
require_relative 'transaction_isolation/active_record/connection_adapters/sqlite3_adapter'
end

$stderr.puts( "transaction_isolation" )

if defined?( Rails )
Rails.application.config.after_initialize do
$stderr.puts( "apply_transaction_isolation_patch" )
apply_transaction_isolation_patch
end
else
apply_transaction_isolation_patch
end
13 changes: 13 additions & 0 deletions lib/transaction_isolation/active_record/base.rb
@@ -0,0 +1,13 @@
require 'active_record/base'

module TransactionIsolation
module ActiveRecord
module Base
def isolation_level( isolation_level, &block )
connection.isolation_level( isolation_level, &block )
end
end
end
end

ActiveRecord::Base.extend( TransactionIsolation::ActiveRecord::Base )
@@ -0,0 +1,32 @@
require 'active_record/connection_adapters/abstract_adapter'

module TransactionIsolation
module ActiveRecord
module ConnectionAdapters # :nodoc:
module AbstractAdapter

VALID_ISOLATION_LEVELS = [:read_uncommitted, :read_committed, :repeatable_read, :serializable]

# If true, #isolation_level(level) method is available
def supports_isolation_levels?
false
end

def isolation_level( level )
raise NotImplementedError
end

private

def validate_isolation_level( isolation_level )
unless VALID_ISOLATION_LEVELS.include?( isolation_level )
raise ArgumentError, "Invalid isolation level '#{isolation_level}'. Supported levels include #{VALID_ISOLATION_LEVELS.join( ', ' )}."
end
end

end
end
end
end

ActiveRecord::ConnectionAdapters::AbstractAdapter.send( :include, TransactionIsolation::ActiveRecord::ConnectionAdapters::AbstractAdapter )
@@ -0,0 +1,64 @@
if defined?( ActiveRecord::ConnectionAdapters::Mysql2Adapter )

#require 'active_record/connection_adapters/mysql2_adapter'

module TransactionIsolation
module ActiveRecord
module ConnectionAdapters # :nodoc:
module Mysql2Adapter

def included( base )
base.class_eval do
alias_method :translate_exception_without_transaction_isolation_conflict, :translate_exception
alias_method :translate_exception, :translate_exception_with_transaction_isolation_conflict
end
end

def supports_isolation_levels?
true
end

VENDOR_SPECIFIC_ISOLATION_LEVELS = {
:read_uncommitted => 'READ UNCOMMITTED',
:read_committed => 'READ COMMITTED',
:repeatable_read => 'REPEATABLE READ',
:serializable => 'SERIALIZABLE'
}

def translate_exception_with_transaction_isolation_conflict( exception, message )
if tx_isolation_conflict?( exception )
::ActiveRecord::TransactionIsolationConflict.new( "Transaction isolation conflict detected: #{exception.message}", exception )
else
translate_exception_without_transaction_isolation_conflict( exception, message )
end
end

def tx_isolation_conflict?( exception )
[ "Deadlock found when trying to get lock",
"Lock wait timeout exceeded"].any? do |error_message|
exception.message =~ /#{Regexp.escape( error_message )}/
end
end

def isolation_level( level )
validate_isolation_level( level )

original_isolation = select_value( "select @@session.tx_isolation" ).gsub( '-', ' ' )

execute( "SET SESSION TRANSACTION ISOLATION LEVEL #{VENDOR_SPECIFIC_ISOLATION_LEVELS[level]}" )

begin
yield
ensure
execute "SET SESSION TRANSACTION ISOLATION LEVEL #{original_isolation}"
end if block_given?
end

end
end
end
end

ActiveRecord::ConnectionAdapters::Mysql2Adapter.send( :include, TransactionIsolation::ActiveRecord::ConnectionAdapters::Mysql2Adapter )

end
@@ -0,0 +1,45 @@
if defined?( ActiveRecord::ConnectionAdapters::PostgreSQLAdapter )

#require 'active_record/connection_adapters/postgresql_adapter'

module TransactionIsolation
module ActiveRecord
module ConnectionAdapters # :nodoc:
module PostgreSQLAdapter

def supports_isolation_levels?
true
end

VENDOR_SPECIFIC_ISOLATION_LEVELS = {
:read_uncommitted => 'READ UNCOMMITTED',
:read_committed => 'READ COMMITTED',
:repeatable_read => 'REPEATABLE READ',
:serializable => 'SERIALIZABLE'
}

TRANSACTION_CONFLICT_ERRORS = [
"deadlock detected",
"could not serialize access"
]

def isolation_level( level )
validate_isolation_level( level )

execute "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL #{VENDOR_SPECIFIC_ISOLATION_LEVELS[level]}"

begin
yield
ensure
execute "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL #{'READ COMMITTED'}"
end if block_given?
end

end
end
end
end

ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send( :include, TransactionIsolation::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter )

end
@@ -0,0 +1,44 @@
if defined?( ActiveRecord::ConnectionAdapters::SQLiteAdapter )

#require 'active_record/connection_adapters/sqlite3_adapter'

module TransactionIsolation
module ActiveRecord
module ConnectionAdapters # :nodoc:
module SQLite3Adapter

def supports_isolation_levels?
true
end

VENDOR_SPECIFIC_ISOLATION_LEVELS = {
:read_uncommitted => 'read_uncommitted = true',
:read_committed => 'read_uncommitted = false',
:repeatable_read => 'read_uncommitted = false',
:serializable => 'read_uncommitted = false'
}

TRANSACTION_CONFLICT_ERRORS = [
"is locked"
]

def isolation_level( level )
validate_isolation_level( level )

execute "PRAGMA #{VENDOR_SPECIFIC_ISOLATION_LEVELS[level]}"

begin
yield
ensure
execute "PRAGMA #{VENDOR_SPECIFIC_ISOLATION_LEVELS[initial_isolation_level]}"
end if block_given?
end

end
end
end
end

ActiveRecord::ConnectionAdapters::SQLite3Adapter.send( :include, TransactionIsolation::ActiveRecord::ConnectionAdapters::SQLite3Adapter )

end
14 changes: 14 additions & 0 deletions lib/transaction_isolation/active_record/errors.rb
@@ -0,0 +1,14 @@
require 'active_record/errors'

module TransactionIsolation
module ActiveRecord
# This exception represents both deadlocks and serialization conflicts.
# Deadlocks happen when db engine is using lock-based concurrency control.
# Serialization conflicts happen when db engine is using multi-version concurrency control.
# Often db engines combine both approaches and thus generate both types of errors.

class TransactionIsolationConflict < ::ActiveRecord::WrappedDatabaseException; end
end
end

ActiveRecord.send( :include, TransactionIsolation::ActiveRecord )
8 changes: 8 additions & 0 deletions lib/transaction_isolation/apply_monkey_patching.rb
@@ -0,0 +1,8 @@
# All monkey patching is done in this file

ActiveRecord.send( :include, TransactionIsolation::ActiveRecord )
ActiveRecord::Base.extend( TransactionIsolation::ActiveRecord::Base )
ActiveRecord::ConnectionAdapters::AbstractAdapter.send( :include, TransactionIsolation::ActiveRecord::ConnectionAdapters::AbstractAdapter )
ActiveRecord::ConnectionAdapters::Mysql2Adapter.send( :include, TransactionIsolation::ActiveRecord::ConnectionAdapters::Mysql2Adapter )
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send( :include, TransactionIsolation::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter )
ActiveRecord::ConnectionAdapters::SQLite3Adapter.send( :include, TransactionIsolation::ActiveRecord::ConnectionAdapters::SQLite3Adapter )
3 changes: 3 additions & 0 deletions lib/transaction_isolation/version.rb
@@ -0,0 +1,3 @@
module TransactionIsolation
VERSION = "0.9"
end
3 changes: 3 additions & 0 deletions test/db/all.rb
@@ -0,0 +1,3 @@
require 'active_record'
require_relative 'db'
require_relative 'migrations'
35 changes: 35 additions & 0 deletions test/db/db.rb
@@ -0,0 +1,35 @@
require 'fileutils'

module TransactionIsolation
module Test
module Db

def self.connect_to_mysql2
::ActiveRecord::Base.establish_connection(
:adapter => "mysql2",
:database => "transaction_isolation_test", #database_filepath
:user => 'root',
:password => ''
)
end

def self.connect_to_postgresql
::ActiveRecord::Base.establish_connection(
:adapter => "postgresql",
:database => "transaction_isolation_test", #database_filepath
:user => 'qertoip',
:password => 'test123'
)
end

def self.connect_to_sqlite3
ActiveRecord::Base.establish_connection(
:adapter => "sqlite3",
:database => ":memory:",
:verbosity => "silent"
)
end

end
end
end
20 changes: 20 additions & 0 deletions test/db/migrations.rb
@@ -0,0 +1,20 @@
module TransactionIsolation
module Test
module Migrations

def self.run!
c = ::ActiveRecord::Base.connection

# Queued Jobs

c.create_table "queued_jobs", :force => true do |t|
t.text "job", :null => false
t.integer "status", :default => 0, :null => false
t.timestamps
end

end

end
end
end
Binary file added test/icons/fail.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/icons/pass.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/icons/pending.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions test/library_setup.rb
@@ -0,0 +1,20 @@
# Prepares application to be tested (requires files, connects to db, resets schema and data, applies patches, etc.)

# Initialize database
require 'db/all'

case ENV['db']
when 'mysql2'
TransactionIsolation::Test::Db.connect_to_mysql2
when 'postgresql'
TransactionIsolation::Test::Db.connect_to_postgresql
when 'sqlite3'
TransactionIsolation::Test::Db.connect_to_sqlite3
else
TransactionIsolation::Test::Db.connect_to_mysql2
end

TransactionIsolation::Test::Migrations.run!

# Load the code that will be tested
require 'transaction_isolation'
Empty file added test/log/.gitkeep
Empty file.

0 comments on commit 01ea3f9

Please sign in to comment.