transaction_isolation_continued 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +171 -0
  3. data/.gitignore +6 -0
  4. data/Gemfile +8 -0
  5. data/Gemfile.local +18 -0
  6. data/LICENSE +19 -0
  7. data/README.md +118 -0
  8. data/Rakefile +11 -0
  9. data/d +1 -0
  10. data/gemfiles/Gemfile.base +6 -0
  11. data/gemfiles/activerecord-5.2/Gemfile.base +4 -0
  12. data/gemfiles/activerecord-5.2/Gemfile.mysql2 +10 -0
  13. data/gemfiles/activerecord-5.2/Gemfile.postgresql +10 -0
  14. data/gemfiles/activerecord-5.2/Gemfile.sqlite3 +10 -0
  15. data/gemfiles/activerecord-6.0/Gemfile.base +4 -0
  16. data/gemfiles/activerecord-6.0/Gemfile.mysql2 +10 -0
  17. data/gemfiles/activerecord-6.0/Gemfile.postgresql +10 -0
  18. data/gemfiles/activerecord-6.0/Gemfile.sqlite3 +10 -0
  19. data/gemfiles/activerecord-6.1/Gemfile.base +4 -0
  20. data/gemfiles/activerecord-6.1/Gemfile.mysql2 +10 -0
  21. data/gemfiles/activerecord-6.1/Gemfile.postgresql +10 -0
  22. data/gemfiles/activerecord-6.1/Gemfile.sqlite3 +10 -0
  23. data/gemfiles/activerecord-7.0/Gemfile.base +4 -0
  24. data/gemfiles/activerecord-7.0/Gemfile.mysql2 +10 -0
  25. data/gemfiles/activerecord-7.0/Gemfile.postgresql +10 -0
  26. data/gemfiles/activerecord-7.0/Gemfile.sqlite3 +11 -0
  27. data/lib/transaction_isolation/active_record/base.rb +13 -0
  28. data/lib/transaction_isolation/active_record/connection_adapters/abstract_adapter.rb +32 -0
  29. data/lib/transaction_isolation/active_record/connection_adapters/mysql2_adapter.rb +83 -0
  30. data/lib/transaction_isolation/active_record/connection_adapters/postgresql_adapter.rb +81 -0
  31. data/lib/transaction_isolation/active_record/connection_adapters/sqlite3_adapter.rb +80 -0
  32. data/lib/transaction_isolation/active_record/errors.rb +10 -0
  33. data/lib/transaction_isolation/configuration.rb +19 -0
  34. data/lib/transaction_isolation/version.rb +3 -0
  35. data/lib/transaction_isolation.rb +51 -0
  36. data/test/db/all.rb +4 -0
  37. data/test/db/db.rb +37 -0
  38. data/test/db/migrations.rb +20 -0
  39. data/test/db/queued_job.rb +2 -0
  40. data/test/integration/active_record/base/isolation_level_test.rb +23 -0
  41. data/test/integration/active_record/connection_adapters/any_adapter/current_isolation_level_test.rb +33 -0
  42. data/test/integration/active_record/connection_adapters/any_adapter/current_vendor_isolation_level_test.rb +33 -0
  43. data/test/integration/active_record/connection_adapters/any_adapter/isolation_level_test.rb +69 -0
  44. data/test/integration/active_record/connection_adapters/any_adapter/supports_isolation_levels_test.rb +23 -0
  45. data/test/integration/active_record/connection_adapters/any_adapter/translate_exception_test.rb +46 -0
  46. data/test/library_setup.rb +25 -0
  47. data/test/log/.gitkeep +0 -0
  48. data/test/test_console.rb +11 -0
  49. data/test/test_helper.rb +12 -0
  50. data/test/test_runner.rb +4 -0
  51. data/tests +6 -0
  52. data/transaction_isolation_continued.gemspec +25 -0
  53. metadata +154 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 32f804fa89e4072268ea34713263099072269395caf1bca34528d1632ac37a33
