activerecord-mysql-reconnect 0.0.1 → 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 CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- YjQ4MjM5ZDVlNmU3OWY3YTc4OGZjZGFhNjM4ZDgyYjM3YjZmM2I1Yg==
5
- data.tar.gz: !binary |-
6
- ZGJkZjJmZDJkNDA4OWVhY2EzOTZjOGEzYzVlMDkyNzJjZDY5MzZiOA==
2
+ SHA1:
3
+ metadata.gz: 4dc078edb058b76e0a2608757ee28874723e17b0
4
+ data.tar.gz: 3ce76b372e6dcc6c0ea9a1d3c92852376671aa86
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- MTBkOTJmOGU1ZTU5YWNmNjk4NTU1ZTRlMjE5YjM3ZTkxZjZhNjY2MjEwZjU1
10
- NzNlMGU2MGZjYjVlZDU1MmFhYjFiNTQ5ZmI1OGZlZmRhZjRmMjI4ZDg5M2Jm
11
- YmZiYzMwMzcwNmEyZmY2Y2M0NTkwN2IxOWY5NWE1MGNiNjQ1Mzk=
12
- data.tar.gz: !binary |-
13
- M2ZmNjNmNjdlMTg2YzcwZTU0OWJiMWU2ZThkMWUwYThiNzNmMmI5ZGZkNzIy
14
- MzNjNjc3NTZkOTg2MjY2M2RjZmFmZjAyYmE0Y2IxMDNjYzE2OWE3ZjYxNDZk
15
- MzUzNWE3YzE3NDQyNmFmNjY1OWI1ODhmMTE2MWQzMThmOGQyYTA=
6
+ metadata.gz: f026083fd7126a3910045110d0fb697b004667eef51ad4bbaa201256ecdd8b216b1ba11401b19faa730e4a0f7bc2207ff11d62e25eb0c5c0c0508f942d2ee1d9
7
+ data.tar.gz: b0a162b084442e8fc6990f736f55cfedd5c36fb620403fbe3b1ec8351b8a5bf36b69058dc1564051474f14aec4da8667341e45ee8c0cc7acbe5295f1cfe9ab52
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ test.rb
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ --colour
data/README.md CHANGED
@@ -1,7 +1,10 @@
1
- # Activerecord::Mysql::Reconnect
1
+ # activerecord-mysql-reconnect
2
2
 
3
3
  It is the library to reconnect automatically when ActiveRecord is disconnected from MySQL.
4
4
 
