Skip to content

Commit 3d8f11b

Browse files
authored
Merge pull request alfa-jpn#11 from hawknewton/upstream_safe_disconnect
Provide a disconnect only option that is safe to use with transactions (with changes)
2 parents a10f4cc + fc16d54 commit 3d8f11b

File tree

3 files changed

+80
-16
lines changed

3 files changed

+80
-16
lines changed

README.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,16 @@ Or install it yourself as:
2222
$ gem install mysql2-aurora
2323

2424
## Usage
25-
This gem extends Mysql2::Client. You can use `aurora_max_retry` option.
25+
This gem extends Mysql2::Client. You can use `aurora_max_retry` and `aurora_disconnect_on_readonly` options.
2626

2727
```ruby
2828
Mysql2::Client.new(
2929
host: 'localhost',
3030
username: 'root',
3131
password: 'change_me',
3232
reconnect: true,
33-
aurora_max_retry: 5
33+
aurora_max_retry: 5,
34+
aurora_disconnect_on_readonly: true
3435
)
3536
```
3637

@@ -44,8 +45,39 @@ development:
4445
password: change_me
4546
reconnect: true
4647
aurora_max_retry: 5
48+
aurora_disconnect_on_readonly: true
49+
4750
```
4851

52+
There are essentially two methods for handling and RDS Aurora failover. When there is an Aurora RDS failover event the primary writable server can change it's role to become a read_only replica. This can happen without active database connections droppping.
53+
This leaves the connection in a state where writes will fail, but the application belives it's connected to a writeable server. Writes will now perpetually fail until the database connection is closed and re-established connecting back to the new primary.
54+
55+
To provide automatic recovery from this method you can use either a graceful retry, or an immediate disconnection option.
56+
57+
### Retry
58+
59+
Setting aurora_max_retry, mysql2 will not disconnect and automatically attempt re-connection to the database when a read_only error message is encountered.
60+
This has the benefit that to the application the error is transparent and the query will be re-run against the new primary when the connection succeeds.
61+
62+
It is however not safe to use with transactions
63+
64+
Consider:
65+
66+
* Transaction is started on the primary server A
67+
* Failover event occurs, A is now readonly
68+
* Application issues a write statement, read_only exception is thrown
69+
* mysql2-aurora gem handles this by reconnecting transparently to the new primary B
70+
* Aplication continues issuing writes however on a new connection in auto-commit mode, no new transaction was started
71+
72+
The application remains un-aware it is now operating outside of a transaction, this can leave data in an inconcistent state, and issuing a ROLLBACK, or COMMIT will not have the expected outcome.
73+
74+
### Immediate disconnect
75+
76+
Setting aurora_disconnect_on_readonly to true, will cause mysql2 to close the connection to the database on read_only exception. The original exception will be thrown up the stack to the application.
77+
With the database connection disconnected, the next statement will hit the disconnected error and the application can handle this as it would normally when been disconnected from the database.
78+
79+
This is safe with transactions.
80+
4981
## Contributing
5082

5183
Bug reports and pull requests are welcome on GitHub at https://github.com/alfa-jpn/mysql2-aurora.

lib/mysql2/aurora.rb

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ class Client
1313
# @note [Override] with reconnect options
1414
# @param [Hash] opts Options
1515
# @option opts [Integer] aurora_max_retry Max retry count, when failover. (Default: 5)
16+
# @option opts [Bool] aurora_disconnect_on_readonly, when readonly exception hit terminate the connection (Default: false)
1617
def initialize(opts)
17-
@opts = Mysql2::Util.key_hash_as_symbols(opts)
18+
@opts = Mysql2::Util.key_hash_as_symbols(opts)
1819
@max_retry = @opts.delete(:aurora_max_retry) || 5
20+
@disconnect_only = @opts.delete(:aurora_disconnect_on_readonly) || false
1921
reconnect!
2022
end
2123

@@ -27,19 +29,23 @@ def query(*args)
2729
begin
2830
client.query(*args)
2931
rescue Mysql2::Error => e
32+
raise e unless e.message&.include?('--read-only')
33+
3034
try_count += 1
3135

32-
if e.message&.include?('--read-only') && try_count <= @max_retry
36+
if @disconnect_only
37+
warn '[mysql2-aurora] Database is readonly, Aurora failover event likely occured, closing database connection'
38+
disconnect!
39+
elsif try_count <= @max_retry
3340
retry_interval_seconds = [1.5 * (try_count - 1), 10].min
3441

3542
warn "[mysql2-aurora] Database is readonly. Retry after #{retry_interval_seconds}seconds"
3643
sleep retry_interval_seconds
3744
reconnect!
38-
3945
retry
40-
else
41-
raise e
4246
end
47+
48+
raise e
4349
end
4450
end
4551

@@ -48,16 +54,19 @@ def query(*args)
4854
def reconnect!
4955
query_options = (@client&.query_options&.dup || {})
5056

51-
begin
52-
@client&.close
53-
rescue StandardError
54-
nil
55-
end
57+
disconnect!
5658

5759
@client = Mysql2::Aurora::ORIGINAL_CLIENT_CLASS.new(@opts)
5860
@client.query_options.merge!(query_options)
5961
end
6062

63+
# Close connection to database server
64+
def disconnect!
65+
@client&.close
66+
rescue StandardError
67+
nil
68+
end
69+
6170
# Delegate method call to client.
6271
# @param [String] name Method name
6372
# @param [Array] args Method arguments

spec/mysql2/aurora_spec.rb

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
RSpec.describe Mysql2::Aurora::Client do
22
let :client do
33
Mysql2::Client.new(
4-
host: ENV['TEST_DB_HOST'],
5-
username: ENV['TEST_DB_USER'],
6-
password: ENV['TEST_DB_PASS'],
7-
aurora_max_retry: 10
4+
host: ENV['TEST_DB_HOST'],
5+
username: ENV['TEST_DB_USER'],
6+
password: ENV['TEST_DB_PASS'],
7+
aurora_max_retry: 10,
8+
aurora_disconnect_on_readonly: aurora_disconnect_on_readonly
89
)
910
end
1011

12+
let(:aurora_disconnect_on_readonly) { false }
13+
1114
describe 'Mysql2::Aurora::VERSION' do
1215
subject do
1316
Mysql2::Aurora::VERSION
@@ -49,6 +52,26 @@
4952
end
5053

5154
describe '#query' do
55+
context 'When aurora_disconnect_on_readonly is true' do
56+
let(:aurora_disconnect_on_readonly) { true }
57+
58+
before :each do
59+
allow(client).to receive(:warn)
60+
allow(client.client).to receive(:query).and_raise(Mysql2::Error, 'ERROR 1290 (HY000): The MySQL server is running with the --read-only option so it cannot execute this statement')
61+
end
62+
63+
subject do
64+
client.query('SELECT CURRENT_USER() AS user')
65+
end
66+
67+
describe '#query' do
68+
it 'disconnects immediately' do
69+
expect(client).to receive(:disconnect!)
70+
expect { subject }.to raise_error(Mysql2::Error)
71+
end
72+
end
73+
end
74+
5275
subject do
5376
client.query('SELECT CURRENT_USER() AS user')
5477
end

0 commit comments

Comments
 (0)