transaction_isolation_continued 1.0.5 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.dockerignore +1 -0
- data/.github/workflows/main.yml +35 -20
- data/.github/workflows/rubygem.yml +28 -0
- data/.gitignore +3 -0
- data/{Gemfile.local → Gemfile.old.local} +9 -0
- data/README.md +77 -56
- data/docker/ruby-2.5/Dockerfile +6 -0
- data/docker/ruby-2.7/Dockerfile +6 -0
- data/docker/ruby-3.0/Dockerfile +6 -0
- data/docker/ruby-3.1/Dockerfile +6 -0
- data/docker/test-ruby-2.5.sh +26 -0
- data/docker/test-ruby-2.7.sh +29 -0
- data/docker/test-ruby-3.0.sh +26 -0
- data/docker/test-ruby-3.1.sh +26 -0
- data/docker/test-ruby.sh +8 -0
- data/docker-compose.yml +60 -0
- data/gemfiles/Gemfile.base +4 -0
- data/gemfiles/activerecord-5.2/Gemfile.sqlite3 +1 -1
- data/gemfiles/activerecord-6.0/Gemfile.sqlite3 +1 -1
- data/gemfiles/activerecord-6.1/Gemfile.sqlite3 +1 -1
- data/gemfiles/activerecord-7.0/Gemfile.sqlite3 +1 -1
- data/lib/transaction_isolation/active_record/base.rb +5 -3
- data/lib/transaction_isolation/active_record/connection_adapters/abstract_adapter.rb +10 -9
- data/lib/transaction_isolation/active_record/connection_adapters/mysql2_adapter.rb +29 -27
- data/lib/transaction_isolation/active_record/connection_adapters/postgresql_adapter.rb +26 -24
- data/lib/transaction_isolation/active_record/connection_adapters/sqlite3_adapter.rb +24 -21
- data/lib/transaction_isolation/active_record/errors.rb +2 -0
- data/lib/transaction_isolation/configuration.rb +6 -4
- data/lib/transaction_isolation/version.rb +3 -1
- data/lib/transaction_isolation.rb +16 -11
- data/lib/transaction_isolation_continued.rb +1 -0
- data/spec/gem_template_spec.rb +11 -0
- data/spec/spec_helper.rb +15 -0
- data/test/db/all.rb +2 -0
- data/test/db/db.rb +15 -15
- data/test/db/migrations.rb +6 -7
- data/test/db/queued_job.rb +2 -0
- data/test/integration/active_record/base/isolation_level_test.rb +3 -9
- data/test/integration/active_record/connection_adapters/any_adapter/current_isolation_level_test.rb +8 -16
- data/test/integration/active_record/connection_adapters/any_adapter/current_vendor_isolation_level_test.rb +8 -16
- data/test/integration/active_record/connection_adapters/any_adapter/isolation_level_test.rb +18 -29
- data/test/integration/active_record/connection_adapters/any_adapter/supports_isolation_levels_test.rb +2 -10
- data/test/integration/active_record/connection_adapters/any_adapter/translate_exception_test.rb +20 -26
- data/test/library_setup.rb +11 -9
- data/test/test_helper.rb +9 -6
- data/test/test_runner.rb +3 -2
- data/transaction_isolation_continued.gemspec +15 -15
- metadata +33 -20
- data/d +0 -1
- data/test/test_console.rb +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f5abbf92825c1d63049fbae813f27d3df651bc4895a01626d6d01c4223ebc6ed
|
4
|
+
data.tar.gz: c589d78b41a18f1984f36e94f3eacd5f61740617a02889e6d754964c456671dd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 25c0ae13dc44a64f176d182a9874473c43bf3b98e1d29df6a1ede734a3730d8fb9604a7059417961989c2ab95e9f21f8c799bd491b03213c40b5930e90e5afd2
|
7
|
+
data.tar.gz: 9f74455b9ae024d26958f12bda07bb7c714db27ad20ae7f7f94d9bdde1a6978f2a6237aa39acfbdfc50ea5c9854d2edb057e447dd5a6bf46bb87c198a16e1bba
|
data/.dockerignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Gemfile.*.lock
|
data/.github/workflows/main.yml
CHANGED
@@ -102,7 +102,7 @@ jobs:
|
|
102
102
|
POSTGRESQL_DB_PASS: database
|
103
103
|
POSTGRESQL_DB_NAME: transaction_isolation_continued_test
|
104
104
|
steps:
|
105
|
-
- uses: actions/checkout@
|
105
|
+
- uses: actions/checkout@v4
|
106
106
|
- name: Set up Ruby
|
107
107
|
uses: ruby/setup-ruby@v1
|
108
108
|
with:
|
@@ -110,6 +110,11 @@ jobs:
|
|
110
110
|
bundler-cache: true
|
111
111
|
- name: Run bundle update
|
112
112
|
run: bundle update
|
113
|
+
- name: Setup Code Climate test-reporter
|
114
|
+
run: |
|
115
|
+
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
116
|
+
chmod +x ./cc-test-reporter
|
117
|
+
./cc-test-reporter before-build
|
113
118
|
- name: Start Mysql
|
114
119
|
if: matrix.db == 'mysql2'
|
115
120
|
run: |
|
@@ -145,27 +150,37 @@ jobs:
|
|
145
150
|
echo $(( COUNT++ )) > /dev/null
|
146
151
|
sleep 2
|
147
152
|
done
|
148
|
-
- name: test
|
149
|
-
run: echo $BUNDLE_GEMFILE
|
150
153
|
- name: Run tests
|
151
154
|
run: db=${{ matrix.db }} bundle exec rake test
|
152
155
|
- name: Shutdown database
|
153
156
|
if: always() && (matrix.db == 'postgresql' || matrix.db == 'mysql2')
|
154
157
|
run: docker stop database
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
158
|
+
- name: Format coverage
|
159
|
+
run: |
|
160
|
+
./cc-test-reporter format-coverage -t simplecov -o coverage/cc_resultset.json
|
161
|
+
- name: Upload reports' artifacts
|
162
|
+
if: success() || failure()
|
163
|
+
uses: actions/upload-artifact@v4
|
164
|
+
with:
|
165
|
+
name: coverage_artifact_ruby_${{ matrix.ruby }}_ar_${{ matrix.activerecord }}_db_${{ matrix.db }}
|
166
|
+
if-no-files-found: ignore
|
167
|
+
path: coverage
|
168
|
+
retention-days: 1
|
169
|
+
finish:
|
170
|
+
needs: test
|
171
|
+
runs-on: ubuntu-latest
|
172
|
+
steps:
|
173
|
+
- uses: actions/checkout@v4
|
174
|
+
- name: Download Code Climate test-reporter
|
175
|
+
run: |
|
176
|
+
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
177
|
+
chmod +x ./cc-test-reporter
|
178
|
+
- name: Download reports' artifacts
|
179
|
+
uses: actions/download-artifact@v4
|
180
|
+
with:
|
181
|
+
path: coverage_reports
|
182
|
+
pattern: coverage_artifact_*
|
183
|
+
- name: sum-coverage
|
184
|
+
run: |
|
185
|
+
./cc-test-reporter sum-coverage coverage_reports/coverage_artifact_ruby*/cc_resultset.json -o coverage/codeclimate.json
|
186
|
+
./cc-test-reporter upload-coverage -r ${{secrets.CC_TEST_REPORTER_ID}} -i coverage/codeclimate.json
|
@@ -0,0 +1,28 @@
|
|
1
|
+
name: Ruby Gem
|
2
|
+
|
3
|
+
on:
|
4
|
+
workflow_dispatch:
|
5
|
+
push:
|
6
|
+
branches: [ main ]
|
7
|
+
|
8
|
+
jobs:
|
9
|
+
build:
|
10
|
+
name: Build + Publish
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
steps:
|
13
|
+
- uses: actions/checkout@v3
|
14
|
+
- name: Set up Ruby
|
15
|
+
uses: ruby/setup-ruby@v1
|
16
|
+
with:
|
17
|
+
ruby-version: 2.7
|
18
|
+
bundler-cache: true
|
19
|
+
- name: Publish to RubyGems
|
20
|
+
run: |
|
21
|
+
mkdir -p $HOME/.gem
|
22
|
+
touch $HOME/.gem/credentials
|
23
|
+
chmod 0600 $HOME/.gem/credentials
|
24
|
+
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
25
|
+
gem build *.gemspec
|
26
|
+
gem push *.gem
|
27
|
+
env:
|
28
|
+
GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
|
data/.gitignore
CHANGED
@@ -11,6 +11,15 @@ platform :ruby do
|
|
11
11
|
gem "pg"
|
12
12
|
end
|
13
13
|
|
14
|
+
gem 'ruby2_keywords' if RUBY_VERSION < '2.7'
|
15
|
+
|
16
|
+
group :test do
|
17
|
+
gem 'simplecov'
|
18
|
+
gem 'simplecov_json_formatter'
|
19
|
+
end
|
20
|
+
|
21
|
+
gem 'rubocop', require: false
|
22
|
+
|
14
23
|
platform :jruby do
|
15
24
|
gem 'activerecord-jdbcpostgresql-adapter'
|
16
25
|
gem 'activerecord-jdbcmysql-adapter'
|
data/README.md
CHANGED
@@ -1,104 +1,125 @@
|
|
1
|
-
|
1
|
+
[![Gem Version](https://badge.fury.io/rb/transaction_isolation_continued.svg)](https://badge.fury.io/rb/transaction_isolation_continued)
|
2
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/01a5d17010b32f041ac3/maintainability)](https://codeclimate.com/github/iagopiimenta/transaction_isolation_continued/maintainability)
|
3
|
+
[![Test Coverage](https://api.codeclimate.com/v1/badges/01a5d17010b32f041ac3/test_coverage)](https://codeclimate.com/github/iagopiimenta/transaction_isolation_continued/test_coverage)
|
4
|
+
[![CI PR Builds](https://github.com/iagopiimenta/transaction_isolation_continued/actions/workflows/main.yml/badge.svg)](https://github.com/iagopiimenta/transaction_isolation_continued/actions/workflows/main.yml)
|
2
5
|
|
3
|
-
|
4
|
-
# transaction_isolation
|
6
|
+
# transaction_isolation_continued
|
5
7
|
|
6
8
|
Set transaction isolation level in the ActiveRecord in a database agnostic way.
|
7
9
|
Works with MySQL, PostgreSQL and SQLite as long as you are using new adapters mysql2, pg or sqlite3.
|
8
|
-
Supports all ANSI SQL isolation levels:
|
10
|
+
Supports all ANSI SQL isolation levels: `:serializable`, `:repeatable_read`, `:read_committed`, `:read_uncommitted`.
|
9
11
|
|
10
12
|
See also [transaction_retry](https://github.com/qertoip/transaction_retry) gem for auto-retrying transactions
|
11
13
|
on deadlocks and serialization errors.
|
12
14
|
|
13
15
|
## Example
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
-
|
17
|
+
```ruby
|
18
|
+
ActiveRecord::Base.isolation_level(:serializable) do
|
19
|
+
# your code
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
## Requirements
|
24
|
+
|
25
|
+
- Rails: ActiveRecord 5.2+.
|
26
|
+
- Database: MySQL(compatible with mysql 5 and 8), PostgreSQL, SQLite.
|
27
|
+
- Ruby: MRI 2.5+.
|
18
28
|
|
19
29
|
## Installation
|
20
30
|
|
21
31
|
Add this to your Gemfile:
|
22
32
|
|
23
|
-
|
33
|
+
```ruby
|
34
|
+
gem 'transaction_isolation'
|
35
|
+
```
|
24
36
|
|
25
37
|
Then run:
|
26
38
|
|
27
|
-
|
39
|
+
```bash
|
40
|
+
bundle
|
41
|
+
```
|
28
42
|
|
29
|
-
|
43
|
+
**It works out of the box with Ruby on Rails**.
|
30
44
|
|
31
45
|
If you have a standalone ActiveRecord-based project you'll need to call:
|
32
46
|
|
33
|
-
|
47
|
+
```ruby
|
48
|
+
TransactionIsolation.apply_activerecord_patch # after connecting to the database
|
49
|
+
```
|
34
50
|
|
35
|
-
|
51
|
+
**after** connecting to the database. This is because ActiveRecord loads adapters lazilly and only then they can be patched.
|
36
52
|
|
37
53
|
## Features
|
38
54
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
## Testimonials
|
47
|
-
|
48
|
-
This gem was initially developed for and successfully works in production at [Kontomierz.pl](http://kontomierz.pl) - the finest Polish personal finance app.
|
55
|
+
- Setting transaction isolation level: `:serializable`, `:repeatable_read`, `:read_committed`, `:read_uncommitted`
|
56
|
+
- Auto-reverting to the original isolation level after the block
|
57
|
+
- Database agnostic
|
58
|
+
- MySQL, PostgreSQL and SQLite supported
|
59
|
+
- Exception translation. All deadlocks and serialization errors are wrapped in a `ActiveRecord::TransactionIsolationConflict` exception
|
60
|
+
- Use it in your Rails application or a standalone ActiveRecord-based project
|
49
61
|
|
50
62
|
## Real world example
|
51
63
|
|
52
64
|
When implementing a table-based job queue you should ensure that only one worker process can pop a particular job from the queue.
|
65
|
+
|
53
66
|
Wrapping your code in a transaction is not enough because by default databases do not isolate transactions to the full extent,
|
54
|
-
which leads to occasional phantom reads.
|
67
|
+
which leads to occasional phantom reads.
|
68
|
+
|
69
|
+
It is therefore necessary to manually raise the transaction isolation level.
|
70
|
+
|
55
71
|
The highest level of transaction isolation is called "serializable" and that's what we need here:
|
56
72
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
end
|
75
|
-
end
|
73
|
+
```ruby
|
74
|
+
class QueuedJob < ActiveRecord::Base
|
75
|
+
# Job status
|
76
|
+
TODO = 1
|
77
|
+
PROCESSING = 2
|
78
|
+
DONE = 3
|
79
|
+
|
80
|
+
# Returns first job from the queue or nil if the queue is empty
|
81
|
+
def pop
|
82
|
+
QueuedJob.isolation_level(:serializable) do
|
83
|
+
QueuedJob.transaction do
|
84
|
+
queued_job = find_by_status(TODO)
|
85
|
+
if queud_job
|
86
|
+
queued_job.update_attribute(:status, PROCESSING)
|
87
|
+
return queued_job
|
88
|
+
else
|
89
|
+
return nil
|
76
90
|
end
|
77
|
-
rescue ActiveRecord::TransactionIsolationConflict => e
|
78
|
-
logger.warn( e.message )
|
79
|
-
retry
|
80
91
|
end
|
81
|
-
|
82
92
|
end
|
93
|
+
rescue ActiveRecord::TransactionIsolationConflict => e
|
94
|
+
logger.warn(e.message)
|
95
|
+
retry
|
96
|
+
end
|
97
|
+
end
|
98
|
+
```
|
83
99
|
|
84
100
|
[Read more about isolation levels in Wikipedia](http://tinyurl.com/nrqjbb)
|
85
101
|
|
86
|
-
## Requirements
|
87
|
-
|
88
|
-
* Ruby 1.9.2
|
89
|
-
* ActiveRecord 3.0.11+
|
90
|
-
|
91
102
|
## Running tests
|
92
103
|
|
93
104
|
Run tests on the selected database (mysql2 by default):
|
94
105
|
|
95
|
-
|
96
|
-
|
97
|
-
|
106
|
+
```bash
|
107
|
+
# passing desired database, active record version and ruby version
|
108
|
+
docker compose run -e db=sqlite3 -e BUNDLE_GEMFILE=gemfiles/activerecord-7.0/Gemfile.sqlite3 ruby_2_7 bash -c ./docker/test-ruby.sh
|
109
|
+
|
110
|
+
# db options: mysql2, postgresql, sqlite3
|
111
|
+
# active record version options: 5.2, 6.0, 6.1, 7.0
|
112
|
+
# ruby version options: 2.5, 2.7, 3.0, 3.1
|
113
|
+
```
|
98
114
|
|
99
|
-
Run tests on all supported databases:
|
115
|
+
Run tests on all supported databases by ruby version:
|
100
116
|
|
101
|
-
|
117
|
+
```bash
|
118
|
+
docker compose up ruby_2_5
|
119
|
+
docker compose up ruby_2_7
|
120
|
+
docker compose up ruby_3_0
|
121
|
+
docker compose up ruby_3_1
|
122
|
+
```
|
102
123
|
|
103
124
|
Database configuration is hardcoded in test/db/db.rb; feel free to improve this and submit a pull request.
|
104
125
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
# fix conflicts between different ruby versions
|
4
|
+
rm -rf gemfiles/**/Gemfile.*.lock
|
5
|
+
|
6
|
+
gemfiles=(
|
7
|
+
"gemfiles/activerecord-5.2/Gemfile.sqlite3"
|
8
|
+
"gemfiles/activerecord-6.0/Gemfile.sqlite3"
|
9
|
+
"gemfiles/activerecord-6.1/Gemfile.sqlite3"
|
10
|
+
|
11
|
+
"gemfiles/activerecord-5.2/Gemfile.mysql2"
|
12
|
+
"gemfiles/activerecord-6.0/Gemfile.mysql2"
|
13
|
+
"gemfiles/activerecord-6.1/Gemfile.mysql2"
|
14
|
+
|
15
|
+
"gemfiles/activerecord-5.2/Gemfile.postgresql"
|
16
|
+
"gemfiles/activerecord-6.0/Gemfile.postgresql"
|
17
|
+
"gemfiles/activerecord-6.1/Gemfile.postgresql"
|
18
|
+
)
|
19
|
+
|
20
|
+
for gemfile in "${gemfiles[@]}"; do
|
21
|
+
database=$(echo $gemfile | cut -d'.' -f3)
|
22
|
+
|
23
|
+
echo BUNDLE_GEMFILE=$gemfile db=$database bundle exec rake test
|
24
|
+
BUNDLE_GEMFILE=$gemfile bundle install
|
25
|
+
BUNDLE_GEMFILE=$gemfile db=$database bundle exec rake test
|
26
|
+
done
|
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
# fix conflicts between different ruby versions
|
4
|
+
rm -rf gemfiles/**/Gemfile.*.lock
|
5
|
+
|
6
|
+
gemfiles=(
|
7
|
+
"gemfiles/activerecord-5.2/Gemfile.sqlite3"
|
8
|
+
"gemfiles/activerecord-6.0/Gemfile.sqlite3"
|
9
|
+
"gemfiles/activerecord-6.1/Gemfile.sqlite3"
|
10
|
+
"gemfiles/activerecord-7.0/Gemfile.sqlite3"
|
11
|
+
|
12
|
+
"gemfiles/activerecord-5.2/Gemfile.mysql2"
|
13
|
+
"gemfiles/activerecord-6.0/Gemfile.mysql2"
|
14
|
+
"gemfiles/activerecord-6.1/Gemfile.mysql2"
|
15
|
+
"gemfiles/activerecord-7.0/Gemfile.mysql2"
|
16
|
+
|
17
|
+
"gemfiles/activerecord-5.2/Gemfile.postgresql"
|
18
|
+
"gemfiles/activerecord-6.0/Gemfile.postgresql"
|
19
|
+
"gemfiles/activerecord-6.1/Gemfile.postgresql"
|
20
|
+
"gemfiles/activerecord-7.0/Gemfile.postgresql"
|
21
|
+
)
|
22
|
+
|
23
|
+
for gemfile in "${gemfiles[@]}"; do
|
24
|
+
database=$(echo $gemfile | cut -d'.' -f3)
|
25
|
+
|
26
|
+
echo BUNDLE_GEMFILE=$gemfile db=$database bundle exec rake test
|
27
|
+
BUNDLE_GEMFILE=$gemfile bundle install
|
28
|
+
BUNDLE_GEMFILE=$gemfile db=$database bundle exec rake test
|
29
|
+
done
|
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
# fix conflicts between different ruby versions
|
4
|
+
rm -rf gemfiles/**/Gemfile.*.lock
|
5
|
+
|
6
|
+
gemfiles=(
|
7
|
+
"gemfiles/activerecord-6.0/Gemfile.sqlite3"
|
8
|
+
"gemfiles/activerecord-6.1/Gemfile.sqlite3"
|
9
|
+
"gemfiles/activerecord-7.0/Gemfile.sqlite3"
|
10
|
+
|
11
|
+
"gemfiles/activerecord-6.0/Gemfile.mysql2"
|
12
|
+
"gemfiles/activerecord-6.1/Gemfile.mysql2"
|
13
|
+
"gemfiles/activerecord-7.0/Gemfile.mysql2"
|
14
|
+
|
15
|
+
"gemfiles/activerecord-6.0/Gemfile.postgresql"
|
16
|
+
"gemfiles/activerecord-6.1/Gemfile.postgresql"
|
17
|
+
"gemfiles/activerecord-7.0/Gemfile.postgresql"
|
18
|
+
)
|
19
|
+
|
20
|
+
for gemfile in "${gemfiles[@]}"; do
|
21
|
+
database=$(echo $gemfile | cut -d'.' -f3)
|
22
|
+
|
23
|
+
echo BUNDLE_GEMFILE=$gemfile db=$database bundle exec rake test
|
24
|
+
BUNDLE_GEMFILE=$gemfile bundle install
|
25
|
+
BUNDLE_GEMFILE=$gemfile db=$database bundle exec rake test
|
26
|
+
done
|
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
# fix conflicts between different ruby versions
|
4
|
+
rm -rf gemfiles/**/Gemfile.*.lock
|
5
|
+
|
6
|
+
gemfiles=(
|
7
|
+
"gemfiles/activerecord-6.0/Gemfile.sqlite3"
|
8
|
+
"gemfiles/activerecord-6.1/Gemfile.sqlite3"
|
9
|
+
"gemfiles/activerecord-7.0/Gemfile.sqlite3"
|
10
|
+
|
11
|
+
"gemfiles/activerecord-6.0/Gemfile.mysql2"
|
12
|
+
"gemfiles/activerecord-6.1/Gemfile.mysql2"
|
13
|
+
"gemfiles/activerecord-7.0/Gemfile.mysql2"
|
14
|
+
|
15
|
+
"gemfiles/activerecord-6.0/Gemfile.postgresql"
|
16
|
+
"gemfiles/activerecord-6.1/Gemfile.postgresql"
|
17
|
+
"gemfiles/activerecord-7.0/Gemfile.postgresql"
|
18
|
+
)
|
19
|
+
|
20
|
+
for gemfile in "${gemfiles[@]}"; do
|
21
|
+
database=$(echo $gemfile | cut -d'.' -f3)
|
22
|
+
|
23
|
+
echo BUNDLE_GEMFILE=$gemfile db=$database bundle exec rake test
|
24
|
+
BUNDLE_GEMFILE=$gemfile bundle install
|
25
|
+
BUNDLE_GEMFILE=$gemfile db=$database bundle exec rake test
|
26
|
+
done
|
data/docker/test-ruby.sh
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
# fix conflicts between different ruby versions
|
4
|
+
rm -rf gemfiles/**/Gemfile.*.lock
|
5
|
+
|
6
|
+
echo BUNDLE_GEMFILE=$BUNDLE_GEMFILE db=$db bundle exec rake test
|
7
|
+
BUNDLE_GEMFILE=$BUNDLE_GEMFILE bundle install
|
8
|
+
BUNDLE_GEMFILE=$BUNDLE_GEMFILE db=$db bundle exec rake test
|
data/docker-compose.yml
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
services:
|
2
|
+
ruby_2_5: &ruby_2_5
|
3
|
+
working_dir: /app
|
4
|
+
build:
|
5
|
+
context: .
|
6
|
+
dockerfile: docker/ruby-2.5/Dockerfile
|
7
|
+
volumes:
|
8
|
+
- .:/app
|
9
|
+
command: bash -c "bundle install && bundle exec rake test"
|
10
|
+
environment:
|
11
|
+
POSTGRESQL_DB_HOST: postgres
|
12
|
+
POSTGRESQL_DB_USER: transaction_retry_continued
|
13
|
+
POSTGRESQL_DB_PASS: database
|
14
|
+
POSTGRESQL_DB_NAME: transaction_isolation_continued_test
|
15
|
+
MYSQL_DB_HOST: mysql
|
16
|
+
MYSQL_DB_USER: root
|
17
|
+
MYSQL_DB_PASS: database
|
18
|
+
MYSQL_DB_NAME: transaction_isolation_continued_test
|
19
|
+
depends_on:
|
20
|
+
- mysql
|
21
|
+
- postgres
|
22
|
+
ruby_2_7:
|
23
|
+
<<: *ruby_2_5
|
24
|
+
build:
|
25
|
+
context: .
|
26
|
+
dockerfile: docker/ruby-2.7/Dockerfile
|
27
|
+
ruby_3_0:
|
28
|
+
<<: *ruby_2_5
|
29
|
+
command: ./docker/test-ruby-3.0.sh
|
30
|
+
build:
|
31
|
+
context: .
|
32
|
+
dockerfile: docker/ruby-3.0/Dockerfile
|
33
|
+
ruby_3_1:
|
34
|
+
<<: *ruby_2_5
|
35
|
+
command: ./docker/test-ruby-3.1.sh
|
36
|
+
build:
|
37
|
+
context: .
|
38
|
+
dockerfile: docker/ruby-3.1/Dockerfile
|
39
|
+
postgres:
|
40
|
+
image: postgres:9.6
|
41
|
+
environment:
|
42
|
+
POSTGRES_USER: transaction_retry_continued
|
43
|
+
POSTGRES_PASSWORD: database
|
44
|
+
POSTGRES_DB: transaction_isolation_continued_test
|
45
|
+
ports:
|
46
|
+
- "5432:5432"
|
47
|
+
healthcheck:
|
48
|
+
test: [ "CMD-SHELL", "pg_isready -q" ]
|
49
|
+
interval: 5s
|
50
|
+
timeout: 5s
|
51
|
+
retries: 5
|
52
|
+
mysql:
|
53
|
+
image: biarms/mysql:5.7
|
54
|
+
environment:
|
55
|
+
MYSQL_ROOT_PASSWORD: database
|
56
|
+
MYSQL_DATABASE: transaction_isolation_continued_test
|
57
|
+
ports:
|
58
|
+
- "3306:3306"
|
59
|
+
healthcheck:
|
60
|
+
test: "mysqladmin ping --host=0.0.0.0 --password=database --silent"
|
data/gemfiles/Gemfile.base
CHANGED
@@ -3,4 +3,8 @@ gemspec path: File.expand_path('..', __FILE__)
|
|
3
3
|
|
4
4
|
gem 'ruby2_keywords' if RUBY_VERSION < '2.7'
|
5
5
|
|
6
|
+
group :test do
|
7
|
+
gem 'simplecov'
|
8
|
+
end
|
9
|
+
|
6
10
|
File.exist?(gemfile_local = File.expand_path('../Gemfile.local', __FILE__)) and eval File.read(gemfile_local), binding, gemfile_local
|
@@ -1,13 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_record/base'
|
2
4
|
|
3
5
|
module TransactionIsolation
|
4
6
|
module ActiveRecord
|
5
7
|
module Base
|
6
|
-
def isolation_level(
|
7
|
-
connection.isolation_level(
|
8
|
+
def isolation_level(isolation_level, &block)
|
9
|
+
connection.isolation_level(isolation_level, &block)
|
8
10
|
end
|
9
11
|
end
|
10
12
|
end
|
11
13
|
end
|
12
14
|
|
13
|
-
ActiveRecord::Base.extend(
|
15
|
+
ActiveRecord::Base.extend(TransactionIsolation::ActiveRecord::Base)
|
@@ -1,32 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_record/connection_adapters/abstract_adapter'
|
2
4
|
|
3
5
|
module TransactionIsolation
|
4
6
|
module ActiveRecord
|
5
7
|
module ConnectionAdapters # :nodoc:
|
6
8
|
module AbstractAdapter
|
7
|
-
|
8
|
-
VALID_ISOLATION_LEVELS = [:read_uncommitted, :read_committed, :repeatable_read, :serializable]
|
9
|
+
VALID_ISOLATION_LEVELS = %i[read_uncommitted read_committed repeatable_read serializable].freeze
|
9
10
|
|
10
11
|
# If true, #isolation_level(level) method is available
|
11
12
|
def supports_isolation_levels?
|
12
13
|
false
|
13
14
|
end
|
14
15
|
|
15
|
-
def isolation_level(
|
16
|
+
def isolation_level(level)
|
16
17
|
raise NotImplementedError
|
17
18
|
end
|
18
19
|
|
19
20
|
private
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
raise ArgumentError, "Invalid isolation level '#{isolation_level}'. Supported levels include #{VALID_ISOLATION_LEVELS.join( ', ' )}."
|
24
|
-
end
|
25
|
-
end
|
22
|
+
def validate_isolation_level(isolation_level)
|
23
|
+
return if VALID_ISOLATION_LEVELS.include?(isolation_level)
|
26
24
|
|
25
|
+
raise ArgumentError,
|
26
|
+
"Invalid isolation level '#{isolation_level}'. Supported levels include #{VALID_ISOLATION_LEVELS.join(', ')}."
|
27
|
+
end
|
27
28
|
end
|
28
29
|
end
|
29
30
|
end
|
30
31
|
end
|
31
32
|
|
32
|
-
ActiveRecord::ConnectionAdapters::AbstractAdapter.
|
33
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter.include TransactionIsolation::ActiveRecord::ConnectionAdapters::AbstractAdapter
|