5
+ [![Gem Version](https://badge.fury.io/rb/activerecord-mysql-reconnect.png)](http://badge.fury.io/rb/activerecord-mysql-reconnect)
6
+ [![Build Status](https://drone.io/bitbucket.org/winebarrel/activerecord-mysql-reconnect/status.png)](https://drone.io/bitbucket.org/winebarrel/activerecord-mysql-reconnect/latest)
7
+
5
8
  ## Installation
6
9
 
7
10
  Add this line to your application's Gemfile:
@@ -19,8 +22,6 @@ Or install it yourself as:
19
22
  ## Usage
20
23
 
21
24
  ```ruby
22
- #!/usr/bin/env ruby
23
- require 'active_record'
24
25
  require 'activerecord/mysql/reconnect'
25
26
  require 'logger'
26
27
 
@@ -28,7 +29,7 @@ ActiveRecord::Base.establish_connection(
28
29
  adapter: 'mysql2',
29
30
  host: '127.0.0.1',
30
31
  username: 'root',
31
- database: 'test',
32
+ database: 'employees',
32
33
  )
33
34
 
34
35
  ActiveRecord::Base.logger = Logger.new($stdout)
@@ -55,6 +56,29 @@ MySQL server has gone away. Trying to reconnect in 0.5 seconds.
55
56
  300024
56
57
  ```
57
58
 
58
- ## Link
59
+ ### without_retry
60
+
61
+ ```ruby
62
+ ActiveRecord::Base.without_retry do
63
+ Employee.count
64
+ end
65
+ ```
66
+
67
+ ### retryable_transaction
59
68
 
60
- * [RubyGems.org site](http://rubygems.org/gems/activerecord-mysql-reconnect)
69
+ ```ruby
70
+ ActiveRecord::Base.retryable_transaction do
71
+ Employee.create(emp_no: 1, birth_date: Time.now, first_name: 'Scott', last_name: 'Tiger', hire_date: Time.now)
72
+ Employee.create(emp_no: 2, birth_date: Time.now, first_name: 'Scott', last_name: 'Tiger', hire_date: Time.now)
73
+ Employee.create(emp_no: 3, birth_date: Time.now, first_name: 'Scott', last_name: 'Tiger', hire_date: Time.now)
74
+ end
75
+ ```
76
+
77
+ ## Running tests
78
+
79
+ ```sh
80
+ mysql.server start
81
+ export ACTIVERECORD_MYSQL_RECONNECT_MYSQL_RESTART='mysql.server restart'
82
+ bundle install
83
+ bundle exec rake
84
+ ```
data/Rakefile CHANGED
@@ -1 +1,5 @@
1
- require "bundler/gem_tasks"
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new('spec')
5
+ task :default => :spec
@@ -4,22 +4,23 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'activerecord/mysql/reconnect/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "activerecord-mysql-reconnect"
7
+ spec.name = 'activerecord-mysql-reconnect'
8
8
  spec.version = Activerecord::Mysql::Reconnect::VERSION
9
- spec.authors = ["Genki Sugawara"]
10
- spec.email = ["sugawara@cookpad.com"]
9
+ spec.authors = ['Genki Sugawara']
10
+ spec.email = ['sugawara@cookpad.com']
11
11
  spec.description = %q{It is the library to reconnect automatically when ActiveRecord is disconnected from MySQL.}
12
- spec.summary = %q{It is the library to reconnect automatically when ActiveRecord is disconnected from MySQL.}
13
- spec.homepage = "https://bitbucket.org/winebarrel/activerecord-mysql-reconnect"
14
- spec.license = "MIT"
12
+ spec.summary = spec.description
13
+ spec.homepage = 'https://bitbucket.org/winebarrel/activerecord-mysql-reconnect'
14
+ spec.license = 'MIT'
15
15
 
16
16
  spec.files = `git ls-files`.split($/)
17
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
19
+ spec.require_paths = ['lib']
20
20
 
21
- spec.add_dependency "activerecord", "~> 3.2.14"
22
- spec.add_dependency "retryable", "~> 1.3.3"
23
- spec.add_development_dependency "bundler", "~> 1.3"
24
- spec.add_development_dependency "rake"
21
+ spec.add_dependency 'activerecord', '~> 3.2.14'
22
+ spec.add_development_dependency 'bundler', '~> 1.3'
23
+ spec.add_development_dependency 'rake'
24
+ spec.add_development_dependency 'rspec', '>= 2.14.1'
25
+ spec.add_development_dependency 'mysql2'
25
26
  end
@@ -1,35 +1,125 @@
1
1
  require 'active_record'
2
2
  require 'active_record/connection_adapters/abstract_mysql_adapter'
3
3
  require 'active_support'
4
- require 'retryable'
4
+ require 'logger'
5
5
 
6
6
  class ActiveRecord::Base
7
7
  class_attribute :execution_tries
8
8
  class_attribute :execution_retry_wait
9
+
10
+ class << self
11
+ def without_retry
12
+ begin
13
+ Thread.current[ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::WITHOUT_RETRY_KEY] = true
14
+ yield
15
+ ensure
16
+ Thread.current[ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::WITHOUT_RETRY_KEY] = nil
17
+ end
18
+ end
19
+
20
+ def retryable_transaction
21
+ begin
22
+ Thread.current[ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::RETRYABLE_TRANSACTION_KEY] = []
23
+
24
+ ActiveRecord::Base.transaction do
25
+ yield
26
+ end
27
+ ensure
28
+ Thread.current[ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::RETRYABLE_TRANSACTION_KEY] = nil
29
+ end
30
+ end
31
+ end
9
32
  end
10
33
 
11
34
  class ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
12
- MYSQL_SERVER_HAS_GONE_AWAY = 'MySQL server has gone away'
13
- DEFAULT_EXECUTION_TRIES = 1
35
+ DEFAULT_EXECUTION_TRIES = 3
14
36
  DEFAULT_EXECUTION_RETRY_WAIT = 0.5
15
37
 
38
+ ERROR_MESSAGES = [
39
+ 'MySQL server has gone away',
40
+ 'Server shutdown in progress',
41
+ 'closed MySQL connection',
42
+ "Can't connect to MySQL server",
43
+ 'Query execution was interrupted',
44
+ ]
45
+
46
+ WITHOUT_RETRY_KEY = 'activerecord-mysql-reconnect-without-retry'
47
+ RETRYABLE_TRANSACTION_KEY = 'activerecord-mysql-reconnect-transaction-retry'
48
+
16
49
  def execute_with_reconnect(sql, name = nil)
17
- retryable_options = {
18
- :tries => (ActiveRecord::Base.execution_tries || DEFAULT_EXECUTION_TRIES),
19
- :matching => /#{MYSQL_SERVER_HAS_GONE_AWAY}/,
20
- :sleep => proc {|n|
21
- wait = (ActiveRecord::Base.execution_retry_wait || DEFAULT_EXECUTION_RETRY_WAIT) * (n + 1)
22
- logger.warn("#{MYSQL_SERVER_HAS_GONE_AWAY}. Trying to reconnect in #{wait} seconds.")
23
- sleep(wait)
24
- reconnect!
25
- 0
26
- }
27
- }
28
-
29
- retryable(retryable_options) do
30
- execute_without_reconnect(sql, name)
50
+ retryable(sql, name) do |sql_names|
51
+ retval = nil
52
+
53
+ sql_names.each do |s, n|
54
+ retval = execute_without_reconnect(s, n)
55
+ end
56
+
57
+ add_sql_to_transaction(sql, name)
58
+ retval
31
59
  end
32
60
  end
33
-
61
+
34
62
  alias_method_chain :execute, :reconnect
63
+
64
+ private
65
+
66
+ def retryable(sql, name, &block)
67
+ tries = ActiveRecord::Base.execution_tries || DEFAULT_EXECUTION_TRIES
68
+ logger = ActiveRecord::Base.logger || Logger.new($stderr)
69
+ block_with_reconnect = nil
70
+ retval = nil
71
+ sql_names = [[sql, name]]
72
+
73
+ retryable_loop(tries) do |n|
74
+ begin
75
+ retval = (block_with_reconnect || block).call(sql_names)
76
+ break
77
+ rescue => e
78
+ if not without_retry? and (tries.zero? or n < tries) and e.message =~ Regexp.union(ERROR_MESSAGES)
79
+ unless block_with_reconnect
80
+ block_with_reconnect = proc {|i| reconnect! ; block.call(i) }
81
+ end
82
+
83
+ sql_names = merge_transaction(sql, name)
84
+ wait = (ActiveRecord::Base.execution_retry_wait || DEFAULT_EXECUTION_RETRY_WAIT) * n
85
+ logger.warn("MySQL server has gone away. Trying to reconnect in #{wait} seconds. (cause: #{e} [#{e.class}])")
86
+ sleep(wait)
87
+
88
+ next
89
+ else
90
+ raise e
91
+ end
92
+ end
93
+ end
94
+
95
+ return retval
96
+ end
97
+
98
+ def retryable_loop(n)
99
+ if n.zero?
100
+ loop { n += 1 ; yield(n) }
101
+ else
102
+ n.times {|i| yield(i + 1) }
103
+ end
104
+ end
105
+
106
+ def without_retry?
107
+ !!Thread.current[WITHOUT_RETRY_KEY]
108
+ end
109
+
110
+ def add_sql_to_transaction(sql, name)
111
+ if (buf = Thread.current[RETRYABLE_TRANSACTION_KEY])
112
+ buf << [sql, name]
113
+ end
114
+ end
115
+
116
+ def merge_transaction(sql, name)
117
+ sql_name = [sql, name]
118
+
119
+ if (buf = Thread.current[RETRYABLE_TRANSACTION_KEY])
120
+ buf + [sql_name]
121
+ else
122
+ [sql_name]
123
+ end
124
+ end
35
125
  end
@@ -1,7 +1,7 @@
1
1
  module Activerecord
2
2
  module Mysql
3
3
  module Reconnect
4
- VERSION = "0.0.1"
4
+ VERSION = '0.1.0'
5
5
  end
6
6
  end
7
7
  end
@@ -0,0 +1,168 @@
1
+ describe Hash do
2
+ it 'select all' do
3
+ expect {
4
+ expect(Employee.all.length).to eq(300024)
5
+ mysql_restart
6
+ expect(Employee.all.length).to eq(300024)
7
+ }.to_not raise_error
8
+ end
9
+
10
+ it 'count' do
11
+ expect {
12
+ expect(Employee.count).to eq(300024)
13
+ mysql_restart
14
+ expect(Employee.count).to eq(300024)
15
+ }.to_not raise_error
16
+ end
17
+
18
+ it 'on select' do
19
+ expect {
20
+ thread_running = false
21
+
22
+ th = Thread.start {
23
+ thread_running = true
24
+ expect(Employee.where(:id => 1).pluck('sleep(15)')).to eq([1])
25
+ thread_running = false
26
+ }
27
+
28
+ th.abort_on_exception = true
29
+ sleep 3
30
+ expect(thread_running).to be_true
31
+ mysql_restart
32
+ expect(Employee.count).to be >= 300024
33
+ th.join
34
+ }.to_not raise_error
35
+ end
36
+
37
+ it 'on insert' do
38
+ expect {
39
+ thread_running = false
40
+
41
+ th = Thread.start {
42
+ thread_running = true
43
+ emp = Employee.create(
44
+ :emp_no => 1,
45
+ :birth_date => Time.now,
46
+ :first_name => "' + sleep(15) + '",
47
+ :last_name => 'Tiger',
48
+ :hire_date => Time.now
49
+ )
50
+ thread_running = false
51
+ expect(emp.id).to eq(300025)
52
+ expect(emp.emp_no).to eq(1)
53
+ }
54
+
55
+ th.abort_on_exception = true
56
+ sleep 3
57
+ expect(thread_running).to be_true
58
+ mysql_restart
59
+ expect(Employee.count).to be >= 300024
60
+ th.join
61
+ }.to_not raise_error
62
+ end
63
+
64
+ it 'op update' do
65
+ expect {
66
+ thread_running = false
67
+
68
+ th = Thread.start {
69
+ thread_running = true
70
+ emp = Employee.where(:id => 1).first
71
+ emp.first_name = "' + sleep(15) + '"
72
+ emp.last_name = 'ZapZapZap'
73
+ emp.save!
74
+ thread_running = false
75
+
76
+ emp = Employee.where(:id => 1).first
77
+ expect(emp.last_name).to eq('ZapZapZap')
78
+ }
79
+
80
+ th.abort_on_exception = true
81
+ sleep 3
82
+ expect(thread_running).to be_true
83
+ mysql_restart
84
+ expect(Employee.count).to eq(300024)
85
+ th.join
86
+ }.to_not raise_error
87
+ end
88
+
89
+ it 'without_retry' do
90
+ expect {
91
+ ActiveRecord::Base.without_retry do
92
+ Employee.count
93
+ mysql_restart
94
+ Employee.count
95
+ end
96
+ }.to raise_error(ActiveRecord::StatementInvalid)
97
+ end
98
+
99
+ it 'transaction' do
100
+ expect {
101
+ expect(Employee.count).to eq(300024)
102
+
103
+ ActiveRecord::Base.transaction do
104
+ emp = Employee.create(
105
+ :emp_no => 1,
106
+ :birth_date => Time.now,
107
+ :first_name => 'Scott',
108
+ :last_name => 'Tiger',
109
+ :hire_date => Time.now
110
+ )
111
+ expect(emp.id).to eq(300025)
112
+ expect(emp.emp_no).to eq(1)
113
+ mysql_restart
114
+ emp = Employee.create(
115
+ :emp_no => 2,
116
+ :birth_date => Time.now,
117
+ :first_name => 'Scott',
118
+ :last_name => 'Tiger',
119
+ :hire_date => Time.now
120
+ )
121
+ expect(emp.id).to eq(300025)
122
+ expect(emp.emp_no).to eq(2)
123
+ end
124
+
125
+ expect(Employee.count).to eq(300025)
126
+ }.to_not raise_error
127
+ end
128
+
129
+ it 'retryable_transaction' do
130
+ expect {
131
+ expect(Employee.count).to eq(300024)
132
+
133
+ ActiveRecord::Base.retryable_transaction do
134
+ emp = Employee.create(
135
+ :emp_no => 1,
136
+ :birth_date => Time.now,
137
+ :first_name => 'Scott',
138
+ :last_name => 'Tiger',
139
+ :hire_date => Time.now
140
+ )
141
+ expect(emp.id).to eq(300025)
142
+ expect(emp.emp_no).to eq(1)
143
+ mysql_restart
144
+ emp = Employee.create(
145
+ :emp_no => 2,
146
+ :birth_date => Time.now,
147
+ :first_name => 'Scott',
148
+ :last_name => 'Tiger',
149
+ :hire_date => Time.now
150
+ )
151
+ expect(emp.id).to eq(300026)
152
+ expect(emp.emp_no).to eq(2)
153
+ mysql_restart
154
+ emp = Employee.create(
155
+ :emp_no => 3,
156
+ :birth_date => Time.now,
157
+ :first_name => 'Scott',
158
+ :last_name => 'Tiger',
159
+ :hire_date => Time.now
160
+ )
161
+ expect(emp.id).to eq(300027)
162
+ expect(emp.emp_no).to eq(3)
163
+ end
164
+
165
+ expect(Employee.count).to eq(300027)
166
+ }.to_not raise_error
167
+ end
168
+ end