Skip to content

Commit 82aa624

Browse files
committed
Use mysql's own read timeout option too
do_query implements its own read timeout using rb_wait_for_single_fd, but blocking operations, like mysql_ping, will still block until the tcp connection expires if there is a problem (multiple minutes usually). ActiveRecord's connection pool makes liberal use of ping, so an unresponsive mysql server can cause rails apps to hang even if read_timeout has been configured. We fix this by using the mysql library's own MYSQL_OPT_READ_TIMEOUT option. It fixes mysql_ping and seems not to interfere with with do_query. I don't think writing a test case for this is practical - it requires dropping packets to/from the mysql server after establishing the connection. I've tested it like this manually though and it makes all your fast fail dreams come true.
1 parent 89ee259 commit 82aa624

File tree

2 files changed

+21
-6
lines changed

2 files changed

+21
-6
lines changed

ext/mysql2/client.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,11 @@ static VALUE _mysql_client_options(VALUE self, int opt, VALUE value) {
613613
retval = &intval;
614614
break;
615615

616+
case MYSQL_OPT_READ_TIMEOUT:
617+
intval = NUM2INT(value);
618+
retval = &intval;
619+
break;
620+
616621
case MYSQL_OPT_LOCAL_INFILE:
617622
intval = (value == Qfalse ? 0 : 1);
618623
retval = &intval;
@@ -907,6 +912,20 @@ static VALUE set_connect_timeout(VALUE self, VALUE value) {
907912
return _mysql_client_options(self, MYSQL_OPT_CONNECT_TIMEOUT, value);
908913
}
909914

915+
static VALUE set_read_timeout(VALUE self, VALUE value) {
916+
long int sec;
917+
Check_Type(value, T_FIXNUM);
918+
sec = FIX2INT(value);
919+
if (sec < 0) {
920+
rb_raise(cMysql2Error, "read_timeout must be a positive integer, you passed %ld", sec);
921+
}
922+
// Set the instance variable here even though _mysql_client_options
923+
// might not succeed, because the timeout is used in other ways
924+
// elsewhere
925+
rb_iv_set(self, "@read_timeout", value);
926+
return _mysql_client_options(self, MYSQL_OPT_READ_TIMEOUT, value);
927+
}
928+
910929
static VALUE set_charset_name(VALUE self, VALUE value) {
911930
char * charset_name;
912931
#ifdef HAVE_RUBY_ENCODING_H
@@ -1012,6 +1031,7 @@ void init_mysql2_client() {
10121031

10131032
rb_define_private_method(cMysql2Client, "reconnect=", set_reconnect, 1);
10141033
rb_define_private_method(cMysql2Client, "connect_timeout=", set_connect_timeout, 1);
1034+
rb_define_private_method(cMysql2Client, "read_timeout=", set_read_timeout, 1);
10151035
rb_define_private_method(cMysql2Client, "local_infile=", set_local_infile, 1);
10161036
rb_define_private_method(cMysql2Client, "charset_name=", set_charset_name, 1);
10171037
rb_define_private_method(cMysql2Client, "ssl_set", set_ssl_options, 5);

lib/mysql2/client.rb

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,14 @@ def initialize(opts = {})
2121
initialize_ext
2222

2323
# Set MySQL connection options (each one is a call to mysql_options())
24-
[:reconnect, :connect_timeout, :local_infile].each do |key|
24+
[:reconnect, :connect_timeout, :local_infile, :read_timeout].each do |key|
2525
next unless opts.key?(key)
2626
send(:"#{key}=", opts[key])
2727
end
2828

2929
# force the encoding to utf8
3030
self.charset_name = opts[:encoding] || 'utf8'
3131

32-
@read_timeout = opts[:read_timeout]
33-
if @read_timeout and @read_timeout < 0
34-
raise Mysql2::Error, "read_timeout must be a positive integer, you passed #{@read_timeout}"
35-
end
36-
3732
ssl_set(*opts.values_at(:sslkey, :sslcert, :sslca, :sslcapath, :sslcipher))
3833

3934
if [:user,:pass,:hostname,:dbname,:db,:sock].any?{|k| @query_options.has_key?(k) }

0 commit comments

Comments
 (0)