openstax_transaction_retry 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1c9efa231f44b802b7971bc7f31fb06e0f01bb47c40d16fd56a4c0843f99c72f
4
+ data.tar.gz: 8b415b5023c270aa9dec5000eb0c60f2d2dbb0f26fd5b4f489b20c7d6842b9f9
5
+ SHA512:
6
+ metadata.gz: 155c201291db2070d61814d2ea73dbdc69c58d284552a7bd84c1be367a3b33aa5e9dfc78cd8be24aa8e1d51f1cac15f74087da33b1314d91d72b19bd7682c28b
7
+ data.tar.gz: 5ca4998b159bf2a83a9839d4cb2f57aeb20a16c33730320e233a7733af712189fea737b969628937c88c97c553520a062042a913ab0d65dc43c31930be85447e
@@ -0,0 +1,39 @@
1
+ name: Tests
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - main
8
+
9
+ env:
10
+ DB_NAME: oxtr_test
11
+ DB_USERNAME: oxtr
12
+ DB_PASSWORD: oxtr_pass
13
+ DB_HOST: localhost
14
+ DB_PORT: 5432
15
+
16
+ jobs:
17
+ lint-and-test:
18
+ timeout-minutes: 10
19
+ runs-on: ubuntu-22.04
20
+ services:
21
+ db:
22
+ image: postgres:12
23
+ ports:
24
+ - 5432:5432
25
+ env:
26
+ POSTGRES_DB: oxtr_test
27
+ POSTGRES_USER: oxtr
28
+ POSTGRES_PASSWORD: oxtr_pass
29
+ options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
30
+
31
+ steps:
32
+ - uses: actions/checkout@v3
33
+ - uses: ruby/setup-ruby@v1
34
+ with:
35
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
36
+ - name: Run Tests
37
+ run: |
38
+ bundle exec rubocop
39
+ db=postgresql bundle exec rake test
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .idea
6
+ test/log/*.log
data/.rubocop.yml ADDED
@@ -0,0 +1,95 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.6
3
+ SuggestExtensions: false
4
+ NewCops: enable
5
+ Exclude:
6
+ - 'db/**/*.rb'
7
+ - 'app/bindings/**/*.rb'
8
+ - 'node_modules/**/*'
9
+ - 'tmp/**/*'
10
+ - 'vendor/**/*'
11
+ - '.git/**/*'
12
+
13
+ Layout/EmptyLinesAroundClassBody:
14
+ Enabled: false
15
+
16
+ Layout/EmptyLinesAroundBlockBody:
17
+ Enabled: false
18
+
19
+ Layout/LineLength:
20
+ Max: 100
21
+ AllowedPatterns: ['(\A|\s)#']
22
+ Exclude:
23
+ - '*.gemspec'
24
+
25
+ Metrics/AbcSize:
26
+ Max: 50
27
+
28
+ Naming/PredicateName:
29
+ Enabled: false
30
+
31
+ Metrics/ClassLength:
32
+ Max: 500
33
+ CountAsOne: ['heredoc']
34
+
35
+ Metrics/MethodLength:
36
+ Max: 50
37
+ CountAsOne: ['heredoc']
38
+
39
+ Metrics/BlockLength:
40
+ CountAsOne: ['heredoc']
41
+ Max: 30
42
+ Exclude:
43
+ - '**/*_open_api.rb'
44
+ - '**/*.rake'
45
+ - 'lib/patches/api/**/*.rb'
46
+
47
+ Metrics/ModuleLength:
48
+ CountAsOne: ['heredoc']
49
+
50
+ Style/Alias:
51
+ EnforcedStyle: prefer_alias_method
52
+
53
+ Style/Documentation:
54
+ Enabled: false
55
+
56
+ Style/OpenStructUse:
57
+ Enabled: false
58
+
59
+ Style/StringLiterals:
60
+ ConsistentQuotesInMultiline: true
61
+
62
+ Layout/SpaceAroundEqualsInParameterDefault:
63
+ EnforcedStyle: no_space
64
+
65
+ Style/AccessorGrouping:
66
+ EnforcedStyle: separated
67
+
68
+ Style/SingleLineMethods:
69
+ Enabled: false # don't abuse it, but sometimes it is right
70
+
71
+ Style/RegexpLiteral:
72
+ AllowInnerSlashes: true
73
+
74
+ Style/SymbolProc:
75
+ AllowedMethods:
76
+ - respond_to
77
+ - define_method
78
+
79
+ Layout/ClosingParenthesisIndentation:
80
+ Enabled: false # when true hanging parens look weird
81
+
82
+ Layout/MultilineMethodCallBraceLayout:
83
+ Enabled: false
84
+
85
+ Layout/MultilineMethodCallIndentation:
86
+ EnforcedStyle: indented_relative_to_receiver
87
+
88
+ Style/ClassAndModuleChildren:
89
+ Enabled: false
90
+
91
+ Style/SymbolArray:
92
+ EnforcedStyle: brackets
93
+
94
+ Style/Lambda:
95
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.1
@@ -0,0 +1,15 @@
1
+ version: v1.0
2
+ name: Initial Pipeline
3
+ agent:
4
+ machine:
5
+ type: e1-standard-2
6
+ os_image: ubuntu1804
7
+ blocks:
8
+ - name: 'Block #1'
9
+ task:
10
+ jobs:
11
+ - name: 'Job #1'
12
+ commands:
13
+ - checkout
14
+ - bundle install --path vendor/bundle
15
+ - db=sqlite3 bundle exec rake test
data/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ ### Unreleased
2
+
3
+ ### 1.2.0 - 2023-08-24
4
+ * Forked and renamed to OpenStaxTransactionRetry
5
+ * Fix bug with calling overloaded transaction with a hash
6
+
7
+
8
+ ### 1.1.0 - 2019-06-17
9
+
10
+ * Add `TransactionRetry.before_retry` configuration option to run Proc before transaction retry
11
+ * Add `TransactionRetry.retry_on` configuration option to include more Errors to retry on
12
+ * Update dependency activerecord >= 5.1
13
+ * Update dependency ruby 2.2.2
14
+ * Upgrade dependency transaction_isolation to 1.0.5
15
+ * Adapt gemspec info
16
+ * Forked gem from [transaction_retry 1.0.3](https://github.com/qertoip/transaction_retry)
data/Gemfile ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'http://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in kontolib.gemspec
6
+ gemspec
7
+
8
+ group :test do
9
+ # Use the gem instead of a dated version bundled with Ruby
10
+ gem 'minitest'
11
+
12
+ gem 'simplecov', require: false
13
+
14
+ gem 'mysql2'
15
+ gem 'pg'
16
+ gem 'rubocop'
17
+ gem 'sqlite3'
18
+ end
19
+
20
+ group :development do
21
+ gem 'rake'
22
+ # enhance irb
23
+ gem 'awesome_print', require: false
24
+ gem 'pry', require: false
25
+ 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,114 @@
1
+ # transaction_retry
2
+
3
+ Retries database transaction on deadlock and transaction serialization errors. Supports MySQL, PostgreSQL, and SQLite.
4
+
5
+ This is a forked project from
6
+
7
+ * otimalworkshop: https://github.com/optimalworkshop/transaction_retry
8
+ * qertoip: https://github.com/qertoip/transaction_retry
9
+
10
+ OpenStax forked the project to correct a bug with Ruby version 3 or above that caused
11
+ an **ArgumentError**: wrong number of arguments (given 1, expected 0)
12
+
13
+ when calling ActiveRecord::Base.transaction with a hash such as:
14
+
15
+ `ActiveRecord::Base.transaction(requires_new: true)`
16
+
17
+ Details of the fix are in commit https://github.com/openstax/transaction_retry/commit/9184c88ab917271026e08d6dd5e890740f7fdd48
18
+
19
+ ## Example
20
+
21
+ The gem works automatically by rescuing ActiveRecord::TransactionIsolationConflict and retrying the transaction.
22
+
23
+ ## Installation
24
+
25
+ Add this to your Gemfile:
26
+
27
+ gem 'transaction_retry', git: 'https://github.com/optimalworkshop/transaction_retry.git'
28
+
29
+ Then run:
30
+
31
+ bundle
32
+
33
+ __It works out of the box with Ruby on Rails__.
34
+
35
+ If you have a standalone ActiveRecord-based project you'll need to call:
36
+
37
+ OpenStaxTransactionRetry.apply_activerecord_patch # after connecting to the database
38
+
39
+ __after__ connecting to the database.
40
+
41
+ ## Database deadlock and serialization errors that are retried
42
+
43
+ #### MySQL
44
+
45
+ * Deadlock found when trying to get lock
46
+ * Lock wait timeout exceeded
47
+
48
+ #### PostgreSQL
49
+
50
+ * deadlock detected
51
+ * could not serialize access
52
+
53
+ #### SQLite
54
+
55
+ * The database file is locked
56
+ * A table in the database is locked
57
+ * Database lock protocol error
58
+
59
+ ## Configuration
60
+
61
+ You can optionally configure transaction_retry gem in your config/initializers/transaction_retry.rb (or anywhere else):
62
+
63
+ ```
64
+ OpenStaxTransactionRetry.max_retries = 3
65
+ OpenStaxTransactionRetry.wait_times = [0, 1, 2, 4, 8, 16, 32] # seconds to sleep after retry n
66
+ OpenStaxTransactionRetry.retry_on = CustomErrorClass # To add another error class to retry on (ActiveRecord::TransactionIsolationConflict always included)
67
+ or
68
+ OpenStaxTransactionRetry.retry_on = [<custom error classes>]
69
+ OpenStaxTransactionRetry.before_retry = ->(retry_num, error) { ... }
70
+ ```
71
+
72
+ ## Features
73
+
74
+ * Supports MySQL, PostgreSQL, and SQLite (as long as you are using new drivers mysql2, pg, sqlite3).
75
+ * Exponential sleep times between retries (0, 1, 2, 4 seconds).
76
+ * Logs every retry as a warning.
77
+ * Intentionally does not retry nested transactions.
78
+ * Configurable number of retries and sleep time between them.
79
+ * Configure a custom hook to run before every retry.
80
+ * Use it in your Rails application or a standalone ActiveRecord-based project.
81
+
82
+ ## Testimonials
83
+
84
+ This gem was initially developed for and successfully works in production at [Kontomierz.pl](http://kontomierz.pl) - the finest Polish personal finance app.
85
+
86
+ ## Requirements
87
+
88
+ * ruby 2.2.2+
89
+ * activerecord 5.1+
90
+
91
+ ## Running tests
92
+
93
+ Run tests on the selected database (mysql2 by default):
94
+
95
+ db=mysql2 DB_USERNAME=<db user> DB_PASSWORD=<db password> bundle exec rake test
96
+ db=postgresql DB_USERNAME=<db user> DB_PASSWORD=<db password> bundle exec rake test
97
+ db=sqlite3 bundle exec rake test
98
+
99
+ Run tests on all supported databases:
100
+
101
+ ./tests
102
+
103
+ ## How intrusive is this gem?
104
+
105
+ You should be very suspicious about any gem that monkey patches your stock Ruby on Rails framework.
106
+
107
+ This gem is carefully written to not be more intrusive than it needs to be:
108
+
109
+ * wraps ActiveRecord::Base#transaction class method using alias_method to add new behaviour
110
+ * introduces two new private class methods in ActiveRecord::Base (with names that should never collide)
111
+
112
+ ## License
113
+
114
+ Released under the MIT license. Copyright (C) 2012 Piotr 'Qertoip' Włodarek.
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+
5
+ require 'rake/testtask'
6
+
7
+ Rake::TestTask.new do |t|
8
+ t.libs += %w[test lib]
9
+ t.pattern = 'test/integration/**/*_test.rb'
10
+ t.verbose = true
11
+ end
12
+
13
+ task default: [:test]
data/d ADDED
@@ -0,0 +1 @@
1
+ bundle exec ruby test/test_console.rb
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/base'
4
+
5
+ module OpenStaxTransactionRetry
6
+ module ActiveRecord
7
+ module Base
8
+ def self.included(base)
9
+ base.extend(ClassMethods)
10
+ base.class_eval do
11
+ class << self
12
+ alias_method :transaction_without_retry, :transaction
13
+ alias_method :transaction, :transaction_with_retry
14
+ end
15
+ end
16
+ end
17
+
18
+ module ClassMethods
19
+ # rubocop:todo Metrics/PerceivedComplexity
20
+ def transaction_with_retry(**objects, &block) # rubocop:todo Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
21
+ retry_count = 0
22
+
23
+ opts = if objects.is_a? Hash
24
+ objects
25
+ elsif objects.is_a?(Array) && objects.last.is_a?(Hash)
26
+ objects.last
27
+ else
28
+ {}
29
+ end
30
+
31
+ retry_on = opts.delete(:retry_on) || OpenStaxTransactionRetry.retry_on
32
+ max_retries = opts.delete(:max_retries) || OpenStaxTransactionRetry.max_retries
33
+ before_retry = opts.delete(:before_retry) || OpenStaxTransactionRetry.before_retry
34
+
35
+ begin
36
+ transaction_without_retry(**objects, &block)
37
+ rescue ::ActiveRecord::TransactionIsolationConflict, *retry_on => e
38
+ raise if retry_count >= max_retries
39
+ raise if tr_in_nested_transaction?
40
+
41
+ retry_count += 1
42
+ logger&.warn "#{e.class.name} detected. Retry num #{retry_count}..."
43
+ before_retry&.call(retry_count, e)
44
+ tr_exponential_pause(retry_count)
45
+ retry
46
+ end
47
+ end
48
+ # rubocop:enable Metrics/PerceivedComplexity
49
+
50
+ private
51
+
52
+ # Sleep 0, 1, 2, 4, ... seconds up to the OpenStaxTransactionRetry.max_retries.
53
+ # Cap the sleep time at 32 seconds.
54
+ # An ugly tr_ prefix is used to minimize the risk of method clash in the future.
55
+ def tr_exponential_pause(count)
56
+ seconds = OpenStaxTransactionRetry.wait_times[count - 1] || 32
57
+
58
+ if OpenStaxTransactionRetry.fuzz
59
+ fuzz_factor = [seconds * 0.25, 1].max
60
+
61
+ seconds += (rand * (fuzz_factor * 2)) - fuzz_factor
62
+ end
63
+
64
+ sleep(seconds) if seconds.positive?
65
+ end
66
+
67
+ # Returns true if we are in the nested transaction (the one with :requires_new => true).
68
+ # Returns false otherwise.
69
+ # An ugly tr_ prefix is used to minimize the risk of method clash in the future.
70
+ def tr_in_nested_transaction?
71
+ connection.open_transactions != 0
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ ActiveRecord::Base.include OpenStaxTransactionRetry::ActiveRecord::Base
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenStaxTransactionRetry
4
+ VERSION = '1.2.0'
5
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require 'transaction_isolation'
5
+ require_relative 'open_stax_transaction_retry/version'
6
+
7
+ module OpenStaxTransactionRetry
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 'open_stax_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
+ OpenStaxTransactionRetry.apply_activerecord_patch
21
+ end
22
+ end
23
+ end
24
+
25
+ def self.before_retry=(lambda_block)
26
+ @@before_retry = lambda_block # rubocop:todo Style/ClassVars
27
+ end
28
+
29
+ def self.before_retry
30
+ @@before_retry ||= nil # rubocop:todo Style/ClassVars
31
+ end
32
+
33
+ def self.retry_on=(error_classes)
34
+ @@retry_on = Array(error_classes) # rubocop:todo Style/ClassVars
35
+ end
36
+
37
+ def self.retry_on
38
+ @@retry_on ||= [] # rubocop:todo Style/ClassVars
39
+ end
40
+
41
+ def self.max_retries
42
+ @@max_retries ||= 3 # rubocop:todo Style/ClassVars
43
+ end
44
+
45
+ def self.max_retries=(n) # rubocop:todo Naming/MethodParameterName
46
+ @@max_retries = n # rubocop:todo Style/ClassVars
47
+ end
48
+
49
+ def self.wait_times
50
+ @@wait_times ||= [0, 1, 2, 4, 8, 16, 32] # rubocop:todo Style/ClassVars
51
+ end
52
+
53
+ def self.wait_times=(array_of_seconds)
54
+ @@wait_times = array_of_seconds # rubocop:todo Style/ClassVars
55
+ end
56
+
57
+ def self.fuzz
58
+ @@fuzz ||= true # rubocop:todo Style/ClassVars
59
+ end
60
+
61
+ def self.fuzz=(val)
62
+ @@fuzz = val # rubocop:todo Style/ClassVars
63
+ end
64
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
4
+ require 'open_stax_transaction_retry/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'openstax_transaction_retry'
8
+ s.version = OpenStaxTransactionRetry::VERSION
9
+ s.authors = ['Nathan Stitt', 'Optimal Workshop', 'Piotr \'Qertoip\' Włodarek']
10
+ s.email = []
11
+ s.homepage = 'https://github.com/openstax/transaction_retry'
12
+ s.summary = 'Retries database transaction on deadlock and transaction serialization errors. Supports MySQL, PostgreSQL and SQLite.'
13
+ s.description = 'Retries database transaction on deadlock and transaction serialization errors. Supports MySQL, PostgreSQL and SQLite (as long as you are using new drivers mysql2, pg, sqlite3).'
14
+ s.required_ruby_version = '>= 2.6'
15
+
16
+ s.files = `git ls-files`.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', '>= 5.1'
21
+ s.add_runtime_dependency 'transaction_isolation', '>= 1.0.5'
22
+ s.metadata['rubygems_mfa_required'] = 'true'
23
+ end
data/test/db/all.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require_relative 'db'
5
+ require_relative 'migrations'
6
+ require_relative 'queued_job'
data/test/db/db.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module OpenStaxTransactionRetry
6
+ module Test
7
+ module Db
8
+ def self.connect_to_mysql2
9
+ ::ActiveRecord::Base.establish_connection(
10
+ adapter: 'mysql2',
11
+ database: 'transaction_retry_test',
12
+ username: ENV.fetch('DB_USERNAME'),
13
+ password: ENV.fetch('DB_PASSWORD', nil)
14
+ )
15
+ end
16
+
17
+ def self.connect_to_postgresql
18
+ ::ActiveRecord::Base.establish_connection(
19
+ adapter: 'postgresql',
20
+ host: ENV.fetch('DB_HOST', nil),
21
+ port: ENV.fetch('DB_PORT', nil),
22
+ database: ENV.fetch('DB_NAME', nil),
23
+ user: ENV.fetch('DB_USERNAME', nil),
24
+ password: ENV.fetch('DB_PASSWORD', nil)
25
+ )
26
+ end
27
+
28
+ def self.connect_to_sqlite3
29
+ ActiveRecord::Base.establish_connection(
30
+ adapter: 'sqlite3',
31
+ database: ':memory:',
32
+ verbosity: 'silent'
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenStaxTransactionRetry
4
+ module Test
5
+ module Migrations
6
+ def self.run!
7
+ c = ::ActiveRecord::Base.connection
8
+
9
+ # Queued Jobs
10
+
11
+ c.create_table 'queued_jobs', force: true do |t|
12
+ t.text 'job', null: false
13
+ t.integer 'status', default: 0, null: false
14
+ t.timestamps
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class QueuedJob < ActiveRecord::Base
4
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TransactionWithRetryTest < Minitest::Test
6
+ class CustomError < StandardError
7
+ end
8
+
9
+ def setup
10
+ @original_max_retries = OpenStaxTransactionRetry.max_retries
11
+ @original_wait_times = OpenStaxTransactionRetry.wait_times
12
+ @original_retry_on = OpenStaxTransactionRetry.retry_on
13
+ @original_before_retry = OpenStaxTransactionRetry.before_retry
14
+ end
15
+
16
+ def teardown
17
+ OpenStaxTransactionRetry.max_retries = @original_max_retries
18
+ OpenStaxTransactionRetry.wait_times = @original_wait_times
19
+ OpenStaxTransactionRetry.retry_on = @original_retry_on
20
+ OpenStaxTransactionRetry.before_retry = @original_before_retry
21
+ QueuedJob.delete_all
22
+ end
23
+
24
+ def test_does_not_break_transaction
25
+ ActiveRecord::Base.transaction do
26
+ QueuedJob.create!(job: 'is fun!')
27
+ assert_equal(1, QueuedJob.count)
28
+ end
29
+ assert_equal(1, QueuedJob.count)
30
+ QueuedJob.first.destroy
31
+ end
32
+
33
+ def test_does_not_break_transaction_rollback
34
+ ActiveRecord::Base.transaction do
35
+ QueuedJob.create!(job: 'gives money!')
36
+ raise ActiveRecord::Rollback
37
+ end
38
+ assert_equal(0, QueuedJob.count)
39
+ end
40
+
41
+ def test_retries_transaction_on_transaction_isolation_conflict
42
+ first_run = true
43
+
44
+ ActiveRecord::Base.transaction do
45
+ if first_run
46
+ first_run = false
47
+ message = 'Deadlock found when trying to get lock'
48
+ raise ActiveRecord::TransactionIsolationConflict, message
49
+ end
50
+ QueuedJob.create!(job: 'is cool!')
51
+ end
52
+ assert_equal(1, QueuedJob.count)
53
+
54
+ QueuedJob.first.destroy
55
+ end
56
+
57
+ def test_does_not_retry_on_unknown_error
58
+ first_run = true
59
+
60
+ assert_raises(CustomError) do
61
+ ActiveRecord::Base.transaction do
62
+ if first_run
63
+ first_run = false
64
+ raise CustomError, 'random error'
65
+ end
66
+ QueuedJob.create!(job: 'is cool!')
67
+ end
68
+ end
69
+ assert_equal(0, QueuedJob.count)
70
+ end
71
+
72
+ def test_retries_on_custom_error
73
+ first_run = true
74
+ ActiveRecord::Base.transaction(retry_on: CustomError) do
75
+ if first_run
76
+ first_run = false
77
+ raise CustomError, 'random error'
78
+ end
79
+ QueuedJob.create!(job: 'is cool!')
80
+ end
81
+ assert_equal(1, QueuedJob.count)
82
+ QueuedJob.first.destroy
83
+ end
84
+
85
+ def test_retries_on_configured_retry_on
86
+ OpenStaxTransactionRetry.retry_on = CustomError
87
+ first_run = true
88
+ ActiveRecord::Base.transaction do
89
+ if first_run
90
+ first_run = false
91
+ raise CustomError, 'random error'
92
+ end
93
+ QueuedJob.create!(job: 'is cool!')
94
+ end
95
+ assert_equal(1, QueuedJob.count)
96
+ QueuedJob.first.destroy
97
+ end
98
+
99
+ def test_retries_transaction_on_transaction_isolation_when_retry_on_set
100
+ OpenStaxTransactionRetry.retry_on = CustomError
101
+ first_run = true
102
+ ActiveRecord::Base.transaction do
103
+ if first_run
104
+ first_run = false
105
+ message = 'Deadlock found when trying to get lock'
106
+ raise ActiveRecord::TransactionIsolationConflict, message
107
+ end
108
+ QueuedJob.create!(job: 'is cool!')
109
+ end
110
+ assert_equal(1, QueuedJob.count)
111
+ QueuedJob.first.destroy
112
+ end
113
+
114
+ def test_does_not_retry_transaction_more_than_max_retries_times
115
+ OpenStaxTransactionRetry.max_retries = 1
116
+ run = 0
117
+
118
+ assert_raises(ActiveRecord::TransactionIsolationConflict) do
119
+ ActiveRecord::Base.transaction do
120
+ run += 1
121
+ message = 'Deadlock found when trying to get lock'
122
+ raise ActiveRecord::TransactionIsolationConflict, message
123
+ end
124
+ end
125
+
126
+ assert_equal(2, run) # normal run + one retry
127
+
128
+ OpenStaxTransactionRetry.max_retries = 3
129
+
130
+ run = 0
131
+
132
+ assert_raises(ActiveRecord::TransactionIsolationConflict) do
133
+ ActiveRecord::Base.transaction(max_retries: 1) do
134
+ run += 1
135
+ message = 'Deadlock found when trying to get lock'
136
+ raise ActiveRecord::TransactionIsolationConflict, message
137
+ end
138
+ end
139
+
140
+ assert_equal(2, run) # normal run + one retry
141
+ end
142
+
143
+ def test_does_not_retry_nested_transaction
144
+ first_try = true
145
+
146
+ ActiveRecord::Base.transaction do
147
+ assert_raises(ActiveRecord::TransactionIsolationConflict) do
148
+ ActiveRecord::Base.transaction(requires_new: true) do
149
+ if first_try
150
+ first_try = false
151
+ message = 'Deadlock found when trying to get lock'
152
+ raise ActiveRecord::TransactionIsolationConflict, message
153
+ end
154
+ QueuedJob.create!(job: 'is cool!')
155
+ end
156
+ end
157
+ end
158
+
159
+ assert_equal(0, QueuedJob.count)
160
+ end
161
+
162
+ def test_run_custom_lambda_before_retry
163
+ code_run = false
164
+ retry_id = nil
165
+ error_instance = nil
166
+ first_try = true
167
+ lambda_code = lambda do |retry_num, error|
168
+ code_run = true
169
+ retry_id = retry_num
170
+ error_instance = error
171
+ end
172
+
173
+ ActiveRecord::Base.transaction(before_retry: lambda_code) do
174
+ if first_try
175
+ first_try = false
176
+ raise ActiveRecord::TransactionIsolationConflict
177
+ end
178
+ QueuedJob.create!(job: 'is cool!')
179
+ end
180
+ assert_equal 1, QueuedJob.count
181
+ assert code_run
182
+ assert_equal 1, retry_id
183
+ assert_equal ActiveRecord::TransactionIsolationConflict, error_instance.class
184
+ end
185
+
186
+ def test_run_custom_global_lambda_before_retry
187
+ code_run = false
188
+ retry_id = nil
189
+ error_instance = nil
190
+ OpenStaxTransactionRetry.before_retry = lambda { |retry_num, error|
191
+ code_run = true
192
+ retry_id = retry_num
193
+ error_instance = error
194
+ }
195
+ first_try = true
196
+
197
+ ActiveRecord::Base.transaction do
198
+ if first_try
199
+ first_try = false
200
+ raise ActiveRecord::TransactionIsolationConflict
201
+ end
202
+ QueuedJob.create!(job: 'is cool!')
203
+ end
204
+ assert_equal 1, QueuedJob.count
205
+ assert code_run
206
+ assert_equal 1, retry_id
207
+ assert_equal ActiveRecord::TransactionIsolationConflict, error_instance.class
208
+ end
209
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Prepares application to be tested (requires files, connects to db, resets schema and data, applies patches, etc.)
4
+
5
+ # Initialize database
6
+ require 'db/all'
7
+
8
+ case ENV.fetch('db', 'sqlite3')
9
+ when 'mysql2'
10
+ OpenStaxTransactionRetry::Test::Db.connect_to_mysql2
11
+ when 'postgresql'
12
+ OpenStaxTransactionRetry::Test::Db.connect_to_postgresql
13
+ when 'sqlite3'
14
+ OpenStaxTransactionRetry::Test::Db.connect_to_sqlite3
15
+ else
16
+ raise "Unknown database: #{ENV.fetch('db', nil)}"
17
+ end
18
+
19
+ require 'logger'
20
+ ActiveRecord::Base.logger = Logger.new(File.expand_path("#{File.dirname(__FILE__)}/log/test.log"))
21
+
22
+ OpenStaxTransactionRetry::Test::Migrations.run!
23
+
24
+ # Load the code that will be tested
25
+ require 'open_stax_transaction_retry'
26
+
27
+ OpenStaxTransactionRetry.apply_activerecord_patch
data/test/log/.gitkeep ADDED
File without changes
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ensure that LOAD_PATH is the same as when running "rake test"; normally rake takes care of that
4
+ $LOAD_PATH << File.expand_path('.', File.dirname(__FILE__))
5
+ $LOAD_PATH << File.expand_path('./lib', File.dirname(__FILE__))
6
+ $LOAD_PATH << File.expand_path('./test', File.dirname(__FILE__))
7
+
8
+ # Boot the app
9
+ require_relative 'library_setup'
10
+
11
+ # Fire the console
12
+ require 'pry'
13
+
14
+ binding.pry # rubocop:disable Lint/Debugger
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Load test coverage tool (must be loaded before any code)
4
+ # require 'simplecov'
5
+ # SimpleCov.start do
6
+ # add_filter '/test/'
7
+ # add_filter '/config/'
8
+ # end
9
+
10
+ # Load and initialize the application to be tested
11
+ require 'library_setup'
12
+
13
+ # Load test frameworks
14
+ require 'minitest/autorun'
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ # Load all tests
6
+ Dir.glob('./**/*_test.rb').sort.each { |test_file| require test_file }
data/tests ADDED
@@ -0,0 +1,6 @@
1
+
2
+ db=mysql2 bundle exec rake
3
+
4
+ db=postgresql bundle exec rake
5
+
6
+ db=sqlite3 bundle exec rake
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: openstax_transaction_retry
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Nathan Stitt
8
+ - Optimal Workshop
9
+ - Piotr 'Qertoip' Włodarek
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2023-08-28 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activerecord
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: '5.1'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '5.1'
29
+ - !ruby/object:Gem::Dependency
30
+ name: transaction_isolation
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: 1.0.5
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 1.0.5
43
+ description: Retries database transaction on deadlock and transaction serialization
44
+ errors. Supports MySQL, PostgreSQL and SQLite (as long as you are using new drivers
45
+ mysql2, pg, sqlite3).
46
+ email: []
47
+ executables: []
48
+ extensions: []
49
+ extra_rdoc_files: []
50
+ files:
51
+ - ".github/workflows/ci.yml"
52
+ - ".gitignore"
53
+ - ".rubocop.yml"
54
+ - ".ruby-version"
55
+ - ".semaphore/semaphore.yml"
56
+ - CHANGELOG.md
57
+ - Gemfile
58
+ - LICENSE
59
+ - README.md
60
+ - Rakefile
61
+ - d
62
+ - lib/open_stax_transaction_retry.rb
63
+ - lib/open_stax_transaction_retry/active_record/base.rb
64
+ - lib/open_stax_transaction_retry/version.rb
65
+ - open_stax_transaction_retry.gemspec
66
+ - test/db/all.rb
67
+ - test/db/db.rb
68
+ - test/db/migrations.rb
69
+ - test/db/queued_job.rb
70
+ - test/integration/active_record/base/transaction_with_retry_test.rb
71
+ - test/library_setup.rb
72
+ - test/log/.gitkeep
73
+ - test/test_console.rb
74
+ - test/test_helper.rb
75
+ - test/test_runner.rb
76
+ - tests
77
+ homepage: https://github.com/openstax/transaction_retry
78
+ licenses: []
79
+ metadata:
80
+ rubygems_mfa_required: 'true'
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '2.6'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubygems_version: 3.4.7
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: Retries database transaction on deadlock and transaction serialization errors.
100
+ Supports MySQL, PostgreSQL and SQLite.
101
+ test_files: []