4
+ data.tar.gz: 9fa045df1dd64b0e45fb74eb8e507f7ff0aa2376e61843a203bec64dfaf8a5e2
5
+ SHA512:
6
+ metadata.gz: 56a421307a4fd2a8c66bfa35e9189b99c160cd8db82fc5103a5d6c7ead76a0a8699cc3f2f089d79c521fa0d8b475129530d7aeaab7c9e4ac77f714f6dc550cdd
7
+ data.tar.gz: 66f6c3dd3ee7a0fd8ecad4292cfdf13ae563dcd5afdacada47030bbdd93c5c11917f3fac07caa62ec67b466a30677d95b7595d96c5fd384021f9fccc7290458e
@@ -0,0 +1,171 @@
1
+ name: CI PR Builds
2
+ 'on':
3
+ push:
4
+ branches:
5
+ - master
6
+ pull_request:
7
+ concurrency:
8
+ group: ci-${{ github.ref }}
9
+ cancel-in-progress: true
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ ruby:
17
+ - '2.5'
18
+ - '2.7'
19
+ - '3.0'
20
+ - '3.1'
21
+ activerecord:
22
+ - '5.2'
23
+ - '6.0'
24
+ - '6.1'
25
+ - '7.0'
26
+ db:
27
+ - mysql2
28
+ - sqlite3
29
+ - skip
30
+ dbversion:
31
+ - skip
32
+ exclude:
33
+ - ruby: '3.0'
34
+ activerecord: '5.2'
35
+ - ruby: '3.1'
36
+ activerecord: '5.2'
37
+ - ruby: '2.5'
38
+ activerecord: '7.0'
39
+ - db: skip
40
+ dbversion: skip
41
+ include:
42
+ - ruby: '2.5'
43
+ activerecord: '5.2'
44
+ db: postgresql
45
+ dbversion: '9.6'
46
+ - ruby: '2.5'
47
+ activerecord: '6.0'
48
+ db: postgresql
49
+ dbversion: '9.6'
50
+ - ruby: '2.5'
51
+ activerecord: '6.1'
52
+ db: postgresql
53
+ dbversion: '9.6'
54
+ - ruby: '2.7'
55
+ activerecord: '5.2'
56
+ db: postgresql
57
+ dbversion: '9.6'
58
+ - ruby: '2.7'
59
+ activerecord: '6.0'
60
+ db: postgresql
61
+ dbversion: '9.6'
62
+ - ruby: '2.7'
63
+ activerecord: '6.1'
64
+ db: postgresql
65
+ dbversion: '9.6'
66
+ - ruby: '2.7'
67
+ activerecord: '7.0'
68
+ db: postgresql
69
+ dbversion: '9.6'
70
+ - ruby: '3.0'
71
+ activerecord: '6.0'
72
+ db: postgresql
73
+ dbversion: '9.6'
74
+ - ruby: '3.0'
75
+ activerecord: '6.1'
76
+ db: postgresql
77
+ dbversion: '9.6'
78
+ - ruby: '3.0'
79
+ activerecord: '7.0'
80
+ db: postgresql
81
+ dbversion: '9.6'
82
+ - ruby: '3.1'
83
+ activerecord: '6.0'
84
+ db: postgresql
85
+ dbversion: '9.6'
86
+ - ruby: '3.1'
87
+ activerecord: '6.1'
88
+ db: postgresql
89
+ dbversion: '9.6'
90
+ - ruby: '3.1'
91
+ activerecord: '7.0'
92
+ db: postgresql
93
+ dbversion: '9.6'
94
+ env:
95
+ BUNDLE_GEMFILE: "${{ github.workspace }}/gemfiles/activerecord-${{ matrix.activerecord }}/Gemfile.${{ matrix.db }}"
96
+ MYSQL_DB_HOST: 127.0.0.1
97
+ MYSQL_DB_USER: root
98
+ MYSQL_DB_PASS: database
99
+ MYSQL_DB_NAME: transaction_isolation_continued_test
100
+ POSTGRESQL_DB_HOST: 127.0.0.1
101
+ POSTGRESQL_DB_USER: transaction_retry_continued
102
+ POSTGRESQL_DB_PASS: database
103
+ POSTGRESQL_DB_NAME: transaction_isolation_continued_test
104
+ steps:
105
+ - uses: actions/checkout@v2
106
+ - name: Set up Ruby
107
+ uses: ruby/setup-ruby@v1
108
+ with:
109
+ ruby-version: "${{ matrix.ruby }}"
110
+ bundler-cache: true
111
+ - name: Run bundle update
112
+ run: bundle update
113
+ - name: Start Mysql
114
+ if: matrix.db == 'mysql2'
115
+ run: |
116
+ docker run --rm --detach \
117
+ -e MYSQL_ROOT_PASSWORD=$MYSQL_DB_PASS \
118
+ -e MYSQL_DATABASE=$MYSQL_DB_NAME \
119
+ -p 3306:3306 \
120
+ --health-cmd "mysqladmin ping --host=127.0.0.1 --password=$MYSQL_DB_PASS --silent" \
121
+ --health-interval 5s \
122
+ --health-timeout 5s \
123
+ --health-retries 5 \
124
+ --name database mysql:5.6
125
+ - name: Start Postgresql
126
+ if: matrix.db == 'postgresql'
127
+ run: |
128
+ docker run --rm --detach \
129
+ -e POSTGRES_USER=$POSTGRESQL_DB_USER \
130
+ -e POSTGRES_PASSWORD=$POSTGRESQL_DB_PASS \
131
+ -e POSTGRES_DB=$POSTGRESQL_DB_NAME \
132
+ -p 5432:5432 \
133
+ --health-cmd "pg_isready -q" \
134
+ --health-interval 5s \
135
+ --health-timeout 5s \
136
+ --health-retries 5 \
137
+ --name database postgres:${{ matrix.dbversion }}
138
+ - name: Wait for database to start
139
+ if: "(matrix.db == 'postgresql' || matrix.db == 'mysql2')"
140
+ run: |
141
+ COUNT=0
142
+ ATTEMPTS=20
143
+ until [[ $COUNT -eq $ATTEMPTS ]]; do
144
+ [ "$(docker inspect -f {{.State.Health.Status}} database)" == "healthy" ] && break
145
+ echo $(( COUNT++ )) > /dev/null
146
+ sleep 2
147
+ done
148
+ - name: test
149
+ run: echo $BUNDLE_GEMFILE
150
+ - name: Run tests
151
+ run: db=${{ matrix.db }} bundle exec rake test
152
+ - name: Shutdown database
153
+ if: always() && (matrix.db == 'postgresql' || matrix.db == 'mysql2')
154
+ run: docker stop database
155
+ # - name: Coveralls Parallel
156
+ # if: "${{ !env.ACT }}"
157
+ # uses: coverallsapp/github-action@master
158
+ # with:
159
+ # github-token: "${{ secrets.GITHUB_TOKEN }}"
160
+ # flag-name: run-${{ matrix.ruby }}-${{ matrix.activerecord }}-${{ matrix.db }}-${{ matrix.dbversion }}
161
+ # parallel: true
162
+ # finish:
163
+ # needs: test
164
+ # runs-on: ubuntu-latest
165
+ # steps:
166
+ # - name: Coveralls Finished
167
+ # if: "${{ !env.ACT }}"
168
+ # uses: coverallsapp/github-action@master
169
+ # with:
170
+ # github-token: "${{ secrets.GITHUB_TOKEN }}"
171
+ # parallel-finished: true
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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gemfile_local = File.expand_path '../Gemfile.local', __FILE__
8
+ eval File.read(gemfile_local), binding, gemfile_local if File.exist? gemfile_local
data/Gemfile.local ADDED
@@ -0,0 +1,18 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem "rake"
6
+
7
+ platform :ruby do
8
+ # https://github.com/rails/rails/pull/51592
9
+ gem "sqlite3", "~> 1.4"
10
+ gem "mysql2"
11
+ gem "pg"
12
+ end
13
+
14
+ platform :jruby do
15
+ gem 'activerecord-jdbcpostgresql-adapter'
16
+ gem 'activerecord-jdbcmysql-adapter'
17
+ gem 'activerecord-jdbcsqlite3-adapter', '>=1.3.0.beta2'
18
+ 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,118 @@
1
+ NO LONGER MAINTAINED - PLEASE FORK OR SEEK OTHER SOLUTIONS
2
+
3
+
4
+ # transaction_isolation
5
+
6
+ Set transaction isolation level in the ActiveRecord in a database agnostic way.
7
+ 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: :serializable, :repeatable_read, :read_committed, :read_uncommitted.
9
+
10
+ See also [transaction_retry](https://github.com/qertoip/transaction_retry) gem for auto-retrying transactions
11
+ on deadlocks and serialization errors.
12
+
13
+ ## Example
14
+
15
+ ActiveRecord::Base.isolation_level( :serializable ) do
16
+ # your code
17
+ end
18
+
19
+ ## Installation
20
+
21
+ Add this to your Gemfile:
22
+
23
+ gem 'transaction_isolation'
24
+
25
+ Then run:
26
+
27
+ bundle
28
+
29
+ __It works out of the box with Ruby on Rails__.
30
+
31
+ If you have a standalone ActiveRecord-based project you'll need to call:
32
+
33
+ TransactionIsolation.apply_activerecord_patch # after connecting to the database
34
+
35
+ __after__ connecting to the database. This is because ActiveRecord loads adapters lazilly and only then they can be patched.
36
+
37
+ ## Features
38
+
39
+ * Setting transaction isolation level: :serializable, :repeatable_read, :read_committed, :read_uncommitted
40
+ * Auto-reverting to the original isolation level after the block
41
+ * Database agnostic
42
+ * MySQL, PostgreSQL and SQLite supported
43
+ * Exception translation. All deadlocks and serialization errors are wrapped in a ActiveRecord::TransactionIsolationConflict exception
44
+ * Use it in your Rails application or a standalone ActiveRecord-based project
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.
49
+
50
+ ## Real world example
51
+
52
+ When implementing a table-based job queue you should ensure that only one worker process can pop a particular job from the queue.
53
+ 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. It is therefore necessary to manually raise the transaction isolation level.
55
+ The highest level of transaction isolation is called "serializable" and that's what we need here:
56
+
57
+ class QueuedJob < ActiveRecord::Base
58
+
59
+ # Job status
60
+ TODO = 1
61
+ PROCESSING = 2
62
+ DONE = 3
63
+
64
+ # Returns first job from the queue or nil if the queue is empty
65
+ def pop
66
+ QueuedJob.isolation_level( :serializable ) do
67
+ QueuedJob.transaction do
68
+ queued_job = find_by_status( TODO )
69
+ if queud_job
70
+ queued_job.update_attribute( :status, PROCESSING )
71
+ return queued_job
72
+ else
73
+ return nil
74
+ end
75
+ end
76
+ end
77
+ rescue ActiveRecord::TransactionIsolationConflict => e
78
+ logger.warn( e.message )
79
+ retry
80
+ end
81
+
82
+ end
83
+
84
+ [Read more about isolation levels in Wikipedia](http://tinyurl.com/nrqjbb)
85
+
86
+ ## Requirements
87
+
88
+ * Ruby 1.9.2
89
+ * ActiveRecord 3.0.11+
90
+
91
+ ## Running tests
92
+
93
+ Run tests on the selected database (mysql2 by default):
94
+
95
+ db=mysql2 bundle exec rake test
96
+ db=postgresql bundle exec rake test
97
+ db=sqlite3 bundle exec rake test
98
+
99
+ Run tests on all supported databases:
100
+
101
+ ./tests
102
+
103
+ Database configuration is hardcoded in test/db/db.rb; feel free to improve this and submit a pull request.
104
+
105
+ ## How intrusive is this gem?
106
+
107
+ You should be very suspicious about any gem that monkey patches your stock Ruby on Rails framework.
108
+
109
+ This gem is carefully written to not be more intrusive than it needs to be:
110
+
111
+ * introduces several new methods to Mysql2Adapter, PostgreSQLAdapter, SQLite3Adapter; names are carefully taken to not collide with future changes
112
+ * wraps #translate_exception method using alias_method_chain to add new translation
113
+ * introduces new class ActiveRecord::TransactionIsolationConflict in the ActiveRecord module
114
+ * introduces new convenience method ActiveRecord::Base.isolation_level akin to ActiveRecord::Base.transaction
115
+
116
+ ## License
117
+
118
+ 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,6 @@
1
+ source 'https://rubygems.org'
2
+ gemspec path: File.expand_path('..', __FILE__)
3
+
4
+ gem 'ruby2_keywords' if RUBY_VERSION < '2.7'
5
+
6
+ File.exist?(gemfile_local = File.expand_path('../Gemfile.local', __FILE__)) and eval File.read(gemfile_local), binding, gemfile_local
@@ -0,0 +1,4 @@
1
+ base_gemfile = File.expand_path('../../Gemfile.base', __FILE__)
2
+ eval File.read(base_gemfile)
3
+
4
+ gem "activerecord", ">= 5.2.0.beta0", "< 5.3"
@@ -0,0 +1,10 @@
1
+ base_gemfile = File.expand_path('../Gemfile.base', __FILE__)
2
+ eval File.read(base_gemfile), binding, base_gemfile
3
+
4
+ platform :ruby do
5
+ gem "mysql2"
6
+ end
7
+
8
+ platform :jruby do
9
+ gem 'activerecord-jdbcmysql-adapter'
10
+ end
@@ -0,0 +1,10 @@
1
+ base_gemfile = File.expand_path('../Gemfile.base', __FILE__)
2
+ eval File.read(base_gemfile), binding, base_gemfile
3
+
4
+ platform :ruby do
5
+ gem "pg"
6
+ end
7
+
8
+ platform :jruby do
9
+ gem 'activerecord-jdbcpostgresql-adapter'
10
+ end
@@ -0,0 +1,10 @@
1
+ base_gemfile = File.expand_path('../Gemfile.base', __FILE__)
2
+ eval File.read(base_gemfile), binding, base_gemfile
3
+
4
+ platform :ruby do
5
+ gem "sqlite3", "~> 1.4"
6
+ end
7
+
8
+ platform :jruby do
9
+ gem 'activerecord-jdbcsqlite3-adapter', '>=1.3.0.beta2'
10
+ end
@@ -0,0 +1,4 @@
1
+ base_gemfile = File.expand_path('../../Gemfile.base', __FILE__)
2
+ eval File.read(base_gemfile)
3
+
4
+ gem "activerecord", ">= 6.0", "< 6.1"
@@ -0,0 +1,10 @@
1
+ base_gemfile = File.expand_path('../Gemfile.base', __FILE__)
2
+ eval File.read(base_gemfile), binding, base_gemfile
3
+
4
+ platform :ruby do
5
+ gem "mysql2"
6
+ end
7
+
8
+ platform :jruby do
9
+ gem 'activerecord-jdbcmysql-adapter'
10
+ end
@@ -0,0 +1,10 @@
1
+ base_gemfile = File.expand_path('../Gemfile.base', __FILE__)
2
+ eval File.read(base_gemfile), binding, base_gemfile
3
+
4
+ platform :ruby do
5
+ gem "pg"
6
+ end
7
+
8
+ platform :jruby do
9
+ gem 'activerecord-jdbcpostgresql-adapter'
10
+ end
@@ -0,0 +1,10 @@
1
+ base_gemfile = File.expand_path('../Gemfile.base', __FILE__)
2
+ eval File.read(base_gemfile), binding, base_gemfile
3
+
4
+ platform :ruby do
5
+ gem "sqlite3", "~> 1.4"
6
+ end
7
+
8
+ platform :jruby do
9
+ gem 'activerecord-jdbcsqlite3-adapter', '>=1.3.0.beta2'
10
+ end
@@ -0,0 +1,4 @@
1
+ base_gemfile = File.expand_path('../../Gemfile.base', __FILE__)
2
+ eval File.read(base_gemfile)
3
+
4
+ gem "activerecord", ">= 6.1", "< 6.2"
@@ -0,0 +1,10 @@
1
+ base_gemfile = File.expand_path('../Gemfile.base', __FILE__)
2
+ eval File.read(base_gemfile), binding, base_gemfile
3
+
4
+ platform :ruby do
5
+ gem "mysql2"
6
+ end
7
+
8
+ platform :jruby do
9
+ gem 'activerecord-jdbcmysql-adapter'
10
+ end
@@ -0,0 +1,10 @@
1
+ base_gemfile = File.expand_path('../Gemfile.base', __FILE__)
2
+ eval File.read(base_gemfile), binding, base_gemfile
3
+
4
+ platform :ruby do
5
+ gem "pg"
6
+ end
7
+
8
+ platform :jruby do
9
+ gem 'activerecord-jdbcpostgresql-adapter'
10
+ end
@@ -0,0 +1,10 @@
1
+ base_gemfile = File.expand_path('../Gemfile.base', __FILE__)
2
+ eval File.read(base_gemfile), binding, base_gemfile
3
+
4
+ platform :ruby do
5
+ gem "sqlite3", "~> 1.4"
6
+ end
7
+
8
+ platform :jruby do
9
+ gem 'activerecord-jdbcsqlite3-adapter', '>=1.3.0.beta2'
10
+ end
@@ -0,0 +1,4 @@
1
+ base_gemfile = File.expand_path('../../Gemfile.base', __FILE__)
2
+ eval File.read(base_gemfile)
3
+
4
+ gem "activerecord", ">= 7.0", "< 7.1"
@@ -0,0 +1,10 @@
1
+ base_gemfile = File.expand_path('../Gemfile.base', __FILE__)
2
+ eval File.read(base_gemfile), binding, base_gemfile
3
+
4
+ platform :ruby do
5
+ gem "mysql2"
6
+ end
7
+
8
+ platform :jruby do
9
+ gem 'activerecord-jdbcmysql-adapter'
10
+ end
@@ -0,0 +1,10 @@
1
+ base_gemfile = File.expand_path('../Gemfile.base', __FILE__)
2
+ eval File.read(base_gemfile), binding, base_gemfile
3
+
4
+ platform :ruby do
5
+ gem "pg"
6
+ end
7
+
8
+ platform :jruby do
9
+ gem 'activerecord-jdbcpostgresql-adapter'
10
+ end
@@ -0,0 +1,11 @@
1
+ base_gemfile = File.expand_path('../Gemfile.base', __FILE__)
2
+ eval File.read(base_gemfile), binding, base_gemfile
3
+
4
+ platform :ruby do
5
+ # https://github.com/rails/rails/pull/51592
6
+ gem "sqlite3", "~> 1.4"
7
+ end
8
+
9
+ platform :jruby do
10
+ gem 'activerecord-jdbcsqlite3-adapter', '>=1.3.0.beta2'
11
+ end
@@ -0,0 +1,13 @@
1
+ require 'active_record/base'
2
+
3
+ module TransactionIsolation
4
+ module ActiveRecord
5
+ module Base
6
+ def isolation_level( isolation_level, &block )
7
+ connection.isolation_level( isolation_level, &block )
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ ActiveRecord::Base.extend( TransactionIsolation::ActiveRecord::Base )
@@ -0,0 +1,32 @@
1
+ require 'active_record/connection_adapters/abstract_adapter'
2
+
3
+ module TransactionIsolation
4
+ module ActiveRecord
5
+ module ConnectionAdapters # :nodoc:
6
+ module AbstractAdapter
7
+
8
+ VALID_ISOLATION_LEVELS = [:read_uncommitted, :read_committed, :repeatable_read, :serializable]
9
+
10
+ # If true, #isolation_level(level) method is available
11
+ def supports_isolation_levels?
12
+ false
13
+ end
14
+
15
+ def isolation_level( level )
16
+ raise NotImplementedError
17
+ end
18
+
19
+ private
20
+
21
+ def validate_isolation_level( isolation_level )
22
+ unless VALID_ISOLATION_LEVELS.include?( isolation_level )
23
+ raise ArgumentError, "Invalid isolation level '#{isolation_level}'. Supported levels include #{VALID_ISOLATION_LEVELS.join( ', ' )}."
24
+ end
25
+ end
26
+
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.send( :include, TransactionIsolation::ActiveRecord::ConnectionAdapters::AbstractAdapter )
@@ -0,0 +1,83 @@
1
+ if defined?( ActiveRecord::ConnectionAdapters::Mysql2Adapter )
2
+
3
+ module TransactionIsolation
4
+ module ActiveRecord
5
+ module ConnectionAdapters # :nodoc:
6
+ module Mysql2Adapter
7
+
8
+ def self.included( base )
9
+ base.class_eval do
10
+ alias_method :translate_exception_without_transaction_isolation_conflict, :translate_exception
11
+ alias_method :translate_exception, :translate_exception_with_transaction_isolation_conflict
12
+ end
13
+ end
14
+
15
+ def supports_isolation_levels?
16
+ true
17
+ end
18
+
19
+ VENDOR_ISOLATION_LEVEL = {
20
+ :read_uncommitted => 'READ UNCOMMITTED',
21
+ :read_committed => 'READ COMMITTED',
22
+ :repeatable_read => 'REPEATABLE READ',
23
+ :serializable => 'SERIALIZABLE'
24
+ }
25
+
26
+ ANSI_ISOLATION_LEVEL = {
27
+ 'READ UNCOMMITTED' => :read_uncommitted,
28
+ 'READ COMMITTED' => :read_committed,
29
+ 'REPEATABLE READ' => :repeatable_read,
30
+ 'SERIALIZABLE' => :serializable
31
+ }
32
+
33
+ def current_isolation_level
34
+ ANSI_ISOLATION_LEVEL[current_vendor_isolation_level]
35
+ end
36
+
37
+ # transaction_isolation was added in MySQL 5.7.20 as an alias for tx_isolation, which is now deprecated and is removed in MySQL 8.0. Applications should be adjusted to use transaction_isolation in preference to tx_isolation.
38
+ def current_vendor_isolation_level
39
+ isolation_variable = TransactionIsolation.config.mysql_isolation_variable
40
+ select_value( "SELECT @@session.#{isolation_variable}" ).gsub( '-', ' ' )
41
+ end
42
+
43
+ def isolation_level( level )
44
+ validate_isolation_level( level )
45
+
46
+ original_vendor_isolation_level = current_vendor_isolation_level if block_given?
47
+
48
+ execute( "SET SESSION TRANSACTION ISOLATION LEVEL #{VENDOR_ISOLATION_LEVEL[level]}" )
49
+
50
+ begin
51
+ yield
52
+ ensure
53
+ execute "SET SESSION TRANSACTION ISOLATION LEVEL #{original_vendor_isolation_level}"
54
+ end if block_given?
55
+ end
56
+
57
+ def translate_exception_with_transaction_isolation_conflict(*args)
58
+ exception = args.first
59
+
60
+ if isolation_conflict?( exception )
61
+ ::ActiveRecord::TransactionIsolationConflict.new( "Transaction isolation conflict detected: #{exception.message}" )
62
+ else
63
+ translate_exception_without_transaction_isolation_conflict(*args)
64
+ end
65
+ end
66
+
67
+ ruby2_keywords :translate_exception_with_transaction_isolation_conflict if respond_to?(:ruby2_keywords, true)
68
+
69
+ def isolation_conflict?( exception )
70
+ [ "Deadlock found when trying to get lock",
71
+ "Lock wait timeout exceeded"].any? do |error_message|
72
+ exception.message =~ /#{Regexp.escape( error_message )}/i
73
+ end
74
+ end
75
+
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ ActiveRecord::ConnectionAdapters::Mysql2Adapter.send( :include, TransactionIsolation::ActiveRecord::ConnectionAdapters::Mysql2Adapter )
82
+
83
+ end