pg_transaction_retry 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .idea
6
+ test/log/*.log
data/Gemfile ADDED
@@ -0,0 +1,20 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in kontolib.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ # Use the gem instead of a dated version bundled with Ruby
8
+ gem 'minitest', '2.8.1'
9
+
10
+ gem 'simplecov', :require => false
11
+
12
+ gem 'pg'
13
+ end
14
+
15
+ group :development do
16
+ gem 'rake'
17
+ # enhance irb
18
+ gem 'awesome_print', :require => false
19
+ gem 'pry', :require => false
20
+ end
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2012 Piotr 'Qertoip' Włodarek
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # pg_transaction_retry
2
+
3
+ Retries database transaction on deadlock and transaction serialization errors. Supports PostgreSQL.
4
+
5
+ ## Example
6
+
7
+ The gem works automatically by rescuing ActiveRecord::TransactionIsolationConflict and retrying the transaction.
8
+
9
+ ## Installation
10
+
11
+ Add this to your Gemfile:
12
+
13
+ gem 'pg_transaction_retry'
14
+
15
+ Then run:
16
+
17
+ bundle
18
+
19
+ __It works out of the box with Ruby on Rails__.
20
+
21
+ If you have a standalone ActiveRecord-based project you'll need to call:
22
+
23
+ TransactionRetry.apply_activerecord_patch # after connecting to the database
24
+
25
+ __after__ connecting to the database.
26
+
27
+ ## Database deadlock and serialization errors that are retried
28
+
29
+ #### PostgreSQL
30
+
31
+ * deadlock detected
32
+ * could not serialize access
33
+
34
+ ## Configuration
35
+
36
+ You can optionally configure pg_transaction_retry gem in your config/initializers/pg_transaction_retry.rb (or anywhere else):
37
+
38
+ TransactionRetry.max_retries = 3
39
+ TransactionRetry.wait_times = [0, 1, 2, 4, 8, 16, 32] # seconds to sleep after retry n
40
+
41
+ ## Features
42
+
43
+ * Supports PostgreSQL (as long as you are using pg).
44
+ * Exponential sleep times between retries (0, 1, 2, 4 seconds).
45
+ * Logs every retry as a warning.
46
+ * Intentionally does not retry nested transactions.
47
+ * Configurable number of retries and sleep time between them.
48
+ * Use it in your Rails application or a standalone ActiveRecord-based project.
49
+
50
+ ## Testimonials
51
+
52
+ This gem was initially developed for and successfully works in production at [Kontomierz.pl](http://kontomierz.pl) - the finest Polish personal finance app.
53
+
54
+ ## Requirements
55
+
56
+ * ruby 1.9.2
57
+ * activerecord 3.0.11+
58
+
59
+ ## Running tests
60
+
61
+ Run tests on the selected database:
62
+
63
+ db=postgresql bundle exec rake test
64
+
65
+ Run tests on all supported databases:
66
+
67
+ ./tests
68
+
69
+ Database configuration is hardcoded in test/db/db.rb; feel free to improve this and submit a pull request.
70
+
71
+ ## How intrusive is this gem?
72
+
73
+ You should be very suspicious about any gem that monkey patches your stock Ruby on Rails framework.
74
+
75
+ This gem is carefully written to not be more intrusive than it needs to be:
76
+
77
+ * wraps ActiveRecord::Base#transaction class method using alias_method to add new behaviour
78
+ * introduces two new private class methods in ActiveRecord::Base (with names that should never collide)
79
+
80
+ ## License
81
+
82
+ Released under the MIT license. Copyright (C) 2012 Piotr 'Qertoip' Włodarek.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs += ["test", "lib"]
7
+ t.pattern = 'test/integration/**/*_test.rb'
8
+ t.verbose = true
9
+ end
10
+
11
+ task :default => [:test]
data/d ADDED
@@ -0,0 +1 @@
1
+ bundle exec ruby test/test_console.rb
@@ -0,0 +1,49 @@
1
+ require "active_record"
2
+ require "transaction_isolation"
3
+
4
+ require_relative "transaction_retry/version"
5
+
6
+ module TransactionRetry
7
+
8
+ # Must be called after ActiveRecord established a connection.
9
+ # Only then we know which connection adapter is actually loaded and can be enhanced.
10
+ # Please note ActiveRecord does not load unused adapters.
11
+ def self.apply_activerecord_patch
12
+ TransactionIsolation.apply_activerecord_patch
13
+ require_relative 'transaction_retry/active_record/base'
14
+ end
15
+
16
+ if defined?( ::Rails )
17
+ # Setup applying the patch after Rails is initialized.
18
+ class Railtie < ::Rails::Railtie
19
+ config.after_initialize do
20
+ TransactionRetry.apply_activerecord_patch
21
+ end
22
+ end
23
+ end
24
+
25
+ def self.max_retries
26
+ @@max_retries ||= 3
27
+ end
28
+
29
+ def self.max_retries=( n )
30
+ @@max_retries = n
31
+ end
32
+
33
+ def self.wait_times
34
+ @@wait_times ||= [0, 1, 2, 4, 8, 16, 32]
35
+ end
36
+
37
+ def self.wait_times=( array_of_seconds )
38
+ @@wait_times = array_of_seconds
39
+ end
40
+
41
+ def self.fuzz
42
+ @@fuzz ||= true
43
+ end
44
+
45
+ def self.fuzz=( val )
46
+ @@fuzz = val
47
+ end
48
+
49
+ end
@@ -0,0 +1,65 @@
1
+ require 'active_record/base'
2
+
3
+ module TransactionRetry
4
+ module ActiveRecord
5
+ module Base
6
+
7
+ def self.included( base )
8
+ base.extend( ClassMethods )
9
+ base.class_eval do
10
+ class << self
11
+ alias_method :transaction_without_retry, :transaction
12
+ alias_method :transaction, :transaction_with_retry
13
+ end
14
+ end
15
+ end
16
+
17
+ module ClassMethods
18
+
19
+ def transaction_with_retry(*objects, &block)
20
+ retry_count = 0
21
+
22
+ begin
23
+ transaction_without_retry(*objects, &block)
24
+ rescue ::ActiveRecord::TransactionIsolationConflict
25
+ raise if retry_count >= TransactionRetry.max_retries
26
+ raise if tr_in_nested_transaction?
27
+
28
+ retry_count += 1
29
+ postfix = { 1 => 'st', 2 => 'nd', 3 => 'rd' }[retry_count] || 'th'
30
+ logger.warn "Transaction isolation conflict detected. Retrying for the #{retry_count}-#{postfix} time..." if logger
31
+ tr_exponential_pause( retry_count )
32
+ retry
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ # Sleep 0, 1, 2, 4, ... seconds up to the TransactionRetry.max_retries.
39
+ # Cap the sleep time at 32 seconds.
40
+ # An ugly tr_ prefix is used to minimize the risk of method clash in the future.
41
+ def tr_exponential_pause( count )
42
+ seconds = TransactionRetry.wait_times[count-1] || 32
43
+
44
+ if TransactionRetry.fuzz
45
+ fuzz_factor = [seconds * 0.25, 1].max
46
+
47
+ seconds += rand * (fuzz_factor * 2) - fuzz_factor
48
+ end
49
+
50
+ sleep( seconds ) if seconds > 0
51
+ end
52
+
53
+ # Returns true if we are in the nested transaction (the one with :requires_new => true).
54
+ # Returns false otherwise.
55
+ # An ugly tr_ prefix is used to minimize the risk of method clash in the future.
56
+ def tr_in_nested_transaction?
57
+ connection.open_transactions != 0
58
+ end
59
+
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ ActiveRecord::Base.send( :include, TransactionRetry::ActiveRecord::Base )
@@ -0,0 +1,3 @@
1
+ module TransactionRetry
2
+ VERSION = "1.0.2"
3
+ end
data/test/db/all.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'active_record'
2
+ require_relative 'db'
3
+ require_relative 'migrations'
4
+ require_relative 'queued_job'
data/test/db/db.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'fileutils'
2
+
3
+ module TransactionRetry
4
+ module Test
5
+ module Db
6
+
7
+ def self.connect_to_postgresql
8
+ ::ActiveRecord::Base.establish_connection(
9
+ :adapter => "postgresql",
10
+ :database => "transaction_retry_test",
11
+ :user => 'qertoip',
12
+ :password => 'test123'
13
+ )
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ module TransactionRetry
2
+ module Test
3
+ module Migrations
4
+
5
+ def self.run!
6
+ c = ::ActiveRecord::Base.connection
7
+
8
+ # Queued Jobs
9
+
10
+ c.create_table "queued_jobs", :force => true do |t|
11
+ t.text "job", :null => false
12
+ t.integer "status", :default => 0, :null => false
13
+ t.timestamps
14
+ end
15
+
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,2 @@
1
+ class QueuedJob < ActiveRecord::Base
2
+ end
@@ -0,0 +1,87 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'test_helper'
4
+
5
+ class TransactionWithRetryTest < MiniTest::Unit::TestCase
6
+
7
+ def setup
8
+ @original_max_retries = TransactionRetry.max_retries
9
+ @original_wait_times = TransactionRetry.wait_times
10
+ end
11
+
12
+ def teardown
13
+ TransactionRetry.max_retries = @original_max_retries
14
+ TransactionRetry.wait_times = @original_wait_times
15
+ QueuedJob.delete_all
16
+ end
17
+
18
+ def test_does_not_break_transaction
19
+ ActiveRecord::Base.transaction do
20
+ QueuedJob.create!( :job => 'is fun!' )
21
+ assert_equal( 1, QueuedJob.count )
22
+ end
23
+ assert_equal( 1, QueuedJob.count )
24
+ QueuedJob.first.destroy
25
+ end
26
+
27
+ def test_does_not_break_transaction_rollback
28
+ ActiveRecord::Base.transaction do
29
+ QueuedJob.create!( :job => 'gives money!' )
30
+ raise ActiveRecord::Rollback
31
+ end
32
+ assert_equal( 0, QueuedJob.count )
33
+ end
34
+
35
+ def test_retries_transaction_on_transaction_isolation_conflict
36
+ first_run = true
37
+
38
+ ActiveRecord::Base.transaction do
39
+ if first_run
40
+ first_run = false
41
+ message = "Deadlock found when trying to get lock"
42
+ raise ActiveRecord::TransactionIsolationConflict.new( ActiveRecord::StatementInvalid.new( message ), message )
43
+ end
44
+ QueuedJob.create!( :job => 'is cool!' )
45
+ end
46
+ assert_equal( 1, QueuedJob.count )
47
+
48
+ QueuedJob.first.destroy
49
+ end
50
+
51
+ def test_does_not_retry_transaction_more_than_max_retries_times
52
+ TransactionRetry.max_retries = 1
53
+ run = 0
54
+
55
+ assert_raises( ActiveRecord::TransactionIsolationConflict ) do
56
+ ActiveRecord::Base.transaction do
57
+ run += 1
58
+ message = "Deadlock found when trying to get lock"
59
+ raise ActiveRecord::TransactionIsolationConflict.new( ActiveRecord::StatementInvalid.new( message ), message )
60
+ end
61
+ end
62
+
63
+ assert_equal( 2, run ) # normal run + one retry
64
+ end
65
+
66
+ def test_does_not_retry_nested_transaction
67
+ first_try = true
68
+
69
+ ActiveRecord::Base.transaction do
70
+
71
+ assert_raises( ActiveRecord::TransactionIsolationConflict ) do
72
+ ActiveRecord::Base.transaction( :requires_new => true ) do
73
+ if first_try
74
+ first_try = false
75
+ message = "Deadlock found when trying to get lock"
76
+ raise ActiveRecord::TransactionIsolationConflict.new( ActiveRecord::StatementInvalid.new( message ), message )
77
+ end
78
+ QueuedJob.create!( :job => 'is cool!' )
79
+ end
80
+ end
81
+
82
+ end
83
+
84
+ assert_equal( 0, QueuedJob.count )
85
+ end
86
+
87
+ end
@@ -0,0 +1,21 @@
1
+ # Prepares application to be tested (requires files, connects to db, resets schema and data, applies patches, etc.)
2
+
3
+ # Initialize database
4
+ require 'db/all'
5
+
6
+ case ENV['db']
7
+ when 'postgresql'
8
+ TransactionRetry::Test::Db.connect_to_postgresql
9
+ else
10
+ TransactionRetry::Test::Db.connect_to_postgresql
11
+ end
12
+
13
+ require 'logger'
14
+ ActiveRecord::Base.logger = Logger.new( File.expand_path( "#{File.dirname( __FILE__ )}/log/test.log" ) )
15
+
16
+ TransactionRetry::Test::Migrations.run!
17
+
18
+ # Load the code that will be tested
19
+ require 'transaction_retry'
20
+
21
+ TransactionRetry.apply_activerecord_patch
data/test/log/.gitkeep ADDED
File without changes
@@ -0,0 +1,11 @@
1
+ # Ensure that LOAD_PATH is the same as when running "rake test"; normally rake takes care of that
2
+ $LOAD_PATH << File.expand_path( ".", File.dirname( __FILE__ ) )
3
+ $LOAD_PATH << File.expand_path( "./lib", File.dirname( __FILE__ ) )
4
+ $LOAD_PATH << File.expand_path( "./test", File.dirname( __FILE__ ) )
5
+
6
+ # Boot the app
7
+ require_relative 'library_setup'
8
+
9
+ # Fire the console
10
+ require 'pry'
11
+ binding.pry
@@ -0,0 +1,12 @@
1
+ # Load test coverage tool (must be loaded before any code)
2
+ #require 'simplecov'
3
+ #SimpleCov.start do
4
+ # add_filter '/test/'
5
+ # add_filter '/config/'
6
+ #end
7
+
8
+ # Load and initialize the application to be tested
9
+ require 'library_setup'
10
+
11
+ # Load test frameworks
12
+ require 'minitest/autorun'
@@ -0,0 +1,4 @@
1
+ require 'test_helper'
2
+
3
+ # Load all tests
4
+ Dir.glob( "./**/*_test.rb" ).each { |test_file| require test_file }
data/tests ADDED
@@ -0,0 +1 @@
1
+ db=postgresql bundle exec rake
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "transaction_retry/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "pg_transaction_retry"
7
+ s.version = TransactionRetry::VERSION
8
+ s.authors = ["Tarkus Liu"]
9
+ s.email = ["tarkus.nnkh@gmail.com"]
10
+ s.homepage = "https://github.com/tarkus/pg_transaction_retry"
11
+ s.summary = %q{Retries database transaction on deadlock and transaction serialization errors.}
12
+ s.description = %q{Retries database transaction on deadlock and transaction serialization errors.}
13
+ s.required_ruby_version = '>= 1.9.2'
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+
20
+ s.add_runtime_dependency "activerecord", ">= 3.0.11"
21
+ s.add_runtime_dependency "transaction_isolation", ">= 1.0.2"
22
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pg_transaction_retry
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tarkus Liu
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-04-17 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.11
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 3.0.11
30
+ - !ruby/object:Gem::Dependency
31
+ name: transaction_isolation
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: 1.0.2
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 1.0.2
46
+ description: Retries database transaction on deadlock and transaction serialization
47
+ errors.
48
+ email:
49
+ - tarkus.nnkh@gmail.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - .gitignore
55
+ - Gemfile
56
+ - LICENSE
57
+ - README.md
58
+ - Rakefile
59
+ - d
60
+ - lib/transaction_retry.rb
61
+ - lib/transaction_retry/active_record/base.rb
62
+ - lib/transaction_retry/version.rb
63
+ - test/db/all.rb
64
+ - test/db/db.rb
65
+ - test/db/migrations.rb
66
+ - test/db/queued_job.rb
67
+ - test/integration/active_record/base/transaction_with_retry_test.rb
68
+ - test/library_setup.rb
69
+ - test/log/.gitkeep
70
+ - test/test_console.rb
71
+ - test/test_helper.rb
72
+ - test/test_runner.rb
73
+ - tests
74
+ - transaction_retry.gemspec
75
+ homepage: https://github.com/tarkus/pg_transaction_retry
76
+ licenses: []
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: 1.9.2
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ! '>='
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ segments:
94
+ - 0
95
+ hash: 2574943293946235221
96
+ requirements: []
97
+ rubyforge_project:
98
+ rubygems_version: 1.8.25
99
+ signing_key:
100
+ specification_version: 3
101
+ summary: Retries database transaction on deadlock and transaction serialization errors.
102
+ test_files:
103
+ - test/db/all.rb
104
+ - test/db/db.rb
105
+ - test/db/migrations.rb
106
+ - test/db/queued_job.rb
107
+ - test/integration/active_record/base/transaction_with_retry_test.rb
108
+ - test/library_setup.rb
109
+ - test/log/.gitkeep
110
+ - test/test_console.rb
111
+ - test/test_helper.rb
112
+ - test/test_runner.rb