Skip to content

Commit 64d7ff8

Browse files
committed
Allow the garbage collector to close connections
1 parent 45ba8d9 commit 64d7ff8

File tree

5 files changed

+99
-8
lines changed

5 files changed

+99
-8
lines changed

ext/mysql2/client.c

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ void decr_mysql2_client(mysql_client_wrapper *wrapper)
233233

234234
if (wrapper->refcount == 0) {
235235
#ifndef _WIN32
236-
if (wrapper->connected) {
236+
if (wrapper->connected && !wrapper->automatic_close) {
237237
/* The client is being garbage collected while connected. Prevent
238238
* mysql_close() from sending a mysql-QUIT or from calling shutdown() on
239239
* the socket by invalidating it. invalidate_fd() will drop this
@@ -260,6 +260,11 @@ 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+
#ifndef _WIN32
264+
wrapper->automatic_close = 0;
265+
#else
266+
wrapper->automatic_close = 1;
267+
#endif
263268
wrapper->server_version = 0;
264269
wrapper->reconnect_enabled = 0;
265270
wrapper->connect_timeout = 0;
@@ -382,12 +387,9 @@ static VALUE rb_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE po
382387

383388
/*
384389
* 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.
390+
* To have the garbage collector close the connection, enable +automatic_close+.
388391
*
389-
* @see http://dev.mysql.com/doc/en/communication-errors.html
390-
* @return [void]
392+
* @return [nil]
391393
*/
392394
static VALUE rb_mysql_client_close(VALUE self) {
393395
GET_CLIENT(self);
@@ -1085,6 +1087,35 @@ static VALUE rb_mysql_client_encoding(VALUE self) {
10851087
}
10861088
#endif
10871089

1090+
/* call-seq:
1091+
* client.automatic_close?
1092+
*
1093+
* @return [Boolean]
1094+
*/
1095+
static VALUE get_automatic_close(VALUE self) {
1096+
GET_CLIENT(self);
1097+
return wrapper->automatic_close ? Qtrue : Qfalse;
1098+
}
1099+
1100+
/* call-seq:
1101+
* client.automatic_close = true
1102+
*
1103+
* Set this to +true+ to let the garbage collector close this connection.
1104+
*/
1105+
static VALUE set_automatic_close(VALUE self, VALUE value) {
1106+
GET_CLIENT(self);
1107+
if (RTEST(value)) {
1108+
wrapper->automatic_close = 1;
1109+
} else {
1110+
#ifndef _WIN32
1111+
wrapper->automatic_close = 0;
1112+
#else
1113+
rb_raise(cMysql2Error, "Connections are always closed by garbage collector on Windows");
1114+
#endif
1115+
}
1116+
return value;
1117+
}
1118+
10881119
/* call-seq:
10891120
* client.reconnect = true
10901121
*
@@ -1268,6 +1299,8 @@ void init_mysql2_client() {
12681299
rb_define_method(cMysql2Client, "more_results?", rb_mysql_client_more_results, 0);
12691300
rb_define_method(cMysql2Client, "next_result", rb_mysql_client_next_result, 0);
12701301
rb_define_method(cMysql2Client, "store_result", rb_mysql_client_store_result, 0);
1302+
rb_define_method(cMysql2Client, "automatic_close?", get_automatic_close, 0);
1303+
rb_define_method(cMysql2Client, "automatic_close=", set_automatic_close, 1);
12711304
rb_define_method(cMysql2Client, "reconnect=", set_reconnect, 1);
12721305
rb_define_method(cMysql2Client, "warning_count", rb_mysql_client_warning_count, 0);
12731306
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].to_i)

spec/mysql2/client_spec.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,62 @@ def run_gc
210210
expect { client.query('SELECT 1') }.to_not raise_exception
211211
end
212212

213+
context "#automatic_close" do
214+
if RUBY_PLATFORM =~ /mingw|mswin/
215+
it "is enabled by default" do
216+
client = Mysql2::Client.new(DatabaseCredentials['root'])
217+
expect(client.automatic_close?).to be(true)
218+
end
219+
220+
it "cannot be disabled" do
221+
expect {
222+
Mysql2::Client.new(DatabaseCredentials['root'].merge(:automatic_close => false))
223+
}.to raise_error(Mysql2::Error)
224+
225+
client = Mysql2::Client.new(DatabaseCredentials['root'])
226+
227+
expect { client.automatic_close = false }.to raise_error(Mysql2::Error)
228+
expect { client.automatic_close = true }.to_not raise_error
229+
end
230+
else
231+
it "is disabled by default" do
232+
client = Mysql2::Client.new(DatabaseCredentials['root'])
233+
expect(client.automatic_close?).to be(false)
234+
end
235+
236+
it "can be configured" do
237+
client = Mysql2::Client.new(DatabaseCredentials['root'].merge(:automatic_close => true))
238+
expect(client.automatic_close?).to be(true)
239+
end
240+
241+
it "can be assigned" do
242+
client = Mysql2::Client.new(DatabaseCredentials['root'])
243+
client.automatic_close = true
244+
expect(client.automatic_close?).to be(true)
245+
246+
client.automatic_close = false
247+
expect(client.automatic_close?).to be(false)
248+
249+
client.automatic_close = 9
250+
expect(client.automatic_close?).to be(true)
251+
252+
client.automatic_close = nil
253+
expect(client.automatic_close?).to be(false)
254+
end
255+
end
256+
257+
it "should terminate connections during garbage collection" do
258+
run_gc
259+
expect {
260+
Mysql2::Client.new(DatabaseCredentials['root'].merge(:automatic_close => true)).query('SELECT 1')
261+
run_gc
262+
}.to_not change {
263+
@client.query("SHOW STATUS LIKE 'Aborted_%'").to_a +
264+
@client.query("SHOW STATUS LIKE 'Threads_connected'").to_a
265+
}
266+
end
267+
end
268+
213269
it "should be able to connect to database with numeric-only name" do
214270
creds = DatabaseCredentials['numericuser']
215271
@client.query "CREATE DATABASE IF NOT EXISTS `#{creds['database']}`"

spec/spec_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,5 +90,6 @@ def with_internal_encoding(encoding)
9090
"test", "test", 'val1', 'val1,val2'
9191
)
9292
]
93+
client.close
9394
end
9495
end

0 commit comments

Comments
 (0)