activerecord-mysql-reconnect 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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