Skip to content

Commit 951af30

Browse files
committed
better handling of network related timeouts
With a working network connection: * command batch is called on non gvl thread * tinytds_err_handler is called with timeout error and returns INT_TIMEOUT * dbcancel is called and command batch returns * nogvl_cleanup is called and timeout error is raised This is all great. The timeout is hit, the db connection lives, and a error is thrown. With a network failure: * command batch is called on non gvl thread * tinytds_err_handler is called with timeout error and returns INT_TIMEOUT * dbcancel is called and does not succeed. command batch never returns * nogvl_cleanup is not called This is not great. dbcancel does not succeed because of the network failure. the command batch does not return until the underlying network timeout on the os is hit. TinyTds doesn't throw an error in the expected timeout window. To fix, we set a flag when a timeout is encountered. We use dbsetinterrupt to check this flag periodically while waiting on a read from the server. Once the flag is set the interrupt with send INT_CANCEL causing the pending command batch to return early. This means nogvl_cleanup will be called and raise the timeout error. This shouldn't have any affect in "normal" timeout conditions due to the fact that dbcancel will actually succeed and cause the normal flow before the interrupt can be called/handled. This is good because in these situtations we still want the dbproc to remain in working condition.
1 parent cce0596 commit 951af30

File tree

4 files changed

+46
-11
lines changed

4 files changed

+46
-11
lines changed

ext/tiny_tds/client.c

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,13 @@ int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, c
8686
but we don't ever want to automatically retry. Instead have the app
8787
decide what to do.
8888
*/
89-
return_value = INT_TIMEOUT;
89+
if (userdata->timing_out) {
90+
return INT_CANCEL;
91+
}
92+
else {
93+
userdata->timing_out = 1;
94+
return_value = INT_TIMEOUT;
95+
}
9096
cancel = 1;
9197
break;
9298

@@ -165,6 +171,33 @@ int tinytds_msg_handler(DBPROCESS *dbproc, DBINT msgno, int msgstate, int severi
165171
return 0;
166172
}
167173

174+
/*
175+
Used by dbsetinterrupt -
176+
This gets called periodically while waiting on a read from the server
177+
Right now, we only care about cases where a read from the server is
178+
taking longer than the specified timeout and dbcancel is not working.
179+
In these cases we decide that we actually want to handle the interrupt
180+
*/
181+
static int check_interrupt(void *ptr) {
182+
GET_CLIENT_USERDATA((DBPROCESS *)ptr);
183+
return userdata->timing_out;
184+
}
185+
186+
/*
187+
Used by dbsetinterrupt -
188+
This gets called if check_interrupt returns TRUE.
189+
Right now, this is only used in cases where a read from the server is
190+
taking longer than the specified timeout and dbcancel is not working.
191+
Return INT_CANCEL to abort the current command batch.
192+
*/
193+
static int handle_interrupt(void *ptr) {
194+
GET_CLIENT_USERDATA((DBPROCESS *)ptr);
195+
if (userdata->timing_out) {
196+
return INT_CANCEL;
197+
}
198+
return INT_CONTINUE;
199+
}
200+
168201
static void rb_tinytds_client_reset_userdata(tinytds_client_userdata *userdata) {
169202
userdata->timing_out = 0;
170203
userdata->dbsql_sent = 0;
@@ -381,6 +414,7 @@ static VALUE rb_tinytds_connect(VALUE self, VALUE opts) {
381414
}
382415
}
383416
dbsetuserdata(cwrap->client, (BYTE*)cwrap->userdata);
417+
dbsetinterrupt(cwrap->client, check_interrupt, handle_interrupt);
384418
cwrap->userdata->closed = 0;
385419
if (!NIL_P(database) && (azure != Qtrue)) {
386420
dbuse(cwrap->client, StringValueCStr(database));

ext/tiny_tds/result.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ static void nogvl_setup(DBPROCESS *client) {
9191
static void nogvl_cleanup(DBPROCESS *client) {
9292
GET_CLIENT_USERDATA(client);
9393
userdata->nonblocking = 0;
94+
userdata->timing_out = 0;
9495
/*
9596
Now that the blocking operation is done, we can finally throw any
9697
exceptions based on errors from SQL Server.

test/client_test.rb

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -129,27 +129,24 @@ class ClientTest < TinyTds::TestCase
129129
end
130130
end
131131

132-
it 'must run this test to prove we account for dropped connections' do
133-
skip
132+
it 'raises TinyTds exception with sql batch timeout due to network failure' do
133+
skip if ENV['CI'] && ENV['APPVEYOR_BUILD_FOLDER'] # only CI using docker
134134
begin
135-
client = new_connection :login_timeout => 2, :timeout => 2
135+
client = new_connection timeout: 2
136136
assert_client_works(client)
137-
STDOUT.puts "Disconnect network!"
138-
sleep 10
139-
STDOUT.puts "This should not get stuck past 6 seconds!"
137+
docker_container('pause', wait_for: 1)
140138
action = lambda { client.execute('SELECT 1 as [one]').each }
141139
assert_raise_tinytds_error(action) do |e|
142140
assert_equal 20003, e.db_error_number
143141
assert_equal 6, e.severity
144142
assert_match %r{timed out}i, e.message, 'ignore if non-english test run'
145143
end
146144
ensure
147-
STDOUT.puts "Reconnect network!"
148-
sleep 10
145+
docker_container('unpause', wait_for: 1)
149146
action = lambda { client.execute('SELECT 1 as [one]').each }
150147
assert_raise_tinytds_error(action) do |e|
151148
assert_equal 20047, e.db_error_number
152-
assert_equal 1, e.severity
149+
assert_equal 9, e.severity
153150
assert_match %r{dead or not enabled}i, e.message, 'ignore if non-english test run'
154151
end
155152
close_client(client)

test/test_helper.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,9 @@ def rollback_transaction(client)
212212
client.execute("ROLLBACK TRANSACTION").do
213213
end
214214

215+
def docker_container(cmd, wait_for: 0)
216+
system("docker #{cmd} $(docker ps --format '{{.Names}}' --filter 'ancestor=metaskills/mssql-server-linux-tinytds:2017-GA') > /dev/null")
217+
sleep(wait_for) if wait_for > 0
218+
end
215219
end
216220
end
217-

0 commit comments

Comments
 (0)