Skip to content

Commit f9b59d0

Browse files
committed
Merge pull request #679 from sodabrew/timeout_error
Fix Timeout interrupt handling on Ruby 2.3 and protect Mysql2::Statement#execute
2 parents 60b3aa0 + a4b0489 commit f9b59d0

File tree

5 files changed

+47
-5
lines changed

5 files changed

+47
-5
lines changed

ext/mysql2/statement.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ void init_mysql2_statement() {
470470

471471
rb_define_method(cMysql2Statement, "param_count", param_count, 0);
472472
rb_define_method(cMysql2Statement, "field_count", field_count, 0);
473-
rb_define_method(cMysql2Statement, "execute", execute, -1);
473+
rb_define_method(cMysql2Statement, "_execute", execute, -1);
474474
rb_define_method(cMysql2Statement, "fields", fields, 0);
475475
rb_define_method(cMysql2Statement, "last_id", rb_mysql_stmt_last_id, 0);
476476
rb_define_method(cMysql2Statement, "affected_rows", rb_mysql_stmt_affected_rows, 0);

lib/mysql2.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,23 @@ def self.key_hash_as_symbols(hash)
6262
return nil unless hash
6363
Hash[hash.map { |k, v| [k.to_sym, v] }]
6464
end
65+
66+
#
67+
# In Mysql2::Client#query and Mysql2::Statement#execute,
68+
# Thread#handle_interrupt is used to prevent Timeout#timeout
69+
# from interrupting query execution.
70+
#
71+
# Timeout::ExitException was removed in Ruby 2.3.0, 2.2.3, and 2.1.8,
72+
# but is present in earlier 2.1.x and 2.2.x, so we provide a shim.
73+
#
74+
if Thread.respond_to?(:handle_interrupt)
75+
require 'timeout'
76+
# rubocop:disable Style/ConstantName
77+
TimeoutError = if defined?(::Timeout::ExitException)
78+
::Timeout::ExitException
79+
else
80+
::Timeout::Error
81+
end
82+
end
6583
end
6684
end

lib/mysql2/client.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,8 @@ def initialize(opts = {})
8080
end
8181

8282
if Thread.respond_to?(:handle_interrupt)
83-
require 'timeout'
84-
8583
def query(sql, options = {})
86-
Thread.handle_interrupt(::Timeout::ExitException => :never) do
84+
Thread.handle_interrupt(::Mysql2::Util::TimeoutError => :never) do
8785
_query(sql, @query_options.merge(options))
8886
end
8987
end

lib/mysql2/statement.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
module Mysql2
22
class Statement
33
include Enumerable
4+
5+
if Thread.respond_to?(:handle_interrupt)
6+
def execute(*args)
7+
Thread.handle_interrupt(::Mysql2::Util::TimeoutError => :never) do
8+
_execute(*args)
9+
end
10+
end
11+
else
12+
def execute(*args)
13+
_execute(*args)
14+
end
15+
end
416
end
517
end

spec/mysql2/client_spec.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ def run_gc
467467
}.to raise_error(Mysql2::Error)
468468
end
469469

470-
it 'should be impervious to connection-corrupting timeouts ' do
470+
it 'should be impervious to connection-corrupting timeouts in #query' do
471471
pending('`Thread.handle_interrupt` is not defined') unless Thread.respond_to?(:handle_interrupt)
472472
# attempt to break the connection
473473
expect { Timeout.timeout(0.1) { @client.query('SELECT SLEEP(0.2)') } }.to raise_error(Timeout::Error)
@@ -476,6 +476,20 @@ def run_gc
476476
expect { @client.query('SELECT 1') }.to_not raise_error
477477
end
478478

479+
it 'should be impervious to connection-corrupting timeouts in #execute' do
480+
# the statement handle gets corrupted and will segfault the tests if interrupted,
481+
# so we can't even use pending on this test, really have to skip it on older Rubies.
482+
skip('`Thread.handle_interrupt` is not defined') unless Thread.respond_to?(:handle_interrupt)
483+
484+
# attempt to break the connection
485+
stmt = @client.prepare('SELECT SLEEP(?)')
486+
expect { Timeout.timeout(0.1) { stmt.execute(0.2) } }.to raise_error(Timeout::Error)
487+
stmt.close
488+
489+
# expect the connection to not be broken
490+
expect { @client.query('SELECT 1') }.to_not raise_error
491+
end
492+
479493
context 'when a non-standard exception class is raised' do
480494
it "should close the connection when an exception is raised" do
481495
expect { Timeout.timeout(0.1, ArgumentError) { @client.query('SELECT SLEEP(1)') } }.to raise_error(ArgumentError)

0 commit comments

Comments
 (0)