Skip to content

Commit 924d934

Browse files
committed
Merge pull request #684 from cbandy/close-during-gc
Close connection during garbage collection
2 parents b35a641 + 890345e commit 924d934

File tree

4 files changed

+122
-36
lines changed

4 files changed

+122
-36
lines changed

ext/mysql2/client.c

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ void decr_mysql2_client(mysql_client_wrapper *wrapper)
234234

235235
if (wrapper->refcount == 0) {
236236
#ifndef _WIN32
237-
if (wrapper->connected) {
237+
if (wrapper->connected && !wrapper->automatic_close) {
238238
/* The client is being garbage collected while connected. Prevent
239239
* mysql_close() from sending a mysql-QUIT or from calling shutdown() on
240240
* the socket by invalidating it. invalidate_fd() will drop this
@@ -260,6 +260,7 @@ static VALUE allocate(VALUE klass) {
260260
obj = Data_Make_Struct(klass, mysql_client_wrapper, rb_mysql_client_mark, rb_mysql_client_free, wrapper);
261261
wrapper->encoding = Qnil;
262262
MARK_CONN_INACTIVE(self);
263+
wrapper->automatic_close = 1;
263264
wrapper->server_version = 0;
264265
wrapper->reconnect_enabled = 0;
265266
wrapper->connect_timeout = 0;
@@ -381,13 +382,12 @@ static VALUE rb_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE po
381382
}
382383

383384
/*
384-
* Terminate the connection; call this when the connection is no longer needed.
385-
* The garbage collector can close the connection, but doing so emits an
386-
* "Aborted connection" error on the server and increments the Aborted_clients
387-
* status variable.
385+
* Immediately disconnect from the server; normally the garbage collector
386+
* will disconnect automatically when a connection is no longer needed.
387+
* Explicitly closing this will free up server resources sooner than waiting
388+
* for the garbage collector.
388389
*
389-
* @see http://dev.mysql.com/doc/en/communication-errors.html
390-
* @return [void]
390+
* @return [nil]
391391
*/
392392
static VALUE rb_mysql_client_close(VALUE self) {
393393
GET_CLIENT(self);
@@ -1081,6 +1081,39 @@ static VALUE rb_mysql_client_encoding(VALUE self) {
10811081
}
10821082
#endif
10831083

1084+
/* call-seq:
1085+
* client.automatic_close?
1086+
*
1087+
* @return [Boolean]
1088+
*/
1089+
static VALUE get_automatic_close(VALUE self) {
1090+
GET_CLIENT(self);
1091+
return wrapper->automatic_close ? Qtrue : Qfalse;
1092+
}
1093+
1094+
/* call-seq:
1095+
* client.automatic_close = false
1096+
*
1097+
* Set this to +false+ to leave the connection open after it is garbage
1098+
* collected. To avoid "Aborted connection" errors on the server, explicitly
1099+
* call +close+ when the connection is no longer needed.
1100+
*
1101+
* @see http://dev.mysql.com/doc/en/communication-errors.html
1102+
*/
1103+
static VALUE set_automatic_close(VALUE self, VALUE value) {
1104+
GET_CLIENT(self);
1105+
if (RTEST(value)) {
1106+
wrapper->automatic_close = 1;
1107+
} else {
1108+
#ifndef _WIN32
1109+
wrapper->automatic_close = 0;
1110+
#else
1111+
rb_warn("Connections are always closed by garbage collector on Windows");
1112+
#endif
1113+
}
1114+
return value;
1115+
}
1116+
10841117
/* call-seq:
10851118
* client.reconnect = true
10861119
*
@@ -1264,6 +1297,8 @@ void init_mysql2_client() {
12641297
rb_define_method(cMysql2Client, "more_results?", rb_mysql_client_more_results, 0);
12651298
rb_define_method(cMysql2Client, "next_result", rb_mysql_client_next_result, 0);
12661299
rb_define_method(cMysql2Client, "store_result", rb_mysql_client_store_result, 0);
1300+
rb_define_method(cMysql2Client, "automatic_close?", get_automatic_close, 0);
1301+
rb_define_method(cMysql2Client, "automatic_close=", set_automatic_close, 1);
12671302
rb_define_method(cMysql2Client, "reconnect=", set_reconnect, 1);
12681303
rb_define_method(cMysql2Client, "warning_count", rb_mysql_client_warning_count, 0);
12691304
rb_define_method(cMysql2Client, "query_info_string", rb_mysql_info, 0);

ext/mysql2/client.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ typedef struct {
4343
int reconnect_enabled;
4444
unsigned int connect_timeout;
4545
int active;
46+
int automatic_close;
4647
int connected;
4748
int initialized;
4849
int refcount;

lib/mysql2/client.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ def initialize(opts = {})
3030
opts[:connect_timeout] = 120 unless opts.key?(:connect_timeout)
3131

3232
# TODO: stricter validation rather than silent massaging
33-
[:reconnect, :connect_timeout, :local_infile, :read_timeout, :write_timeout, :default_file, :default_group, :secure_auth, :init_command].each do |key|
33+
[:reconnect, :connect_timeout, :local_infile, :read_timeout, :write_timeout, :default_file, :default_group, :secure_auth, :init_command, :automatic_close].each do |key|
3434
next unless opts.key?(key)
3535
case key
36-
when :reconnect, :local_infile, :secure_auth
36+
when :reconnect, :local_infile, :secure_auth, :automatic_close
3737
send(:"#{key}=", !!opts[key]) # rubocop:disable Style/DoubleNegation
3838
when :connect_timeout, :read_timeout, :write_timeout
3939
send(:"#{key}=", opts[key]) unless opts[key].nil?

spec/mysql2/client_spec.rb

Lines changed: 77 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# encoding: UTF-8
22
require 'spec_helper'
3+
require 'stringio'
34

45
RSpec.describe Mysql2::Client do
56
context "using defaults file" do
@@ -172,48 +173,97 @@ def run_gc
172173
expect {
173174
Mysql2::Client.new(DatabaseCredentials['root']).close
174175
}.to_not change {
175-
@client.query("SHOW STATUS LIKE 'Aborted_clients'").first['Value'].to_i
176+
@client.query("SHOW STATUS LIKE 'Aborted_%'").to_a +
177+
@client.query("SHOW STATUS LIKE 'Threads_connected'").to_a
176178
}
177179
end
178180

179181
it "should not leave dangling connections after garbage collection" do
180182
run_gc
183+
expect {
184+
expect {
185+
10.times do
186+
Mysql2::Client.new(DatabaseCredentials['root']).query('SELECT 1')
187+
end
188+
}.to change {
189+
@client.query("SHOW STATUS LIKE 'Threads_connected'").first['Value'].to_i
190+
}.by(10)
181191

182-
client = Mysql2::Client.new(DatabaseCredentials['root'])
183-
before_count = client.query("SHOW STATUS LIKE 'Threads_connected'").first['Value'].to_i
192+
run_gc
193+
}.to_not change {
194+
@client.query("SHOW STATUS LIKE 'Aborted_%'").to_a +
195+
@client.query("SHOW STATUS LIKE 'Threads_connected'").to_a
196+
}
197+
end
184198

185-
10.times do
186-
Mysql2::Client.new(DatabaseCredentials['root']).query('SELECT 1')
199+
context "#automatic_close" do
200+
it "is enabled by default" do
201+
client = Mysql2::Client.new(DatabaseCredentials['root'])
202+
expect(client.automatic_close?).to be(true)
187203
end
188-
after_count = client.query("SHOW STATUS LIKE 'Threads_connected'").first['Value'].to_i
189-
expect(after_count).to eq(before_count + 10)
190204

191-
run_gc
192-
final_count = client.query("SHOW STATUS LIKE 'Threads_connected'").first['Value'].to_i
193-
expect(final_count).to eq(before_count)
194-
end
205+
if RUBY_PLATFORM =~ /mingw|mswin/
206+
it "cannot be disabled" do
207+
stderr, $stderr = $stderr, StringIO.new
195208

196-
it "should not close connections when running in a child process" do
197-
pending("fork is not available on this platform") unless Process.respond_to?(:fork)
209+
begin
210+
Mysql2::Client.new(DatabaseCredentials['root'].merge(:automatic_close => false))
211+
expect($stderr.string).to include('always closed by garbage collector')
212+
$stderr.reopen
198213

199-
run_gc
200-
client = Mysql2::Client.new(DatabaseCredentials['root'])
214+
client = Mysql2::Client.new(DatabaseCredentials['root'])
215+
client.automatic_close = false
216+
expect($stderr.string).to include('always closed by garbage collector')
217+
$stderr.reopen
201218

202-
# this empty `fork` call fixes this tests on RBX; without it, the next
203-
# `fork` call hangs forever. WTF?
204-
fork {}
219+
expect { client.automatic_close = true }.to_not change { $stderr.string }
220+
ensure
221+
$stderr = stderr
222+
end
223+
end
224+
else
225+
it "can be configured" do
226+
client = Mysql2::Client.new(DatabaseCredentials['root'].merge(:automatic_close => false))
227+
expect(client.automatic_close?).to be(false)
228+
end
205229

206-
fork do
207-
client.query('SELECT 1')
208-
client = nil
209-
run_gc
210-
end
230+
it "can be assigned" do
231+
client = Mysql2::Client.new(DatabaseCredentials['root'])
232+
client.automatic_close = false
233+
expect(client.automatic_close?).to be(false)
234+
235+
client.automatic_close = true
236+
expect(client.automatic_close?).to be(true)
237+
238+
client.automatic_close = nil
239+
expect(client.automatic_close?).to be(false)
240+
241+
client.automatic_close = 9
242+
expect(client.automatic_close?).to be(true)
243+
end
244+
245+
it "should not close connections when running in a child process" do
246+
run_gc
247+
client = Mysql2::Client.new(DatabaseCredentials['root'])
248+
client.automatic_close = false
211249

212-
Process.wait
250+
# this empty `fork` call fixes this tests on RBX; without it, the next
251+
# `fork` call hangs forever. WTF?
252+
fork {}
253+
254+
fork do
255+
client.query('SELECT 1')
256+
client = nil
257+
run_gc
258+
end
213259

214-
# this will throw an error if the underlying socket was shutdown by the
215-
# child's GC
216-
expect { client.query('SELECT 1') }.to_not raise_exception
260+
Process.wait
261+
262+
# this will throw an error if the underlying socket was shutdown by the
263+
# child's GC
264+
expect { client.query('SELECT 1') }.to_not raise_exception
265+
end
266+
end
217267
end
218268

219269
it "should be able to connect to database with numeric-only name" do

0 commit comments

Comments
 (0)