pg_transaction_retry 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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