Skip to content

Commit 6968cb1

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 2a846b9 commit 6968cb1

File tree

5 files changed

+88
-45
lines changed

5 files changed

+88
-45
lines changed

ext/tiny_tds/client.c

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ 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+
}
6069

6170
int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, char *dberrstr, char *oserrstr) {
6271
static const char *source = "error";
@@ -111,24 +120,16 @@ int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, c
111120
userdata->dbcancel_sent = 1;
112121
}
113122

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-
123+
tinytds_errordata error = {
124+
.is_message = 0,
125+
.cancel = cancel,
126+
.severity = severity,
127+
.dberr = dberr,
128+
.oserr = oserr
129+
};
130+
strncpy(error.error, dberrstr, ERROR_MSG_SIZE);
131+
strncpy(error.source, source, ERROR_MSG_SIZE);
132+
push_userdata_error(userdata, error);
132133
} else {
133134
rb_tinytds_raise_error(dbproc, 0, cancel, dberrstr, source, severity, dberr, oserr);
134135
}
@@ -144,16 +145,21 @@ int tinytds_msg_handler(DBPROCESS *dbproc, DBINT msgno, int msgstate, int severi
144145

145146
// See tinytds_err_handler() for info about why we do this
146147
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-
}
148+
/*
149+
In the case of stored procedure execution we can receive multiple messages
150+
(including errors). We keep track of those here so they can be processed
151+
once the blocking call returns.
152+
*/
153+
tinytds_errordata error = {
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.error, msgtext, ERROR_MSG_SIZE);
161+
strncpy(error.source, source, ERROR_MSG_SIZE);
162+
push_userdata_error(userdata, error);
157163

158164
if (is_message_an_error && !dbdead(dbproc) && !userdata->closed) {
159165
dbcancel(dbproc);
@@ -171,7 +177,12 @@ static void rb_tinytds_client_reset_userdata(tinytds_client_userdata *userdata)
171177
userdata->dbsqlok_sent = 0;
172178
userdata->dbcancel_sent = 0;
173179
userdata->nonblocking = 0;
174-
userdata->nonblocking_error.is_set = 0;
180+
userdata->nonblocking_errors_length = 0;
181+
if (userdata->nonblocking_errors_size) {
182+
free(userdata->nonblocking_errors);
183+
}
184+
userdata->nonblocking_errors = malloc(ERRORS_STACK_INIT_SIZE * sizeof(tinytds_errordata));
185+
userdata->nonblocking_errors_size = ERRORS_STACK_INIT_SIZE;
175186
}
176187

177188
static void rb_tinytds_client_mark(void *ptr) {
@@ -190,7 +201,9 @@ static void rb_tinytds_client_free(void *ptr) {
190201
cwrap->closed = 1;
191202
cwrap->userdata->closed = 1;
192203
}
193-
xfree(cwrap->userdata);
204+
if (cwrap->userdata->nonblocking_errors_size) {
205+
free(cwrap->userdata->nonblocking_errors);
206+
}
194207
xfree(ptr);
195208
}
196209

ext/tiny_tds/client.h

Lines changed: 4 additions & 2 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

ext/tiny_tds/result.c

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,17 +95,20 @@ static void nogvl_cleanup(DBPROCESS *client) {
9595
Now that the blocking operation is done, we can finally throw any
9696
exceptions based on errors from SQL Server.
9797
*/
98-
if (userdata->nonblocking_error.is_set) {
99-
userdata->nonblocking_error.is_set = 0;
98+
for (short int i = 0; i < userdata->nonblocking_errors_length; i++) {
99+
tinytds_errordata error = userdata->nonblocking_errors[i];
100100
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+
error.is_message,
102+
error.cancel,
103+
error.error,
104+
error.source,
105+
error.severity,
106+
error.dberr,
107+
error.oserr
108+
);
108109
}
110+
111+
userdata->nonblocking_errors_length = 0;
109112
}
110113

111114
static RETCODE nogvl_dbsqlok(DBPROCESS *client) {

test/result_test.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,18 @@ 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
655667
end
656668

657669
it 'must not raise an error when severity is 10 or less' do
@@ -770,4 +782,3 @@ def insert_and_select_datatype(datatype)
770782
end
771783

772784
end
773-

test/test_helper.rb

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ 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
156157
loader.close
157158
true
158159
end
@@ -167,7 +168,10 @@ def drop_sql_sybase
167168
) DROP TABLE datatypes
168169
IF EXISTS(
169170
SELECT 1 FROM sysobjects WHERE type = 'P' AND name = 'tinytds_TestReturnCodes'
170-
) DROP PROCEDURE tinytds_TestReturnCodes|
171+
) DROP PROCEDURE tinytds_TestReturnCodes
172+
IF EXISTS(
173+
SELECT 1 FROM sysobjects WHERE type = 'P' AND name = 'tinytds_TestPrintWithError'
174+
) DROP PROCEDURE tinytds_TestPrintWithError|
171175
end
172176

173177
def drop_sql_microsoft
@@ -181,7 +185,11 @@ def drop_sql_microsoft
181185
IF EXISTS (
182186
SELECT name FROM sysobjects
183187
WHERE name = 'tinytds_TestReturnCodes' AND type = 'P'
184-
) DROP PROCEDURE tinytds_TestReturnCodes|
188+
) DROP PROCEDURE tinytds_TestReturnCodes
189+
IF EXISTS (
190+
SELECT name FROM sysobjects
191+
WHERE name = 'tinytds_TestPrintWithError' AND type = 'P'
192+
) DROP PROCEDURE tinytds_TestPrintWithError|
185193
end
186194

187195
def sp_sql
@@ -191,6 +199,13 @@ def sp_sql
191199
RETURN(420) |
192200
end
193201

202+
def sp_error_sql
203+
%|CREATE PROCEDURE tinytds_TestPrintWithError
204+
AS
205+
PRINT 'hello'
206+
RAISERROR('Error following print', 16, 1)|
207+
end
208+
194209
def find_value(id, column, query_options={})
195210
query_options[:timezone] ||= :utc
196211
sql = "SELECT [#{column}] FROM [datatypes] WHERE [id] = #{id}"
@@ -214,4 +229,3 @@ def rollback_transaction(client)
214229

215230
end
216231
end
217-

0 commit comments

Comments
 (0)