With the following code, we see the query result being printed, followed by awake when the external sleep finishes:
require 'tiny_tds'
client = TinyTds::Client.new(
username: ENV.fetch("SQLSERVER_USERNAME"),
password: ENV.fetch("SQLSERVER_PASSWORD"),
host: ENV.fetch("SQLSERVER_HOST"),
port: ENV.fetch("SQLSERVER_PORT").to_i,
database: ENV.fetch("SQLSERVER_DATABASE")
)
external_sleep_thread = Thread.new do
puts `sleep 0.3; echo awake` # I was doing some external instrumentation in my actual code
end
p client.execute("WAITFOR DELAY '00:00:00:200'; SELECT 42 AS TheAnswer").to_a
external_sleep_thread.join
[{"TheAnswer" => 42}]
awake
If we change sleep 0.3 to sleep 0.1 (i.e. shorter than the delay in the SQL) then we unexpectedly no longer see the query result:
it appears that the query is being cancelled when the external sleep returns. Having dug into the code, it appears that this is likely caused by nogvl_dbresults:
|
static RETCODE nogvl_dbresults(DBPROCESS *client) |
|
{ |
|
int retcode = FAIL; |
|
nogvl_setup(client); |
|
retcode = NOGVL_DBCALL(dbresults, client); |
|
nogvl_cleanup(client); |
|
return retcode; |
|
} |
which calls rb_thread_call_without_gvl passing a "unblock_function" (ubf) of dbcancel_ubf:
|
#define NOGVL_DBCALL(_dbfunction, _client) ( \ |
|
(RETCODE)(intptr_t)rb_thread_call_without_gvl( \ |
|
(void *(*)(void *))_dbfunction, _client, \ |
|
(rb_unblock_function_t*)dbcancel_ubf, _client ) \ |
|
) |
it seems that the pg gem used to have similar code: https://groups.google.com/g/ruby-pg/c/5_ylGmog1S4/m/c69d__aNhsUJ which was described as "buggy" 😄
I'm not sure of the exact mechanism, but possibly the SIGCHLD from the external sleep is triggering the ubf, which cancels the query?
N.b. it seems that the pg and mysql2 gems both call rb_thread_call_without_gvl with UBF_IO:
https://github.com/ged/ruby-pg/blob/fc75120b173e43b08edf2e6936bf4ba9c196a47f/ext/gvl_wrappers.h#L63
https://github.com/brianmario/mysql2/blob/b009d7e114729cbae5bef069a1033dd78acf7745/ext/mysql2/result.c#L559
It seems like this gem should also use UBF_IO to avoid undesired query cancellations, albeit I suspect that would mean that a pending query couldn't be cancelled by SIGINT.
This was tested on ruby 3.4.5 (2025-07-16 revision 20cda200d3) +PRISM [arm64-darwin24] against tiny_tds 3.2.1
With the following code, we see the query result being printed, followed by
awakewhen the external sleep finishes:If we change
sleep 0.3tosleep 0.1(i.e. shorter than the delay in the SQL) then we unexpectedly no longer see the query result:it appears that the query is being cancelled when the external sleep returns. Having dug into the code, it appears that this is likely caused by
nogvl_dbresults:tiny_tds/ext/tiny_tds/result.c
Lines 169 to 176 in 0737149
which calls
rb_thread_call_without_gvlpassing a "unblock_function" (ubf) ofdbcancel_ubf:tiny_tds/ext/tiny_tds/result.c
Lines 92 to 96 in 0737149
it seems that the
pggem used to have similar code: https://groups.google.com/g/ruby-pg/c/5_ylGmog1S4/m/c69d__aNhsUJ which was described as "buggy" 😄I'm not sure of the exact mechanism, but possibly the
SIGCHLDfrom the external sleep is triggering the ubf, which cancels the query?N.b. it seems that the
pgandmysql2gems both callrb_thread_call_without_gvlwithUBF_IO:https://github.com/ged/ruby-pg/blob/fc75120b173e43b08edf2e6936bf4ba9c196a47f/ext/gvl_wrappers.h#L63
https://github.com/brianmario/mysql2/blob/b009d7e114729cbae5bef069a1033dd78acf7745/ext/mysql2/result.c#L559
It seems like this gem should also use
UBF_IOto avoid undesired query cancellations, albeit I suspect that would mean that a pending query couldn't be cancelled by SIGINT.This was tested on
ruby 3.4.5 (2025-07-16 revision 20cda200d3) +PRISM [arm64-darwin24]againsttiny_tds3.2.1