transaction_reliability 0.1.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,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