transaction_reliability 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MjBmMWUyN2QyNzRhN2U0OWE3NzA5MjhmZDAyYmZjZTRjMjgyM2MwMg==
5
+ data.tar.gz: !binary |-
6
+ ZTNkMjRlMWIwZjI5ODI2ZjU5YWU0ZWE0NDIzNDQ0NTdkY2ViZDQ3ZQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ OTYyODkxNWU5NzNjNGM3Y2Y3ODQ3NWRjOTIwZDI3YzlhNzU2OGEwNzg5NjM1
10
+ MTZmMjdkNmNlMjZjNzJiNmM5MmM2Njg4YWU0ZTU0MjdjNmNlMDUzMWE2NTYw
11
+ MGI5MjA0YTgzY2VkNzM0ZjVjMTAzYjdhODgzOWViMDQ2OWJiMTQ=
12
+ data.tar.gz: !binary |-
13
+ Yjk2NGQ3ZWVjNDdiZTlmOWY0NGZiZTQzOGYzMTBhNGVkYmJiMmE2YjlkZWVh
14
+ YzBmZWM5YmE5YjYwZTdlYmFkNzkwNzBiOWRiNTVlMGJhMzUxODBiY2M3MzVm
15
+ OGE4ODY4ZDQxNDJiNmM1MmVlNmVlOWEwNzM1YzkxMzkxNjBiOTM=
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ /tmp
2
+ /coverage
3
+ /gemfiles/.bundle
4
+ /pkg
5
+ /vendor
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --drb
data/.travis.yml ADDED
@@ -0,0 +1,15 @@
1
+ # .travis.yml
2
+ rvm:
3
+ - 1.9.3-p547
4
+ - 2.0.0-p598
5
+ - 2.1.5
6
+ - 2.2.0
7
+ gemfile:
8
+ - gemfiles/rails40.gemfile
9
+ - gemfiles/rails41.gemfile
10
+ - gemfiles/rails42.gemfile
11
+ #matrix:
12
+ # exclude:
13
+ # - rbenv: 2.0.0
14
+ # gemfile: gemfiles/rails2.gemfile
15
+ script: "bundle exec rake spec"
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'pry'
4
+ gem 'activerecord', '~> 4.0.13'
5
+
6
+ # Specify your gem's dependencies in pg_funcall.gemspec
7
+ gemspec :path=>"."
data/Gemfile.lock ADDED
@@ -0,0 +1,70 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ transaction_reliability (0.1.0)
5
+ activerecord (>= 4.0.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (4.0.13)
11
+ activesupport (= 4.0.13)
12
+ builder (~> 3.1.0)
13
+ activerecord (4.0.13)
14
+ activemodel (= 4.0.13)
15
+ activerecord-deprecated_finders (~> 1.0.2)
16
+ activesupport (= 4.0.13)
17
+ arel (~> 4.0.0)
18
+ activerecord-deprecated_finders (1.0.3)
19
+ activesupport (4.0.13)
20
+ i18n (~> 0.6, >= 0.6.9)
21
+ minitest (~> 4.2)
22
+ multi_json (~> 1.3)
23
+ thread_safe (~> 0.1)
24
+ tzinfo (~> 0.3.37)
25
+ arel (4.0.2)
26
+ builder (3.1.4)
27
+ coderay (1.1.0)
28
+ diff-lcs (1.2.5)
29
+ docile (1.1.5)
30
+ i18n (0.7.0)
31
+ method_source (0.8.2)
32
+ minitest (4.7.5)
33
+ multi_json (1.10.1)
34
+ pg (0.18.1)
35
+ pry (0.10.1)
36
+ coderay (~> 1.1.0)
37
+ method_source (~> 0.8.1)
38
+ slop (~> 3.4)
39
+ rake (10.4.2)
40
+ rspec (2.14.1)
41
+ rspec-core (~> 2.14.0)
42
+ rspec-expectations (~> 2.14.0)
43
+ rspec-mocks (~> 2.14.0)
44
+ rspec-core (2.14.8)
45
+ rspec-expectations (2.14.5)
46
+ diff-lcs (>= 1.1.3, < 2.0)
47
+ rspec-mocks (2.14.6)
48
+ simplecov (0.9.1)
49
+ docile (~> 1.1.0)
50
+ multi_json (~> 1.0)
51
+ simplecov-html (~> 0.8.0)
52
+ simplecov-html (0.8.0)
53
+ slop (3.6.0)
54
+ thread_safe (0.3.4)
55
+ tzinfo (0.3.43)
56
+ wwtd (0.7.0)
57
+
58
+ PLATFORMS
59
+ ruby
60
+
61
+ DEPENDENCIES
62
+ activerecord (~> 4.0.13)
63
+ bundler (~> 1.7)
64
+ pg (>= 0.17.0)
65
+ pry
66
+ rake (~> 10.0)
67
+ rspec (~> 2.14.0)
68
+ simplecov
69
+ transaction_reliability!
70
+ wwtd
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Robert Sanders
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # TransactionReliability
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'transaction_reliability'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install transaction_reliability
20
+
21
+ ## Usage
22
+
23
+ TODO: Write usage instructions here
24
+
25
+ ## Contributing
26
+
27
+ 1. Fork it ( https://github.com/[my-github-username]/transaction_reliability/fork )
28
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
29
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
30
+ 4. Push to the branch (`git push origin my-new-feature`)
31
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'bundler/setup'
3
+ require 'wwtd/tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec) do |config|
7
+ end
8
+
9
+ task :default => :wwtd
@@ -0,0 +1,37 @@
1
+ # SQLite version 3.x
2
+ # gem install sqlite3
3
+ #
4
+ # Ensure the SQLite 3 gem is defined in your Gemfile
5
+ # gem 'sqlite3'
6
+ development:
7
+ adapter: postgresql
8
+ database: pg_experiments
9
+ pool: 20
10
+ encoding: UTF8
11
+ min_messages: notice
12
+ host: localhost
13
+ username: <%= `whoami`.chomp %>
14
+ timeout: 5000
15
+
16
+ # Warning: The database defined as "test" will be erased and
17
+ # re-generated from your development database when you run "rake".
18
+ # Do not set this db to the same as development or production.
19
+ test:
20
+ adapter: postgresql
21
+ database: pg_experiments_test
22
+ pool: 20
23
+ encoding: UTF8
24
+ min_messages: notice
25
+ host: localhost
26
+ username: <%= `whoami`.chomp %>
27
+ timeout: 5000
28
+
29
+ production:
30
+ adapter: postgresql
31
+ database: pg_experiments_prod
32
+ pool: 20
33
+ encoding: UTF8
34
+ min_messages: notice
35
+ host: localhost
36
+ username: <%= `whoami`.chomp %>
37
+ timeout: 5000
@@ -0,0 +1,17 @@
1
+ test:
2
+ adapter: postgresql
3
+ encoding: UTF8
4
+ database: pg_funcall_test
5
+ username: pgfuncallgem
6
+ password: pgfuncallgem
7
+ min_messages: warning
8
+ host: localhost
9
+
10
+ development:
11
+ adapter: postgresql
12
+ encoding: UTF8
13
+ database: pg_funcall_dev
14
+ username: pgfuncallgem
15
+ password: pgfuncallgem
16
+ min_messages: warning
17
+ host: localhost
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'pry'
4
+ gem 'activerecord', '~> 4.0.13'
5
+ gem 'pg', '~> 0.17.1'
6
+
7
+ # Specify your gem's dependencies in pg_funcall.gemspec
8
+ gemspec :path=>"../"
@@ -0,0 +1,70 @@
1
+ PATH
2
+ remote: ../
3
+ specs:
4
+ transaction_reliability (0.1.0)
5
+ activerecord (>= 4.0.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (4.0.13)
11
+ activesupport (= 4.0.13)
12
+ builder (~> 3.1.0)
13
+ activerecord (4.0.13)
14
+ activemodel (= 4.0.13)
15
+ activerecord-deprecated_finders (~> 1.0.2)
16
+ activesupport (= 4.0.13)
17
+ arel (~> 4.0.0)
18
+ activerecord-deprecated_finders (1.0.3)
19
+ activesupport (4.0.13)
20
+ i18n (~> 0.6, >= 0.6.9)
21
+ minitest (~> 4.2)
22
+ multi_json (~> 1.3)
23
+ thread_safe (~> 0.1)
24
+ tzinfo (~> 0.3.37)
25
+ arel (4.0.2)
26
+ builder (3.1.4)
27
+ coderay (1.1.0)
28
+ diff-lcs (1.2.5)
29
+ docile (1.1.5)
30
+ i18n (0.7.0)
31
+ method_source (0.8.2)
32
+ minitest (4.7.5)
33
+ multi_json (1.10.1)
34
+ pg (0.17.1)
35
+ pry (0.10.1)
36
+ coderay (~> 1.1.0)
37
+ method_source (~> 0.8.1)
38
+ slop (~> 3.4)
39
+ rake (10.4.2)
40
+ rspec (2.14.1)
41
+ rspec-core (~> 2.14.0)
42
+ rspec-expectations (~> 2.14.0)
43
+ rspec-mocks (~> 2.14.0)
44
+ rspec-core (2.14.8)
45
+ rspec-expectations (2.14.5)
46
+ diff-lcs (>= 1.1.3, < 2.0)
47
+ rspec-mocks (2.14.6)
48
+ simplecov (0.9.1)
49
+ docile (~> 1.1.0)
50
+ multi_json (~> 1.0)
51
+ simplecov-html (~> 0.8.0)
52
+ simplecov-html (0.8.0)
53
+ slop (3.6.0)
54
+ thread_safe (0.3.4)
55
+ tzinfo (0.3.42)
56
+ wwtd (0.7.0)
57
+
58
+ PLATFORMS
59
+ ruby
60
+
61
+ DEPENDENCIES
62
+ activerecord (~> 4.0.13)
63
+ bundler (~> 1.7)
64
+ pg (~> 0.17.1)
65
+ pry
66
+ rake (~> 10.0)
67
+ rspec (~> 2.14.0)
68
+ simplecov
69
+ transaction_reliability!
70
+ wwtd
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'pry'
4
+ gem 'activerecord', '~> 4.1.9'
5
+
6
+ # Specify your gem's dependencies in pg_funcall.gemspec
7
+ gemspec :path=>"../"
@@ -0,0 +1,70 @@
1
+ PATH
2
+ remote: ../
3
+ specs:
4
+ transaction_reliability (0.1.0)
5
+ activerecord (>= 4.0.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (4.1.9)
11
+ activesupport (= 4.1.9)
12
+ builder (~> 3.1)
13
+ activerecord (4.1.9)
14
+ activemodel (= 4.1.9)
15
+ activesupport (= 4.1.9)
16
+ arel (~> 5.0.0)
17
+ activesupport (4.1.9)
18
+ i18n (~> 0.6, >= 0.6.9)
19
+ json (~> 1.7, >= 1.7.7)
20
+ minitest (~> 5.1)
21
+ thread_safe (~> 0.1)
22
+ tzinfo (~> 1.1)
23
+ arel (5.0.1.20140414130214)
24
+ builder (3.2.2)
25
+ coderay (1.1.0)
26
+ diff-lcs (1.2.5)
27
+ docile (1.1.5)
28
+ i18n (0.7.0)
29
+ json (1.8.2)
30
+ method_source (0.8.2)
31
+ minitest (5.5.1)
32
+ multi_json (1.10.1)
33
+ pg (0.18.1)
34
+ pry (0.10.1)
35
+ coderay (~> 1.1.0)
36
+ method_source (~> 0.8.1)
37
+ slop (~> 3.4)
38
+ rake (10.4.2)
39
+ rspec (2.14.1)
40
+ rspec-core (~> 2.14.0)
41
+ rspec-expectations (~> 2.14.0)
42
+ rspec-mocks (~> 2.14.0)
43
+ rspec-core (2.14.8)
44
+ rspec-expectations (2.14.5)
45
+ diff-lcs (>= 1.1.3, < 2.0)
46
+ rspec-mocks (2.14.6)
47
+ simplecov (0.9.1)
48
+ docile (~> 1.1.0)
49
+ multi_json (~> 1.0)
50
+ simplecov-html (~> 0.8.0)
51
+ simplecov-html (0.8.0)
52
+ slop (3.6.0)
53
+ thread_safe (0.3.4)
54
+ tzinfo (1.2.2)
55
+ thread_safe (~> 0.1)
56
+ wwtd (0.7.0)
57
+
58
+ PLATFORMS
59
+ ruby
60
+
61
+ DEPENDENCIES
62
+ activerecord (~> 4.1.9)
63
+ bundler (~> 1.7)
64
+ pg (>= 0.17.0)
65
+ pry
66
+ rake (~> 10.0)
67
+ rspec (~> 2.14.0)
68
+ simplecov
69
+ transaction_reliability!
70
+ wwtd
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'pry'
4
+ gem 'activerecord', '~> 4.2.0'
5
+
6
+ # Specify your gem's dependencies in pg_funcall.gemspec
7
+ gemspec :path=>"../"
@@ -0,0 +1,70 @@
1
+ PATH
2
+ remote: ../
3
+ specs:
4
+ transaction_reliability (0.1.0)
5
+ activerecord (>= 4.0.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (4.2.0)
11
+ activesupport (= 4.2.0)
12
+ builder (~> 3.1)
13
+ activerecord (4.2.0)
14
+ activemodel (= 4.2.0)
15
+ activesupport (= 4.2.0)
16
+ arel (~> 6.0)
17
+ activesupport (4.2.0)
18
+ i18n (~> 0.7)
19
+ json (~> 1.7, >= 1.7.7)
20
+ minitest (~> 5.1)
21
+ thread_safe (~> 0.3, >= 0.3.4)
22
+ tzinfo (~> 1.1)
23
+ arel (6.0.0)
24
+ builder (3.2.2)
25
+ coderay (1.1.0)
26
+ diff-lcs (1.2.5)
27
+ docile (1.1.5)
28
+ i18n (0.7.0)
29
+ json (1.8.2)
30
+ method_source (0.8.2)
31
+ minitest (5.5.1)
32
+ multi_json (1.10.1)
33
+ pg (0.18.1)
34
+ pry (0.10.1)
35
+ coderay (~> 1.1.0)
36
+ method_source (~> 0.8.1)
37
+ slop (~> 3.4)
38
+ rake (10.4.2)
39
+ rspec (2.14.1)
40
+ rspec-core (~> 2.14.0)
41
+ rspec-expectations (~> 2.14.0)
42
+ rspec-mocks (~> 2.14.0)
43
+ rspec-core (2.14.8)
44
+ rspec-expectations (2.14.5)
45
+ diff-lcs (>= 1.1.3, < 2.0)
46
+ rspec-mocks (2.14.6)
47
+ simplecov (0.9.1)
48
+ docile (~> 1.1.0)
49
+ multi_json (~> 1.0)
50
+ simplecov-html (~> 0.8.0)
51
+ simplecov-html (0.8.0)
52
+ slop (3.6.0)
53
+ thread_safe (0.3.4)
54
+ tzinfo (1.2.2)
55
+ thread_safe (~> 0.1)
56
+ wwtd (0.7.0)
57
+
58
+ PLATFORMS
59
+ ruby
60
+
61
+ DEPENDENCIES
62
+ activerecord (~> 4.2.0)
63
+ bundler (~> 1.7)
64
+ pg (>= 0.17.0)
65
+ pry
66
+ rake (~> 10.0)
67
+ rspec (~> 2.14.0)
68
+ simplecov
69
+ transaction_reliability!
70
+ wwtd
@@ -0,0 +1,237 @@
1
+ #
2
+ # Provide utilities for the (more) reliable handling of database errors
3
+ # related to concurrency or connectivity.
4
+ #
5
+ # Currently only handles Postgresql and Mysql, assuming use of
6
+ # the PG and Mysql2 drivers respectively
7
+ #
8
+ module TransactionReliability
9
+
10
+ #
11
+ # We inherit from StatementInvalid because of compatibility with
12
+ # all the code which may rescue StatementInvalid
13
+ #
14
+ class TransientTransactionError < ActiveRecord::StatementInvalid
15
+ end
16
+
17
+ #
18
+ # Parent class for deadlocks, serialization failures, and other
19
+ # non-fatal errors that can be handled by retrying a transaction
20
+ #
21
+ class ConcurrencyError < TransientTransactionError
22
+ end
23
+
24
+ #
25
+ # There has been an unresolvable write conflict between two transactions
26
+ #
27
+ class DeadlockDetected < ConcurrencyError
28
+ end
29
+
30
+ #
31
+ # For transaction in isolation level SERIALIZABLE, some conflict
32
+ # has been detected. The transaction should be retried.
33
+ #
34
+ class SerializationFailure < ConcurrencyError
35
+ end
36
+
37
+ #
38
+ # The connection to the database has been lost.
39
+ #
40
+ class ConnectionLost < TransientTransactionError
41
+ end
42
+
43
+ module Helpers
44
+ #
45
+ # Intended to be included in an ActiveRecord model class.
46
+ #
47
+ # Retries a block (which usually contains a transaction) under certain
48
+ # failure conditions, up to a configurable number of times with an
49
+ # exponential backoff delay between each attempt.
50
+ #
51
+ # Conditions for retrying:
52
+ #
53
+ # 1. Database connection lost
54
+ # 2. Query or txn failed due to detected deadlock
55
+ # (Mysql/InnoDB and Postgres can both signal this for just about
56
+ # any transaction)
57
+ # 3. Query or txn failed due to serialization failure
58
+ # (Postgres will signal this for transactions in isolation
59
+ # level SERIALIZABLE)
60
+ #
61
+ # options:
62
+ # retry_count - how many retries to make; default 4
63
+ #
64
+ # backoff - time period before 1st retry, in fractional seconds.
65
+ # will double at every retry. default 0.25 seconds.
66
+ #
67
+ # exit_on_disconnect
68
+ # - whether to call exit if no retry succeeds and
69
+ # the cause is a failed connection
70
+ #
71
+ # exit_on_fail - whether to call exit if no retry succeeds
72
+ #
73
+ # defaults:
74
+ #
75
+ #
76
+ def with_transaction_retry(options = {})
77
+ retry_count = options.fetch(:retry_count, 4)
78
+ backoff = options.fetch(:backoff, 0.25)
79
+ exit_on_fail = options.fetch(:exit_on_fail, false)
80
+ exit_on_disconnect = options.fetch(:exit_on_disconnect, true)
81
+
82
+ count = 0
83
+
84
+ # list of exceptions we may catch
85
+ exceptions = ['ActiveRecord::StatementInvalid', 'PG::Error', 'Mysql2::Error'].
86
+ map {|name| name.safe_constantize}.
87
+ compact
88
+
89
+ #
90
+ # There are times when, for example,
91
+ # a raw PG::Error is throw rather than a wrapped ActiveRecord::StatementInvalid
92
+ #
93
+ # Also, connector-specific classes like PG::Error may not be defined
94
+ #
95
+ begin
96
+ connection_lost = false
97
+ yield
98
+ rescue *exceptions => e
99
+ translated = TransactionReliability.rewrap_exception(e)
100
+
101
+ case translated
102
+ when ConnectionLost
103
+ (options[:connection] || ActiveRecord::Base.connection).reconnect!
104
+ connection_lost = true
105
+ when DeadlockDetected, SerializationFailure
106
+ else
107
+ raise translated
108
+ end
109
+
110
+ # Retry up to retry_count times
111
+ if count < retry_count
112
+ sleep backoff
113
+ count += 1
114
+ backoff *= 2
115
+ retry
116
+ else
117
+ if (connection_lost && exit_on_disconnect) || exit_on_fail
118
+ exit
119
+ else
120
+ raise(translated)
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ #
127
+ # Execute some code in a DB transaction, with retry
128
+ #
129
+ def transaction_with_retry(txn_options = {}, retry_options = {})
130
+ base_obj = self.respond_to?(:transaction) ? self : ActiveRecord::Base
131
+
132
+ with_transaction_retry(retry_options) do
133
+ base_obj.transaction(txn_options) do
134
+ yield
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ # allow the helper methods to be accessed as methods on this module
141
+ extend(Helpers)
142
+
143
+ #
144
+ # Unwrap ActiveRecord::StatementInvalid into some more specific exceptions
145
+ # that we define.
146
+ #
147
+ # Only defined for Mysql2 and PG drivers at the moment
148
+ #
149
+ def self.rewrap_exception(exception)
150
+ if exception.message.start_with?('PG::') || exception.class.name.start_with?('PG::')
151
+ rewrap_pg_exception exception
152
+ elsif exception.message =~ /^mysql2::/i
153
+ rewrap_mysql2_exception exception
154
+ else
155
+ exception
156
+ end
157
+ end
158
+
159
+ protected
160
+
161
+ SQLSTATE_CONNECTION_ERRORS =
162
+ [
163
+ '08000', # connection_exception
164
+ '08003', # connection_does_not_exist
165
+ '08006', # connection_failure
166
+ '08001', # sqlclient_unable_to_establish_sqlconnection
167
+ '08004', # sqlserver_rejected_establishment_of_sqlconnection
168
+ '08007', # transaction_resolution_unknown
169
+ '08P01' # protocol_violation
170
+ ]
171
+
172
+ SQLSTATE_DEADLOCK_ERRORS =
173
+ [
174
+ '40P01' # deadlock detected
175
+ ]
176
+
177
+ SQLSTATE_ISOLATION_ERRORS =
178
+ [
179
+ '40001' # serialization failure
180
+ ]
181
+
182
+
183
+ def self.rewrap_pg_exception(exception)
184
+ message = exception.message
185
+ orig = case
186
+ when exception.is_a?(PG::Error)
187
+ exception
188
+ when exception.respond_to?(:original_exception)
189
+ exception.original_exception
190
+ else
191
+ exception
192
+ end
193
+
194
+ if orig.is_a? PG::Error
195
+ sqlstate = orig.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) rescue nil
196
+ else
197
+ sqlstate = nil
198
+ end
199
+
200
+ case
201
+ when orig.is_a?(PG::ConnectionBad) || SQLSTATE_CONNECTION_ERRORS.include?(sqlstate)
202
+ ConnectionLost.new(message, orig)
203
+ when orig.is_a?(PG::TRDeadlockDetected) || SQLSTATE_DEADLOCK_ERRORS.include?(sqlstate)
204
+ DeadlockDetected.new(message, orig)
205
+ when orig.is_a?(PG::TRSerializationFailure) || SQLSTATE_ISOLATION_ERRORS.include?(sqlstate)
206
+ SerializationFailure.new(message, orig)
207
+ else
208
+ exception
209
+ end
210
+ end
211
+
212
+ #
213
+ # Ugh. This may not work if you're using non-English localization
214
+ # for your MySQL server.
215
+ #
216
+ def self.rewrap_mysql_exception(exception)
217
+ orig = exception.original_exception
218
+ message = exception.message
219
+
220
+ case
221
+ when message =~ /Serialization failure/i
222
+ SerializationFailure.new(message, orig)
223
+ when message =~ /Deadlock found when trying to get lock/i ||
224
+ message =~ /Lock wait timeout exceeded/i
225
+ DeadlockDetected.new(message, orig)
226
+ when message =~ /Lost connection to MySQL server/i ||
227
+ message =~ /Invalid connection handle/i ||
228
+ message =~ /MySQL server has gone away/i ||
229
+ message =~ /Broken pipe/i ||
230
+ message =~ /Server shutdown in progress/i
231
+ ConnectionLost.new(message, orig)
232
+ else
233
+ exception
234
+ end
235
+ end
236
+ end
237
+
@@ -0,0 +1,3 @@
1
+ module TransactionReliability
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ for gemfile in gemfiles/*.gemfile; do
3
+ bundle --no-deployment --gemfile $gemfile
4
+ done
data/script/shell ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ require 'yaml'
6
+ require 'active_record'
7
+ configs = YAML.load(File.read("config/database.yml.example"))
8
+ ActiveRecord::Base.establish_connection(configs['development'])
9
+
10
+ require 'transaction_reliability'
11
+
12
+ def conn
13
+ ActiveRecord::Base.connection
14
+ end
15
+
16
+ begin
17
+ require 'pry'
18
+ Pry.start
19
+ rescue LoadError
20
+ require 'irb'
21
+ IRB.start
22
+ end
23
+
@@ -0,0 +1,195 @@
1
+ require 'spec_helper'
2
+ require 'pg'
3
+
4
+ describe TransactionReliability do
5
+
6
+ class TrSpecTable < ActiveRecord::Base
7
+ extend TransactionReliability::Helpers
8
+ self.table_name = 'public.tr_spec_table'
9
+ end
10
+
11
+ class Foo
12
+ extend TransactionReliability::Helpers
13
+ def self.transaction(*args)
14
+ puts "txn called with #{args.inspect}"
15
+ yield
16
+ end
17
+ end
18
+
19
+ # before(:all) do
20
+ # ActiveRecord::Base.connection.execute <<-SQL
21
+ # DROP TABLE IF EXISTS public.tr_spec_table;
22
+ # CREATE TABLE IF NOT EXISTS public.tr_spec_table
23
+ # (name text, num integer, ts timestamp);
24
+ # SQL
25
+ #
26
+ # # ActiveRecord::Base.reset_column_information
27
+ # end
28
+
29
+ # after(:all) do
30
+ # ActiveRecord::Base.connection.execute <<-SQL
31
+ # DROP TABLE IF EXISTS public.tr_spec_table;
32
+ # SQL
33
+ # end
34
+
35
+ let! :conn1 do
36
+ ActiveRecord::Base.connection_pool.checkout
37
+ end
38
+
39
+ let! :conn2 do
40
+ ActiveRecord::Base.connection_pool.checkout
41
+ end
42
+
43
+ after(:each) do
44
+ [conn1, conn2].each {|conn| ActiveRecord::Base.connection_pool.checkin(conn) }
45
+ end
46
+
47
+ let :counter do
48
+ double('counter')
49
+ end
50
+
51
+ def fake_deadlock
52
+ raise PG::TRDeadlockDetected, "PG::TRDeadlockDetected fake"
53
+ end
54
+
55
+ def fake_serialization_failure
56
+ raise PG::TRSerializationFailure, "PG::TRSerializationFailure fake"
57
+ end
58
+
59
+ def fake_connection_lost
60
+ raise PG::ConnectionBad, "PG::ConnectionBad fake"
61
+ end
62
+
63
+ class ActiveRecord::Base
64
+ def self.transaction(*args)
65
+ puts "calling fake .transaction() in AR::Base"
66
+ yield
67
+ end
68
+
69
+ def self.connection
70
+ double()
71
+ end
72
+ end
73
+
74
+ context 'uncontended' do
75
+ context 'transaction_with_retry' do
76
+ it 'should run the block once' do
77
+ counter.should_receive(:doit).exactly(1).times
78
+ ActiveRecord::Base.should_receive(:transaction).exactly(1).times.and_call_original
79
+
80
+ TransactionReliability.transaction_with_retry do
81
+ counter.doit
82
+ end
83
+ end
84
+ end
85
+
86
+ context 'with_transaction_retry' do
87
+ it 'should run the block once' do
88
+ counter.should_receive(:doit).exactly(1).times
89
+ ActiveRecord::Base.should_not_receive(:transaction)
90
+
91
+ TransactionReliability.with_transaction_retry do
92
+ counter.doit
93
+ end
94
+ end
95
+
96
+ it 'should not re-run on ordinary failure' do
97
+ counter.should_receive(:doit).exactly(1).times
98
+
99
+ expect do
100
+ TransactionReliability.with_transaction_retry do
101
+ counter.doit
102
+ raise ArgumentError
103
+ end
104
+ end.to raise_error(ArgumentError)
105
+ end
106
+ end
107
+
108
+ context 'with simulated error on PG' do
109
+ it 'should run the block the maximum number of times' do
110
+ counter.should_receive(:doit).exactly(5).times
111
+
112
+ expect do
113
+ TransactionReliability.with_transaction_retry(backoff: 0) do
114
+ counter.doit
115
+ fake_deadlock
116
+ end
117
+ end.to raise_error
118
+ end
119
+
120
+ it 'should return TransactionReliability::DeadlockDetected error on unresolved deadlock' do
121
+ expect do
122
+ TransactionReliability.with_transaction_retry(retry_count: 1) do
123
+ fake_deadlock
124
+ end
125
+ end.to raise_error(TransactionReliability::DeadlockDetected)
126
+ end
127
+
128
+ it 'should return TransactionReliability::SerializationFailure on unresolved serialization failure' do
129
+ expect do
130
+ TransactionReliability.with_transaction_retry(retry_count: 1) do
131
+ fake_serialization_failure
132
+ end
133
+ end.to raise_error(TransactionReliability::SerializationFailure)
134
+ end
135
+ end
136
+
137
+ context 'with simulated connection failure' do
138
+ let :countup do
139
+ [0]
140
+ end
141
+
142
+ let :connection do
143
+ double('Connection')
144
+ end
145
+
146
+ before do
147
+ ActiveRecord::Base.should_receive(:connection).at_least(1).times.and_return(connection)
148
+ end
149
+
150
+ it 'should retry the block' do
151
+ counter.should_receive(:doit).exactly(5).times
152
+ connection.should_receive(:reconnect!).at_least(1).times
153
+
154
+ expect do
155
+ TransactionReliability.with_transaction_retry(backoff: 0) do
156
+ counter.doit
157
+ fake_connection_lost
158
+ end
159
+ end.to raise_error
160
+ end
161
+
162
+ it 'should reconnect' do
163
+ connection.should_receive(:reconnect!).exactly(1).times
164
+ counter.should_receive(:doit).exactly(2).times
165
+
166
+ TransactionReliability.with_transaction_retry(backoff: 0) do
167
+ counter.doit
168
+ countup[0] += 1
169
+ fake_connection_lost if countup[0] == 1
170
+ end
171
+ end
172
+
173
+ it 'should raise ConnectionLost if unresolved' do
174
+ connection.should_receive(:reconnect!).at_least(1).times
175
+
176
+ expect do
177
+ TransactionReliability.with_transaction_retry(retry_count: 1, exit_on_disconnect: false) do
178
+ fake_connection_lost
179
+ end
180
+ end.to raise_error(TransactionReliability::ConnectionLost)
181
+ end
182
+
183
+ it 'should call exit if the connection cannot be re-established and configured to exit' do
184
+ connection.should_receive(:reconnect!).at_least(1).times
185
+
186
+ expect do
187
+ TransactionReliability.with_transaction_retry(retry_count: 1, exit_on_disconnect: true) do
188
+ fake_connection_lost
189
+ end
190
+ end.to raise_error(SystemExit)
191
+ end
192
+ end
193
+ end
194
+
195
+ end
@@ -0,0 +1,20 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ if ENV['COVERAGE'] == 'true'
5
+ require 'simplecov'
6
+ SimpleCov.start
7
+ end
8
+
9
+ require 'yaml'
10
+ require 'active_record'
11
+ configs = YAML.load(File.read("config/database.yml.example"))
12
+ ActiveRecord::Base.establish_connection(configs['test'])
13
+
14
+ require 'transaction_reliability'
15
+
16
+ begin
17
+ require 'pry'
18
+ rescue LoadError
19
+ #
20
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'transaction_reliability/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "transaction_reliability"
8
+ spec.version = TransactionReliability::VERSION
9
+ spec.authors = ["Robert Sanders"]
10
+ spec.email = ["robert@curioussquid.com"]
11
+ spec.summary = %q{Functions to wrap and retry a code block when the DB declares a serialization failure or deadlock.}
12
+ # spec.description = %q{TODO: Write a longer description. Optional.}
13
+ spec.homepage = "http://github.com/rsanders/transaction_reliability"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features|gemfiles|config)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activerecord", ">= 4.0.0"
22
+
23
+ spec.add_development_dependency "pg", ">= 0.17.0"
24
+ spec.add_development_dependency "bundler", "~> 1.7"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 2.14.0"
27
+ spec.add_development_dependency "simplecov"
28
+ spec.add_development_dependency "wwtd"
29
+ end
metadata ADDED
@@ -0,0 +1,176 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: transaction_reliability
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Robert Sanders
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 4.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 4.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: pg
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.17.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.17.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '1.7'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '1.7'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: 2.14.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: 2.14.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: wwtd
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ! '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description:
112
+ email:
113
+ - robert@curioussquid.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - .gitignore
119
+ - .rspec
120
+ - .travis.yml
121
+ - Gemfile
122
+ - Gemfile.lock
123
+ - LICENSE.txt
124
+ - README.md
125
+ - Rakefile
126
+ - config/database.yml
127
+ - config/database.yml.example
128
+ - gemfiles/rails40.gemfile
129
+ - gemfiles/rails40.gemfile.lock
130
+ - gemfiles/rails41.gemfile
131
+ - gemfiles/rails41.gemfile.lock
132
+ - gemfiles/rails42.gemfile
133
+ - gemfiles/rails42.gemfile.lock
134
+ - lib/transaction_reliability.rb
135
+ - lib/transaction_reliability/version.rb
136
+ - script/rebundle.sh
137
+ - script/shell
138
+ - spec/lib/transaction_reliability_spec.rb
139
+ - spec/spec_helper.rb
140
+ - transaction_reliability.gemspec
141
+ homepage: http://github.com/rsanders/transaction_reliability
142
+ licenses:
143
+ - MIT
144
+ metadata: {}
145
+ post_install_message:
146
+ rdoc_options: []
147
+ require_paths:
148
+ - lib
149
+ required_ruby_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ! '>='
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ required_rubygems_version: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ! '>='
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ requirements: []
160
+ rubyforge_project:
161
+ rubygems_version: 2.4.5
162
+ signing_key:
163
+ specification_version: 4
164
+ summary: Functions to wrap and retry a code block when the DB declares a serialization
165
+ failure or deadlock.
166
+ test_files:
167
+ - config/database.yml
168
+ - config/database.yml.example
169
+ - gemfiles/rails40.gemfile
170
+ - gemfiles/rails40.gemfile.lock
171
+ - gemfiles/rails41.gemfile
172
+ - gemfiles/rails41.gemfile.lock
173
+ - gemfiles/rails42.gemfile
174
+ - gemfiles/rails42.gemfile.lock
175
+ - spec/lib/transaction_reliability_spec.rb
176
+ - spec/spec_helper.rb