openstax_transaction_retry 1.2.0

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.
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: []