Skip to content

Commit f136f57

Browse files
committed
properly report stored procedure errors when using info messages
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 4953cd9 commit f136f57

File tree

6 files changed

+161
-61
lines changed

6 files changed

+161
-61
lines changed

CHANGELOG.md

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

3+
* Fix error reporting when preceded by info message
4+
35
## 2.1.3
46

57
* Removed old/unused appveyor config

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";
@@ -99,6 +109,16 @@ int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, c
99109
break;
100110
}
101111

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

136138
return return_value;
@@ -142,25 +144,31 @@ int tinytds_msg_handler(DBPROCESS *dbproc, DBINT msgno, int msgstate, int severi
142144

143145
int is_message_an_error = severity > 10 ? 1 : 0;
144146

147+
tinytds_errordata error_data = {
148+
.is_message = !is_message_an_error,
149+
.cancel = is_message_an_error,
150+
.severity = severity,
151+
.dberr = msgno,
152+
.oserr = msgstate
153+
};
154+
strncpy(error_data.error, msgtext, ERROR_MSG_SIZE);
155+
strncpy(error_data.source, source, ERROR_MSG_SIZE);
156+
145157
// See tinytds_err_handler() for info about why we do this
146158
if (userdata && userdata->nonblocking) {
147-
if (!userdata->nonblocking_error.is_set) {
148-
userdata->nonblocking_error.is_message = !is_message_an_error;
149-
userdata->nonblocking_error.cancel = is_message_an_error;
150-
strncpy(userdata->nonblocking_error.error, msgtext, ERROR_MSG_SIZE);
151-
strncpy(userdata->nonblocking_error.source, source, ERROR_MSG_SIZE);
152-
userdata->nonblocking_error.severity = severity;
153-
userdata->nonblocking_error.dberr = msgno;
154-
userdata->nonblocking_error.oserr = msgstate;
155-
userdata->nonblocking_error.is_set = 1;
156-
}
159+
/*
160+
In the case of non-blocking command batch execution we can receive multiple messages
161+
(including errors). We keep track of those here so they can be processed once the
162+
non-blocking call returns.
163+
*/
164+
push_userdata_error(userdata, error_data);
157165

158166
if (is_message_an_error && !dbdead(dbproc) && !userdata->closed) {
159167
dbcancel(dbproc);
160168
userdata->dbcancel_sent = 1;
161169
}
162170
} else {
163-
rb_tinytds_raise_error(dbproc, !is_message_an_error, is_message_an_error, msgtext, source, severity, msgno, msgstate);
171+
rb_tinytds_raise_error(dbproc, error_data);
164172
}
165173
return 0;
166174
}
@@ -171,7 +179,10 @@ static void rb_tinytds_client_reset_userdata(tinytds_client_userdata *userdata)
171179
userdata->dbsqlok_sent = 0;
172180
userdata->dbcancel_sent = 0;
173181
userdata->nonblocking = 0;
174-
userdata->nonblocking_error.is_set = 0;
182+
// the following is mainly done for consistency since the values are reset accordingly in nogvl_setup/cleanup.
183+
// the nonblocking_errors array does not need to be freed here. That is done as part of nogvl_cleanup.
184+
userdata->nonblocking_errors_length = 0;
185+
userdata->nonblocking_errors_size = 0;
175186
}
176187

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

111122
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 & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ def load_current_schema
153153
loader.execute(drop_sql).do
154154
loader.execute(schema_sql).do
155155
loader.execute(sp_sql).do
156+
loader.execute(sp_error_sql).do
157+
loader.execute(sp_several_prints_sql).do
156158
loader.close
157159
true
158160
end
@@ -167,7 +169,16 @@ def drop_sql_sybase
167169
) DROP TABLE datatypes
168170
IF EXISTS(
169171
SELECT 1 FROM sysobjects WHERE type = 'P' AND name = 'tinytds_TestReturnCodes'
170-
) DROP PROCEDURE tinytds_TestReturnCodes|
172+
) DROP PROCEDURE tinytds_TestReturnCodes
173+
IF EXISTS(
174+
SELECT 1 FROM sysobjects WHERE type = 'P' AND name = 'tinytds_TestPrintWithError'
175+
) DROP PROCEDURE tinytds_TestPrintWithError
176+
IF EXISTS(
177+
SELECT 1 FROM sysobjects WHERE type = 'P' AND name = 'tinytds_TestPrintWithError'
178+
) DROP PROCEDURE tinytds_TestPrintWithError
179+
IF EXISTS(
180+
SELECT 1 FROM sysobjects WHERE type = 'P' AND name = 'tinytds_TestSeveralPrints'
181+
) DROP PROCEDURE tinytds_TestSeveralPrints|
171182
end
172183

173184
def drop_sql_microsoft
@@ -181,7 +192,15 @@ def drop_sql_microsoft
181192
IF EXISTS (
182193
SELECT name FROM sysobjects
183194
WHERE name = 'tinytds_TestReturnCodes' AND type = 'P'
184-
) DROP PROCEDURE tinytds_TestReturnCodes|
195+
) DROP PROCEDURE tinytds_TestReturnCodes
196+
IF EXISTS (
197+
SELECT name FROM sysobjects
198+
WHERE name = 'tinytds_TestPrintWithError' AND type = 'P'
199+
) DROP PROCEDURE tinytds_TestPrintWithError
200+
IF EXISTS (
201+
SELECT name FROM sysobjects
202+
WHERE name = 'tinytds_TestSeveralPrints' AND type = 'P'
203+
) DROP PROCEDURE tinytds_TestSeveralPrints|
185204
end
186205

187206
def sp_sql
@@ -191,6 +210,21 @@ def sp_sql
191210
RETURN(420) |
192211
end
193212

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

215249
end
216250
end
217-

0 commit comments

Comments
 (0)