Skip to content
This repository was archived by the owner on Mar 26, 2024. It is now read-only.

Commit 1bdda90

Browse files
committed
Merge pull request brianmario#751 from sodabrew/backport_592
Resolve brianmario#731 with cherry-picked commits from brianmario#592 and brianmario#679.
2 parents d572951 + 94a8d8b commit 1bdda90

File tree

5 files changed

+62
-64
lines changed

5 files changed

+62
-64
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ rvm:
4343
- 2.0.0
4444
- 2.1
4545
- 2.2
46+
- 2.3.1
4647
- ree
4748
matrix:
4849
allow_failures:

ext/mysql2/client.c

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -642,40 +642,27 @@ static VALUE rb_mysql_client_abandon_results(VALUE self) {
642642
* Query the database with +sql+, with optional +options+. For the possible
643643
* options, see @@default_query_options on the Mysql2::Client class.
644644
*/
645-
static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) {
645+
static VALUE rb_query(VALUE self, VALUE sql, VALUE current) {
646646
#ifndef _WIN32
647647
struct async_query_args async_args;
648648
#endif
649649
struct nogvl_send_query_args args;
650-
int async = 0;
651-
VALUE opts, current;
652650
VALUE thread_current = rb_thread_current();
653-
#ifdef HAVE_RUBY_ENCODING_H
654-
rb_encoding *conn_enc;
655-
#endif
656651
GET_CLIENT(self);
657652

658653
REQUIRE_CONNECTED(wrapper);
659654
args.mysql = wrapper->client;
660655

661-
current = rb_hash_dup(rb_iv_get(self, "@query_options"));
662656
RB_GC_GUARD(current);
663657
Check_Type(current, T_HASH);
664658
rb_iv_set(self, "@current_query_options", current);
665659

666-
if (rb_scan_args(argc, argv, "11", &args.sql, &opts) == 2) {
667-
rb_funcall(current, intern_merge_bang, 1, opts);
668-
669-
if (rb_hash_aref(current, sym_async) == Qtrue) {
670-
async = 1;
671-
}
672-
}
673-
674-
Check_Type(args.sql, T_STRING);
660+
Check_Type(sql, T_STRING);
675661
#ifdef HAVE_RUBY_ENCODING_H
676-
conn_enc = rb_to_encoding(wrapper->encoding);
677662
/* ensure the string is in the encoding the connection is expecting */
678-
args.sql = rb_str_export_to_enc(args.sql, conn_enc);
663+
args.sql = rb_str_export_to_enc(sql, rb_to_encoding(wrapper->encoding));
664+
#else
665+
args.sql = sql;
679666
#endif
680667
args.sql_ptr = RSTRING_PTR(args.sql);
681668
args.sql_len = RSTRING_LEN(args.sql);
@@ -699,15 +686,15 @@ static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) {
699686
#ifndef _WIN32
700687
rb_rescue2(do_send_query, (VALUE)&args, disconnect_and_raise, self, rb_eException, (VALUE)0);
701688

702-
if (!async) {
689+
if (rb_hash_aref(current, sym_async) == Qtrue) {
690+
return Qnil;
691+
} else {
703692
async_args.fd = wrapper->client->net.fd;
704693
async_args.self = self;
705694

706695
rb_rescue2(do_query, (VALUE)&async_args, disconnect_and_raise, self, rb_eException, (VALUE)0);
707696

708697
return rb_mysql_client_async_result(self);
709-
} else {
710-
return Qnil;
711698
}
712699
#else
713700
do_send_query(&args);
@@ -1259,7 +1246,6 @@ void init_mysql2_client() {
12591246
rb_define_singleton_method(cMysql2Client, "info", rb_mysql_client_info, 0);
12601247

12611248
rb_define_method(cMysql2Client, "close", rb_mysql_client_close, 0);
1262-
rb_define_method(cMysql2Client, "query", rb_mysql_client_query, -1);
12631249
rb_define_method(cMysql2Client, "abandon_results!", rb_mysql_client_abandon_results, 0);
12641250
rb_define_method(cMysql2Client, "escape", rb_mysql_client_real_escape, 1);
12651251
rb_define_method(cMysql2Client, "server_info", rb_mysql_client_server_info, 0);
@@ -1292,6 +1278,7 @@ void init_mysql2_client() {
12921278
rb_define_private_method(cMysql2Client, "ssl_set", set_ssl_options, 5);
12931279
rb_define_private_method(cMysql2Client, "initialize_ext", initialize_ext, 0);
12941280
rb_define_private_method(cMysql2Client, "connect", rb_connect, 7);
1281+
rb_define_private_method(cMysql2Client, "_query", rb_query, 2);
12951282

12961283
sym_id = ID2SYM(rb_intern("id"));
12971284
sym_version = ID2SYM(rb_intern("version"));

lib/mysql2.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,22 @@ def self.key_hash_as_symbols(hash)
6161
Hash[hash.map { |k,v| [k.to_sym, v] }]
6262
end
6363

64+
#
65+
# In Mysql2::Client#query and Mysql2::Statement#execute,
66+
# Thread#handle_interrupt is used to prevent Timeout#timeout
67+
# from interrupting query execution.
68+
#
69+
# Timeout::ExitException was removed in Ruby 2.3.0, 2.2.3, and 2.1.8,
70+
# but is present in earlier 2.1.x and 2.2.x, so we provide a shim.
71+
#
72+
if Thread.respond_to?(:handle_interrupt)
73+
require 'timeout'
74+
# rubocop:disable Style/ConstantName
75+
TimeoutError = if defined?(::Timeout::ExitException)
76+
::Timeout::ExitException
77+
else
78+
::Timeout::Error
79+
end
80+
end
81+
6482
end

lib/mysql2/client.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,18 @@ def self.default_query_options
7474
@@default_query_options
7575
end
7676

77+
if Thread.respond_to?(:handle_interrupt)
78+
def query(sql, options = {})
79+
Thread.handle_interrupt(::Mysql2::Util::TimeoutError => :never) do
80+
_query(sql, @query_options.merge(options))
81+
end
82+
end
83+
else
84+
def query(sql, options = {})
85+
_query(sql, @query_options.merge(options))
86+
end
87+
end
88+
7789
def query_info
7890
info = query_info_string
7991
return {} unless info

spec/mysql2/client_spec.rb

Lines changed: 22 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -462,59 +462,39 @@ def connect *args
462462
}.should raise_error(Mysql2::Error)
463463
end
464464

465-
it "should close the connection when an exception is raised" do
466-
begin
467-
Timeout.timeout(1, Timeout::Error) do
468-
@client.query("SELECT sleep(2)")
469-
end
470-
rescue Timeout::Error
471-
end
465+
it 'should be impervious to connection-corrupting timeouts in #query' do
466+
pending('`Thread.handle_interrupt` is not defined') unless Thread.respond_to?(:handle_interrupt)
467+
# attempt to break the connection
468+
expect { Timeout.timeout(0.1) { @client.query('SELECT SLEEP(1)') } }.to raise_error(Timeout::Error)
472469

473-
lambda {
474-
@client.query("SELECT 1")
475-
}.should raise_error(Mysql2::Error, 'closed MySQL connection')
470+
# expect the connection to not be broken
471+
expect { @client.query('SELECT 1') }.to_not raise_error
476472
end
477473

478-
it "should handle Timeouts without leaving the connection hanging if reconnect is true" do
479-
client = Mysql2::Client.new(DatabaseCredentials['root'].merge(:reconnect => true))
480-
begin
481-
Timeout.timeout(1, Timeout::Error) do
482-
client.query("SELECT sleep(2)")
483-
end
484-
rescue Timeout::Error
474+
context 'when a non-standard exception class is raised' do
475+
it "should close the connection when an exception is raised" do
476+
expect { Timeout.timeout(0.1, ArgumentError) { @client.query('SELECT SLEEP(1)') } }.to raise_error(ArgumentError)
477+
expect { @client.query('SELECT 1') }.to raise_error(Mysql2::Error, 'closed MySQL connection')
485478
end
486479

487-
lambda {
488-
client.query("SELECT 1")
489-
}.should_not raise_error(Mysql2::Error)
490-
end
480+
it "should handle Timeouts without leaving the connection hanging if reconnect is true" do
481+
client = Mysql2::Client.new(DatabaseCredentials['root'].merge(:reconnect => true))
491482

492-
it "should handle Timeouts without leaving the connection hanging if reconnect is set to true after construction true" do
493-
client = Mysql2::Client.new(DatabaseCredentials['root'])
494-
begin
495-
Timeout.timeout(1, Timeout::Error) do
496-
client.query("SELECT sleep(2)")
497-
end
498-
rescue Timeout::Error
483+
expect { Timeout.timeout(0.1, ArgumentError) { client.query('SELECT SLEEP(1)') } }.to raise_error(ArgumentError)
484+
expect { client.query('SELECT 1') }.to_not raise_error
499485
end
500486

501-
lambda {
502-
client.query("SELECT 1")
503-
}.should raise_error(Mysql2::Error)
487+
it "should handle Timeouts without leaving the connection hanging if reconnect is set to true after construction true" do
488+
client = Mysql2::Client.new(DatabaseCredentials['root'])
504489

505-
client.reconnect = true
490+
expect { Timeout.timeout(0.1, ArgumentError) { client.query('SELECT SLEEP(1)') } }.to raise_error(ArgumentError)
491+
expect { client.query('SELECT 1') }.to raise_error(Mysql2::Error)
506492

507-
begin
508-
Timeout.timeout(1, Timeout::Error) do
509-
client.query("SELECT sleep(2)")
510-
end
511-
rescue Timeout::Error
512-
end
513-
514-
lambda {
515-
client.query("SELECT 1")
516-
}.should_not raise_error(Mysql2::Error)
493+
client.reconnect = true
517494

495+
expect { Timeout.timeout(0.1, ArgumentError) { client.query('SELECT SLEEP(1)') } }.to raise_error(ArgumentError)
496+
expect { client.query('SELECT 1') }.to_not raise_error
497+
end
518498
end
519499

520500
it "threaded queries should be supported" do

0 commit comments

Comments
 (0)