Skip to content

Commit 293fe1f

Browse files
authored
Fix error reporting when preceded by info message (#475)
Capturing errors while in a non-blocking state was originally structured to capture a single error. This was intentional in order to avoid capturing more generic info messages that FreeTDS might send before the Global VM Lock was obtained. In most circumstances this is what we want. However, now that we capture info messages it is possible that a info message will be stored and the actual runtime error will be discarded as non-important. The result is that while a runtime error is reported in the database, a TinyTds error is never thrown and only the info message is handled. A subset of this problem is that only one info message can be captured while in non-blocking mode which prevents stored procedures from reporting multiple info messages to TinyTds. To fix this issue, the reported messages are stored within a dynamic array of tinytds_errordata structs, then processed normally once the GVL is obtained. Given the fact that we don't know the number of messages that will be sent, we dynamically manage and re-allocate memory for the nonblocking_errors array as needed. We can't use the ruby C API because it is not safe to call while in a non-blocking state as shown by #133.
1 parent 2b7d7ab commit 293fe1f

File tree

6 files changed

+160
-60
lines changed

6 files changed

+160
-60
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## (unreleased)
22

33
* Improve handling of network related timeouts
4+
* Fix error reporting when preceded by info message
45

56
## 2.1.3
67

ext/tiny_tds/client.c

Lines changed: 55 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,25 @@ VALUE opt_escape_regex, opt_escape_dblquote;
2424

2525
// Lib Backend (Helpers)
2626

27-
VALUE rb_tinytds_raise_error(DBPROCESS *dbproc, int is_message, int cancel, const char *error, const char *source, int severity, int dberr, int oserr) {
27+
VALUE rb_tinytds_raise_error(DBPROCESS *dbproc, tinytds_errordata error) {
2828
VALUE e;
2929
GET_CLIENT_USERDATA(dbproc);
30-
if (cancel && !dbdead(dbproc) && userdata && !userdata->closed) {
30+
if (error.cancel && !dbdead(dbproc) && userdata && !userdata->closed) {
3131
userdata->dbsqlok_sent = 1;
3232
dbsqlok(dbproc);
3333
userdata->dbcancel_sent = 1;
3434
dbcancel(dbproc);
3535
}
36-
e = rb_exc_new2(cTinyTdsError, error);
37-
rb_funcall(e, intern_source_eql, 1, rb_str_new2(source));
38-
if (severity)
39-
rb_funcall(e, intern_severity_eql, 1, INT2FIX(severity));
40-
if (dberr)
41-
rb_funcall(e, intern_db_error_number_eql, 1, INT2FIX(dberr));
42-
if (oserr)
43-
rb_funcall(e, intern_os_error_number_eql, 1, INT2FIX(oserr));
44-
45-
if (severity <= 10 && is_message) {
36+
e = rb_exc_new2(cTinyTdsError, error.error);
37+
rb_funcall(e, intern_source_eql, 1, rb_str_new2(error.source));
38+
if (error.severity)
39+
rb_funcall(e, intern_severity_eql, 1, INT2FIX(error.severity));
40+
if (error.dberr)
41+
rb_funcall(e, intern_db_error_number_eql, 1, INT2FIX(error.dberr));
42+
if (error.oserr)
43+
rb_funcall(e, intern_os_error_number_eql, 1, INT2FIX(error.oserr));
44+
45+
if (error.severity <= 10 && error.is_message) {
4646
VALUE message_handler = userdata && userdata->message_handler ? userdata->message_handler : Qnil;
4747
if (message_handler && message_handler != Qnil && rb_respond_to(message_handler, intern_call) != 0) {
4848
rb_funcall(message_handler, intern_call, 1, e);
@@ -57,6 +57,16 @@ VALUE rb_tinytds_raise_error(DBPROCESS *dbproc, int is_message, int cancel, cons
5757

5858

5959
// Lib Backend (Memory Management & Handlers)
60+
static void push_userdata_error(tinytds_client_userdata *userdata, tinytds_errordata error) {
61+
// reallocate memory for the array as needed
62+
if (userdata->nonblocking_errors_size == userdata->nonblocking_errors_length) {
63+
userdata->nonblocking_errors_size *= 2;
64+
userdata->nonblocking_errors = realloc(userdata->nonblocking_errors, userdata->nonblocking_errors_size * sizeof(tinytds_errordata));
65+
}
66+
67+
userdata->nonblocking_errors[userdata->nonblocking_errors_length] = error;
68+
userdata->nonblocking_errors_length++;
69+
}
6070

6171
int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, char *dberrstr, char *oserrstr) {
6272
static const char *source = "error";
@@ -105,6 +115,16 @@ int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, c
105115
break;
106116
}
107117

118+
tinytds_errordata error_data = {
119+
.is_message = 0,
120+
.cancel = cancel,
121+
.severity = severity,
122+
.dberr = dberr,
123+
.oserr = oserr
124+
};
125+
strncpy(error_data.error, dberrstr, ERROR_MSG_SIZE);
126+
strncpy(error_data.source, source, ERROR_MSG_SIZE);
127+
108128
/*
109129
When in non-blocking mode we need to store the exception data to throw it
110130
once the blocking call returns, otherwise we will segfault ruby since part
@@ -116,27 +136,9 @@ int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, c
116136
dbcancel(dbproc);
117137
userdata->dbcancel_sent = 1;
118138
}
119-
120-
/*
121-
If we've already captured an error message, don't overwrite it. This is
122-
here because FreeTDS sends a generic "General SQL Server error" message
123-
that will overwrite the real message. This is not normally a problem
124-
because a ruby exception is normally thrown and we bail before the
125-
generic message can be sent.
126-
*/
127-
if (!userdata->nonblocking_error.is_set) {
128-
userdata->nonblocking_error.is_message = 0;
129-
userdata->nonblocking_error.cancel = cancel;
130-
strncpy(userdata->nonblocking_error.error, dberrstr, ERROR_MSG_SIZE);
131-
strncpy(userdata->nonblocking_error.source, source, ERROR_MSG_SIZE);
132-
userdata->nonblocking_error.severity = severity;
133-
userdata->nonblocking_error.dberr = dberr;
134-
userdata->nonblocking_error.oserr = oserr;
135-
userdata->nonblocking_error.is_set = 1;
136-
}
137-
139+
push_userdata_error(userdata, error_data);
138140
} else {
139-
rb_tinytds_raise_error(dbproc, 0, cancel, dberrstr, source, severity, dberr, oserr);
141+
rb_tinytds_raise_error(dbproc, error_data);
140142
}
141143

142144
return return_value;
@@ -148,25 +150,31 @@ int tinytds_msg_handler(DBPROCESS *dbproc, DBINT msgno, int msgstate, int severi
148150

149151
int is_message_an_error = severity > 10 ? 1 : 0;
150152

153+
tinytds_errordata error_data = {
154+
.is_message = !is_message_an_error,
155+
.cancel = is_message_an_error,
156+
.severity = severity,
157+
.dberr = msgno,
158+
.oserr = msgstate
159+
};
160+
strncpy(error_data.error, msgtext, ERROR_MSG_SIZE);
161+
strncpy(error_data.source, source, ERROR_MSG_SIZE);
162+
151163
// See tinytds_err_handler() for info about why we do this
152164
if (userdata && userdata->nonblocking) {
153-
if (!userdata->nonblocking_error.is_set) {
154-
userdata->nonblocking_error.is_message = !is_message_an_error;
155-
userdata->nonblocking_error.cancel = is_message_an_error;
156-
strncpy(userdata->nonblocking_error.error, msgtext, ERROR_MSG_SIZE);
157-
strncpy(userdata->nonblocking_error.source, source, ERROR_MSG_SIZE);
158-
userdata->nonblocking_error.severity = severity;
159-
userdata->nonblocking_error.dberr = msgno;
160-
userdata->nonblocking_error.oserr = msgstate;
161-
userdata->nonblocking_error.is_set = 1;
162-
}
165+
/*
166+
In the case of non-blocking command batch execution we can receive multiple messages
167+
(including errors). We keep track of those here so they can be processed once the
168+
non-blocking call returns.
169+
*/
170+
push_userdata_error(userdata, error_data);
163171

164172
if (is_message_an_error && !dbdead(dbproc) && !userdata->closed) {
165173
dbcancel(dbproc);
166174
userdata->dbcancel_sent = 1;
167175
}
168176
} else {
169-
rb_tinytds_raise_error(dbproc, !is_message_an_error, is_message_an_error, msgtext, source, severity, msgno, msgstate);
177+
rb_tinytds_raise_error(dbproc, error_data);
170178
}
171179
return 0;
172180
}
@@ -204,7 +212,10 @@ static void rb_tinytds_client_reset_userdata(tinytds_client_userdata *userdata)
204212
userdata->dbsqlok_sent = 0;
205213
userdata->dbcancel_sent = 0;
206214
userdata->nonblocking = 0;
207-
userdata->nonblocking_error.is_set = 0;
215+
// the following is mainly done for consistency since the values are reset accordingly in nogvl_setup/cleanup.
216+
// the nonblocking_errors array does not need to be freed here. That is done as part of nogvl_cleanup.
217+
userdata->nonblocking_errors_length = 0;
218+
userdata->nonblocking_errors_size = 0;
208219
}
209220

210221
static void rb_tinytds_client_mark(void *ptr) {

ext/tiny_tds/client.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
void init_tinytds_client();
66

77
#define ERROR_MSG_SIZE 1024
8+
#define ERRORS_STACK_INIT_SIZE 2
89

910
typedef struct {
10-
short int is_set;
1111
int is_message;
1212
int cancel;
1313
char error[ERROR_MSG_SIZE];
@@ -25,7 +25,9 @@ typedef struct {
2525
RETCODE dbsqlok_retcode;
2626
short int dbcancel_sent;
2727
short int nonblocking;
28-
tinytds_errordata nonblocking_error;
28+
short int nonblocking_errors_length;
29+
short int nonblocking_errors_size;
30+
tinytds_errordata *nonblocking_errors;
2931
VALUE message_handler;
3032
} tinytds_client_userdata;
3133

@@ -40,7 +42,7 @@ typedef struct {
4042
rb_encoding *encoding;
4143
} tinytds_client_wrapper;
4244

43-
VALUE rb_tinytds_raise_error(DBPROCESS *dbproc, int is_message, int cancel, const char *error, const char *source, int severity, int dberr, int oserr);
45+
VALUE rb_tinytds_raise_error(DBPROCESS *dbproc, tinytds_errordata error);
4446

4547
// Lib Macros
4648

ext/tiny_tds/result.c

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ static void dbcancel_ubf(DBPROCESS *client) {
8686
static void nogvl_setup(DBPROCESS *client) {
8787
GET_CLIENT_USERDATA(client);
8888
userdata->nonblocking = 1;
89+
userdata->nonblocking_errors_length = 0;
90+
userdata->nonblocking_errors = malloc(ERRORS_STACK_INIT_SIZE * sizeof(tinytds_errordata));
91+
userdata->nonblocking_errors_size = ERRORS_STACK_INIT_SIZE;
8992
}
9093

9194
static void nogvl_cleanup(DBPROCESS *client) {
@@ -96,17 +99,25 @@ static void nogvl_cleanup(DBPROCESS *client) {
9699
Now that the blocking operation is done, we can finally throw any
97100
exceptions based on errors from SQL Server.
98101
*/
99-
if (userdata->nonblocking_error.is_set) {
100-
userdata->nonblocking_error.is_set = 0;
101-
rb_tinytds_raise_error(client,
102-
userdata->nonblocking_error.is_message,
103-
userdata->nonblocking_error.cancel,
104-
userdata->nonblocking_error.error,
105-
userdata->nonblocking_error.source,
106-
userdata->nonblocking_error.severity,
107-
userdata->nonblocking_error.dberr,
108-
userdata->nonblocking_error.oserr);
102+
for (short int i = 0; i < userdata->nonblocking_errors_length; i++) {
103+
tinytds_errordata error = userdata->nonblocking_errors[i];
104+
105+
// lookahead to drain any info messages ahead of raising error
106+
if (!error.is_message) {
107+
for (short int j = i; j < userdata->nonblocking_errors_length; j++) {
108+
tinytds_errordata msg_error = userdata->nonblocking_errors[j];
109+
if (msg_error.is_message) {
110+
rb_tinytds_raise_error(client, msg_error);
111+
}
112+
}
113+
}
114+
115+
rb_tinytds_raise_error(client, error);
109116
}
117+
118+
free(userdata->nonblocking_errors);
119+
userdata->nonblocking_errors_length = 0;
120+
userdata->nonblocking_errors_size = 0;
110121
}
111122

112123
static RETCODE nogvl_dbsqlok(DBPROCESS *client) {

test/result_test.rb

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,48 @@ class ResultTest < TinyTds::TestCase
652652
assert_equal 1, messages.length, 'there should be one message after one print statement'
653653
assert_equal msg, m.message, 'message text'
654654
end
655+
656+
it 'must raise an error preceded by a `print` message' do
657+
messages.clear
658+
action = lambda { @client.execute("EXEC tinytds_TestPrintWithError").do }
659+
assert_raise_tinytds_error(action) do |e|
660+
assert_equal 'hello', messages.first.message, 'message text'
661+
662+
assert_equal "Error following print", e.message
663+
assert_equal 16, e.severity
664+
assert_equal 50000, e.db_error_number
665+
end
666+
end
667+
668+
it 'calls the provided message handler for each of a series of `print` messages' do
669+
messages.clear
670+
@client.execute("EXEC tinytds_TestSeveralPrints").do
671+
assert_equal ['hello 1', 'hello 2', 'hello 3'], messages.map { |e| e.message }, 'message list'
672+
end
673+
674+
it 'should flush info messages before raising error in cases of timeout' do
675+
@client = new_connection timeout: 1, message_handler: Proc.new { |m| messages << m }
676+
action = lambda { @client.execute("print 'hello'; waitfor delay '00:00:02'").do }
677+
messages.clear
678+
assert_raise_tinytds_error(action) do |e|
679+
assert_match %r{timed out}i, e.message, 'ignore if non-english test run'
680+
assert_equal 6, e.severity
681+
assert_equal 20003, e.db_error_number
682+
assert_equal 'hello', messages.first&.message, 'message text'
683+
end
684+
end
685+
686+
it 'should print info messages before raising error in cases of timeout' do
687+
@client = new_connection timeout: 1, message_handler: Proc.new { |m| messages << m }
688+
action = lambda { @client.execute("raiserror('hello', 1, 1) with nowait; waitfor delay '00:00:02'").do }
689+
messages.clear
690+
assert_raise_tinytds_error(action) do |e|
691+
assert_match %r{timed out}i, e.message, 'ignore if non-english test run'
692+
assert_equal 6, e.severity
693+
assert_equal 20003, e.db_error_number
694+
assert_equal 'hello', messages.first&.message, 'message text'
695+
end
696+
end
655697
end
656698

657699
it 'must not raise an error when severity is 10 or less' do
@@ -770,4 +812,3 @@ def insert_and_select_datatype(datatype)
770812
end
771813

772814
end
773-

test/test_helper.rb

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ def load_current_schema
154154
loader.execute(drop_sql).do
155155
loader.execute(schema_sql).do
156156
loader.execute(sp_sql).do
157+
loader.execute(sp_error_sql).do
158+
loader.execute(sp_several_prints_sql).do
157159
loader.close
158160
true
159161
end
@@ -168,7 +170,16 @@ def drop_sql_sybase
168170
) DROP TABLE datatypes
169171
IF EXISTS(
170172
SELECT 1 FROM sysobjects WHERE type = 'P' AND name = 'tinytds_TestReturnCodes'
171-
) DROP PROCEDURE tinytds_TestReturnCodes|
173+
) DROP PROCEDURE tinytds_TestReturnCodes
174+
IF EXISTS(
175+
SELECT 1 FROM sysobjects WHERE type = 'P' AND name = 'tinytds_TestPrintWithError'
176+
) DROP PROCEDURE tinytds_TestPrintWithError
177+
IF EXISTS(
178+
SELECT 1 FROM sysobjects WHERE type = 'P' AND name = 'tinytds_TestPrintWithError'
179+
) DROP PROCEDURE tinytds_TestPrintWithError
180+
IF EXISTS(
181+
SELECT 1 FROM sysobjects WHERE type = 'P' AND name = 'tinytds_TestSeveralPrints'
182+
) DROP PROCEDURE tinytds_TestSeveralPrints|
172183
end
173184

174185
def drop_sql_microsoft
@@ -182,7 +193,15 @@ def drop_sql_microsoft
182193
IF EXISTS (
183194
SELECT name FROM sysobjects
184195
WHERE name = 'tinytds_TestReturnCodes' AND type = 'P'
185-
) DROP PROCEDURE tinytds_TestReturnCodes|
196+
) DROP PROCEDURE tinytds_TestReturnCodes
197+
IF EXISTS (
198+
SELECT name FROM sysobjects
199+
WHERE name = 'tinytds_TestPrintWithError' AND type = 'P'
200+
) DROP PROCEDURE tinytds_TestPrintWithError
201+
IF EXISTS (
202+
SELECT name FROM sysobjects
203+
WHERE name = 'tinytds_TestSeveralPrints' AND type = 'P'
204+
) DROP PROCEDURE tinytds_TestSeveralPrints|
186205
end
187206

188207
def sp_sql
@@ -192,6 +211,21 @@ def sp_sql
192211
RETURN(420) |
193212
end
194213

214+
def sp_error_sql
215+
%|CREATE PROCEDURE tinytds_TestPrintWithError
216+
AS
217+
PRINT 'hello'
218+
RAISERROR('Error following print', 16, 1)|
219+
end
220+
221+
def sp_several_prints_sql
222+
%|CREATE PROCEDURE tinytds_TestSeveralPrints
223+
AS
224+
PRINT 'hello 1'
225+
PRINT 'hello 2'
226+
PRINT 'hello 3'|
227+
end
228+
195229
def find_value(id, column, query_options={})
196230
query_options[:timezone] ||= :utc
197231
sql = "SELECT [#{column}] FROM [datatypes] WHERE [id] = #{id}"

0 commit comments

Comments
 (0)