activerecord-retry 1.0.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.
@@ -0,0 +1,89 @@
1
+ require "active_record"
2
+ require "active_record/errors"
3
+ require "active_support/concern"
4
+ require "active_support/core_ext/integer/inflections"
5
+ require "active_support/core_ext/module/attribute_accessors"
6
+
7
+ module ActiveRecord
8
+ module Retry
9
+ require "active_record/retry/version"
10
+
11
+ extend ActiveSupport::Concern
12
+
13
+ DEFAULT_RETRIES = [1, 2, 4].freeze
14
+ DEFAULT_RETRY_ERRORS = {
15
+ # MySQL errors
16
+ /Deadlock found when trying to get lock/ => :retry,
17
+ /Lock wait timeout exceeded/ => :retry,
18
+ /Lost connection to MySQL server during query/ => [:sleep, :reconnect, :retry],
19
+ /MySQL server has gone away/ => [:sleep, :reconnect, :retry],
20
+ /Query execution was interrupted/ => :retry,
21
+ /The MySQL server is running with the --read-only option so it cannot execute this statement/ => [:sleep, :reconnect, :retry]
22
+ }.freeze
23
+
24
+ included do
25
+ mattr_accessor :retry_errors, :retries
26
+
27
+ self.retries = self::DEFAULT_RETRIES.dup
28
+ self.retry_errors = self::DEFAULT_RETRY_ERRORS.dup
29
+
30
+ class << self
31
+ alias_method :find_by_sql_without_retry, :find_by_sql
32
+ alias_method :find_by_sql, :find_by_sql_with_retry
33
+ alias_method :transaction_without_retry, :transaction
34
+ alias_method :transaction, :transaction_with_retry
35
+ end
36
+ end
37
+
38
+ module ClassMethods
39
+ def find_by_sql_with_retry(*args, &block)
40
+ with_retry { find_by_sql_without_retry(*args, &block) }
41
+ end
42
+
43
+ def transaction_with_retry(*args, &block)
44
+ with_retry { transaction_without_retry(*args, &block) }
45
+ end
46
+
47
+ def with_retry
48
+ tries = 0
49
+
50
+ begin
51
+ yield
52
+ rescue ActiveRecord::StatementInvalid => error
53
+ raise if connection.open_transactions != 0
54
+ raise if tries >= retries.count
55
+
56
+ found, actions = retry_errors.detect { |regex, action| regex =~ error.message }
57
+ raise unless found
58
+
59
+ actions = Array(actions)
60
+ delay = retries[tries]
61
+ tries += 1
62
+
63
+ if logger
64
+ message = "Query failed: '#{error}'. "
65
+ message << actions.map do |action|
66
+ case action
67
+ when :sleep
68
+ "sleeping for #{delay}s"
69
+ when :reconnect
70
+ "reconnecting"
71
+ when :retry
72
+ "retrying"
73
+ end
74
+ end.join(", ").capitalize
75
+ message << " for the #{tries.ordinalize} time."
76
+ logger.warn(message)
77
+ end
78
+
79
+ sleep(delay) if actions.include?(:sleep)
80
+ if actions.include?(:reconnect)
81
+ clear_active_connections!
82
+ establish_connection
83
+ end
84
+ retry if actions.include?(:retry)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,17 @@
1
+ require "rails"
2
+ require "active_record/retry"
3
+
4
+ module ActiveRecord
5
+ module Retry
6
+ class Railtie < Rails::Railtie
7
+ config.active_record.retries = ActiveRecord::Retry::DEFAULT_RETRIES
8
+
9
+ config.after_initialize do |app|
10
+ ActiveSupport.on_load(:active_record) do
11
+ ActiveRecord::Base.send(:include, ActiveRecord::Retry)
12
+ ActiveRecord::Base.retries = app.config.active_record.retries
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveRecord
2
+ module Retry
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
@@ -0,0 +1 @@
1
+ require "active_record/retry"
@@ -0,0 +1,122 @@
1
+ require "test_helper"
2
+ require "active_record/retry"
3
+
4
+ describe ActiveRecord::Retry do
5
+ before do
6
+ @connection = mock()
7
+ @connection.singleton_class.class_exec do
8
+ attr_accessor :open_transactions
9
+ end
10
+ @connection.open_transactions = 0
11
+ @buffer = StringIO.new
12
+ @logger = Logger.new(@buffer)
13
+ @mock = Class.new
14
+ @mock.stubs(:connection).returns(@connection)
15
+ @mock.stubs(:clear_active_connections!)
16
+ @mock.stubs(:establish_connection)
17
+ @mock.stubs(:find_by_sql)
18
+ @mock.stubs(:logger).returns(@logger)
19
+ @mock.stubs(:sleep)
20
+ def @mock.transaction
21
+ connection.open_transactions += 1
22
+ yield
23
+ ensure
24
+ connection.open_transactions -= 1
25
+ end
26
+ @mock.send(:include, ActiveRecord::Retry)
27
+ @mock.retry_errors = {
28
+ /sleep then reconnect then retry/ => [:sleep, :reconnect, :retry],
29
+ /sleep then retry/ => [:sleep, :retry],
30
+ /reconnect then retry/ => [:reconnect, :retry],
31
+ /retry/ => :retry
32
+ }
33
+ end
34
+
35
+ it "should work with no errors" do
36
+ @mock.with_retry { :success }.must_equal(:success)
37
+ end
38
+
39
+ it "should work with less or equal errors than retries" do
40
+ errors = ["sleep then retry", "retry"]
41
+ @mock.retries = [0, 0]
42
+ @mock.with_retry { errors.any? ? raise(ActiveRecord::StatementInvalid, errors.shift) : :success }.must_equal(:success)
43
+ end
44
+
45
+ it "should not work with more errors than retries" do
46
+ errors = ["sleep then retry", "retry", "retry"]
47
+ @mock.retries = [0, 0]
48
+ -> { @mock.with_retry { errors.any? ? raise(ActiveRecord::StatementInvalid, errors.shift) : :success } }.must_raise(ActiveRecord::StatementInvalid)
49
+ end
50
+
51
+ it "should not retry more than retries count" do
52
+ retries = @mock.retries = [2, 4, 8, 16]
53
+ runs = 0
54
+
55
+ -> do
56
+ @mock.with_retry do
57
+ runs += 1
58
+ raise ActiveRecord::StatementInvalid, "retry"
59
+ end
60
+ end.must_raise(ActiveRecord::StatementInvalid)
61
+
62
+ runs.must_equal(1 + retries.count)
63
+ end
64
+
65
+ it "should not retry a query inside of a nested transaction" do
66
+ inner_runs = outer_runs = 0
67
+
68
+ -> do
69
+ @mock.transaction do
70
+ outer_runs += 1
71
+ @mock.with_retry do
72
+ inner_runs += 1
73
+ raise ActiveRecord::StatementInvalid, "retry"
74
+ end
75
+ end
76
+ end.must_raise(ActiveRecord::StatementInvalid)
77
+
78
+ inner_runs.must_equal(outer_runs)
79
+ end
80
+
81
+ it "logs a warning when a query is being retried that describes what it is doing" do
82
+ @mock.retries = [2]
83
+
84
+ -> do
85
+ @mock.with_retry do
86
+ raise ActiveRecord::StatementInvalid, "sleep then reconnect then retry"
87
+ end
88
+ end.must_raise(ActiveRecord::StatementInvalid)
89
+
90
+ warning = @buffer.string
91
+ warning.wont_be_empty
92
+ /sleeping for 2s/i.must_match(warning)
93
+ /reconnecting/i.must_match(warning)
94
+ /retrying/i.must_match(warning)
95
+ /for the 1st time/i.must_match(warning)
96
+ end
97
+
98
+ it "sleeps for the proper times" do
99
+ @mock.expects(:sleep).with(2)
100
+ @mock.expects(:sleep).with(4)
101
+ @mock.expects(:sleep).with(8)
102
+ @mock.retries = [2, 4, 8]
103
+
104
+ -> do
105
+ @mock.with_retry do
106
+ raise ActiveRecord::StatementInvalid, "sleep then retry"
107
+ end
108
+ end.must_raise(ActiveRecord::StatementInvalid)
109
+ end
110
+
111
+ it "clears active connections when action is reconnect" do
112
+ @mock.expects(:clear_active_connections!)
113
+ @mock.expects(:establish_connection)
114
+ @mock.retries = [2]
115
+
116
+ -> do
117
+ @mock.with_retry do
118
+ raise ActiveRecord::StatementInvalid, "reconnect then retry"
119
+ end
120
+ end.must_raise(ActiveRecord::StatementInvalid)
121
+ end
122
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-retry
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Samuel Kadolph
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '3.0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: activesupport
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '3.0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '3.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: bundler
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 1.1.5
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 1.1.5
62
+ - !ruby/object:Gem::Dependency
63
+ name: mocha
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 0.12.1
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 0.12.1
78
+ - !ruby/object:Gem::Dependency
79
+ name: rake
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: 0.9.2.2
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: 0.9.2.2
94
+ description: Retries reads and transactions when an ActiveRecord::StatementInvalid
95
+ occurs that matches a list of errors. Default list of errors includes errors related
96
+ to failover situations to allow for graceful failovers during a request and to attempt
97
+ prevention of data loss for temporary issue.
98
+ email:
99
+ - samuel@kadolph.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - lib/active_record/retry/rails.rb
105
+ - lib/active_record/retry/version.rb
106
+ - lib/active_record/retry.rb
107
+ - lib/activerecord-retry.rb
108
+ - test/active_record/retry_test.rb
109
+ homepage: http://samuelkadolph.github.com/activerecord-retry/
110
+ licenses: []
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ! '>='
119
+ - !ruby/object:Gem::Version
120
+ version: 1.9.2
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ! '>='
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ segments:
128
+ - 0
129
+ hash: -2741769345565936512
130
+ requirements: []
131
+ rubyforge_project:
132
+ rubygems_version: 1.8.24
133
+ signing_key:
134
+ specification_version: 3
135
+ summary: activerecord-retry add query retrying to ActiveRecord reads and transactions
136
+ on specific errors.
137
+ test_files:
138
+ - test/active_record/retry_test.rb