tiny_tds 2.1.3-x64-mingw32 → 2.1.4.pre2-x64-mingw32

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e512a3fac65015371dff5419a96d735ad548ba9c7215956a798c820aa1829e62
4
- data.tar.gz: 56e856f74da1a557a1a2050376bfcaff8dfd90745c00f7ab406ac7d70a66e96b
3
+ metadata.gz: a33a28cbb91241932396a3f039b62ad3d2934853ab55b58925cff0462f051777
4
+ data.tar.gz: 512f9e8ea1728b77606a8ea40e0d494af9d5ced2793c34d9d898932caa23b54c
5
5
  SHA512:
6
- metadata.gz: 7b3941293d3dfffb05c06a18e02e1705f17e619efc242b6ddf518a7f9d03860a48d733e2ca9b66748220872f16b55f4ce8c5103e7c91865a0a6149b6b5b8f717
7
- data.tar.gz: e50f0c8d362a3b402029d12ddd0305f2d41ffa577b056bf044342569eac6c53060096162e593ae8692a0c98f595d501360e8b1c73af57ddea5c0583c105f6da0
6
+ metadata.gz: e9947a953f6a20512edc3a9045c70a87827da0143fecd73b2acf0320ae42bf043a11e6ccaa581ff78dde1980abfe8b8da9d6b8f72b76b6eac1813dbfa9eea857
7
+ data.tar.gz: 727ffa945c0ad7874016cfc481743015f7360126cabb87720ddf4eab1f6bb66894edb38746d22dcf2e35e729ed26a2d26707194d91d1bd3f4da1a96756e5a446
data/.travis.yml CHANGED
@@ -14,9 +14,9 @@ rvm:
14
14
  - 2.7.0
15
15
  before_install:
16
16
  - docker info
17
+ - docker-compose up -d
17
18
  - sudo ./test/bin/install-openssl.sh
18
19
  - sudo ./test/bin/install-freetds.sh
19
- - sudo ./test/bin/setup.sh
20
20
  install:
21
21
  - gem install bundler
22
22
  - bundle --version
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## (unreleased)
2
2
 
3
+ ## 2.1.4
4
+
5
+ * Raise error if client is unable to execute statement
6
+ * Improve handling of network related timeouts
7
+ * Fix error reporting when preceeded by info message
8
+
3
9
  ## 2.1.3
4
10
 
5
11
  * Removed old/unused appveyor config
@@ -262,4 +268,3 @@ Use both dbsetversion() vs. dbsetlversion. Partially reverts #62.
262
268
 
263
269
 
264
270
  ## 0.1.0 Initial release!
265
-
data/README.md CHANGED
@@ -110,7 +110,7 @@ Creating a new client takes a hash of options. For valid iconv encoding options,
110
110
  * :appname - Short string seen in SQL Servers process/activity window.
111
111
  * :tds_version - TDS version. Defaults to "7.3".
112
112
  * :login_timeout - Seconds to wait for login. Default to 60 seconds.
113
- * :timeout - Seconds to wait for a response to a SQL command. Default 5 seconds. Prior to 1.0rc5, FreeTDS was unable to set the timeout on a per-client basis, permitting only a global timeout value. This means that if you're using an older version, the timeout values for all clients will be overwritten each time you instantiate a new `TinyTds::Client` object. If you are using 1.0rc5 or later, all clients will have an independent timeout setting as you'd expect.
113
+ * :timeout - Seconds to wait for a response to a SQL command. Default 5 seconds. Prior to 1.0rc5, FreeTDS was unable to set the timeout on a per-client basis, permitting only a global timeout value. This means that if you're using an older version, the timeout values for all clients will be overwritten each time you instantiate a new `TinyTds::Client` object. If you are using 1.0rc5 or later, all clients will have an independent timeout setting as you'd expect. Timeouts caused by network failure will raise a timeout error 1 second after the configured timeout limit is hit (see [#481](https://github.com/rails-sqlserver/tiny_tds/pull/481) for details).
114
114
  * :encoding - Any valid iconv value like CP1251 or ISO-8859-1. Default UTF-8.
115
115
  * :azure - Pass true to signal that you are connecting to azure.
116
116
  * :contained - Pass true to signal that you are connecting with a contained database user.
@@ -322,9 +322,13 @@ By default row caching is turned on because the SQL Server adapter for ActiveRec
322
322
  TinyTDS takes an opinionated stance on how we handle encoding errors. First, we treat errors differently on reads vs. writes. Our opinion is that if you are reading bad data due to your client's encoding option, you would rather just find `?` marks in your strings vs being blocked with exceptions. This is how things wold work via ODBC or SMS. On the other hand, writes will raise an exception. In this case we raise the SYBEICONVO/2402 error message which has a description of `Error converting characters into server's character set. Some character(s) could not be converted.`. Even though the severity of this message is only a `4` and TinyTDS will automatically strip/ignore unknown characters, we feel you should know that you are inserting bad encodings. In this way, a transaction can be rolled back, etc. Remember, any database write that has bad characters due to the client encoding will still be written to the database, but it is up to you rollback said write if needed. Most ORMs like ActiveRecord handle this scenario just fine.
323
323
 
324
324
 
325
+ ## Timeout Error Handling
326
+
327
+ TinyTDS will raise a `TinyTDS::Error` when a timeout is reached based on the options supplied to the client. Depending on the reason for the timeout, the connection could be dead or alive. When db processing is the cause for the timeout, the connection should still be usable after the error is raised. When network failure is the cause of the timeout, the connection will be dead. If you attempt to execute another command batch on a dead connection you will see a `DBPROCESS is dead or not enabled` error. Therefore, it is recommended to check for a `dead?` connection before trying to execute another command batch.
328
+
325
329
  ## Binstubs
326
330
 
327
- The TinyTDS gem uses binstub wrappers which mirror compiled [FreeTDS Utilities](http://www.freetds.org/userguide/usefreetds.htm) binaries. These native executables are usually installed at the system level when installing FreeTDS. However, when using MiniPortile to install TinyTDS as we do with Windows binaries, these binstubs will find and prefer local gem `exe` directory executables. These are the following binstubs we wrap.
331
+ The TinyTDS gem uses binstub wrappers which mirror compiled [FreeTDS Utilities](https://www.freetds.org/userguide/usefreetds.html) binaries. These native executables are usually installed at the system level when installing FreeTDS. However, when using MiniPortile to install TinyTDS as we do with Windows binaries, these binstubs will find and prefer local gem `exe` directory executables. These are the following binstubs we wrap.
328
332
 
329
333
  * tsql - Used to test connections and debug compile time settings.
330
334
  * defncopy - Used to dump schema structures.
@@ -419,17 +423,20 @@ First, clone the repo using the command line or your Git GUI of choice.
419
423
  $ git clone git@github.com:rails-sqlserver/tiny_tds.git
420
424
  ```
421
425
 
422
- After that, the quickest way to get setup for development is to use [Docker](https://www.docker.com/). Assuming you have [downloaded docker](https://www.docker.com/products/docker) for your platform and you have , you can run our test setup script.
426
+ After that, the quickest way to get setup for development is to use [Docker](https://www.docker.com/). Assuming you have [downloaded docker](https://www.docker.com/products/docker) for your platform, you can use [docker-compose](https://docs.docker.com/compose/install/) to run the necessary containers for testing.
423
427
 
424
428
  ```shell
425
- $ ./test/bin/setup.sh
429
+ $ docker-compose up -d
426
430
  ```
427
431
 
428
- This will download our SQL Server for Linux Docker image based from [microsoft/mssql-server-linux/](https://hub.docker.com/r/microsoft/mssql-server-linux/). Our image already has the `[tinytdstest]` DB and `tinytds` users created. Basically, it does the following.
432
+ This will download our SQL Server for Linux Docker image based from [microsoft/mssql-server-linux/](https://hub.docker.com/r/microsoft/mssql-server-linux/). Our image already has the `[tinytdstest]` DB and `tinytds` users created. This will also download a [toxiproxy](https://github.com/shopify/toxiproxy) Docker image which we can use to simulate network failures for tests. Basically, it does the following.
429
433
 
430
434
  ```shell
435
+ $ docker network create main-network
431
436
  $ docker pull metaskills/mssql-server-linux-tinytds
432
- $ docker run -p 1433:1433 -d metaskills/mssql-server-linux-tinytds
437
+ $ docker run -p 1433:1433 -d --name sqlserver --network main-network metaskills/mssql-server-linux-tinytds
438
+ $ docker pull shopify/toxiproxy
439
+ $ docker run -p 8474:8474 -p 1234:1234 -d --name toxiproxy --network main-network shopify/toxiproxy
433
440
  ```
434
441
 
435
442
  If you are using your own database. Make sure to run these SQL commands as SA to get the test database and user installed.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.1.3
1
+ 2.1.4.pre2
data/appveyor.yml CHANGED
@@ -19,7 +19,19 @@ install:
19
19
  - perl --version
20
20
  - ruby --version
21
21
  - gem --version
22
-
22
+ # Update keyring according to https://www.msys2.org/news/#2020-06-29-new-packagers
23
+ - C:\msys64\usr\bin\curl -O http://repo.msys2.org/msys/x86_64/msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz
24
+ - C:\msys64\usr\bin\curl -O http://repo.msys2.org/msys/x86_64/msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz.sig
25
+ - ridk exec bash -c "pacman-key --verify msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz.sig"
26
+ - ridk exec bash -c "pacman -U --noconfirm --config <(echo) msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz"
27
+ # Update zstd and pacman first https://github.com/msys2/MSYS2-packages/issues/2300
28
+ - C:\msys64\usr\bin\pacman --noconfirm --upgrade https://repo.msys2.org/msys/x86_64/zstd-1.4.7-1-x86_64.pkg.tar.xz # Must come First, or else pacman will install 1.4.8
29
+ - C:\msys64\usr\bin\pacman --noconfirm --upgrade https://repo.msys2.org/msys/x86_64/pacman-5.2.2-5-x86_64.pkg.tar.xz
30
+ # update packages
31
+ - C:\msys64\usr\bin\pacman --noconfirm --ask 20 --sync --refresh --refresh --sysupgrade --sysupgrade
32
+ # Kill all running msys2 binaries to avoid error "size of shared memory region changed".
33
+ # See https://github.com/msys2/MSYS2-packages/issues/258
34
+ - powershell -Command "Get-Process | Where-Object {$_.path -like 'C:\msys64*'} | Stop-Process"
23
35
  # prevent freetds to link to wrong ws2_32 lib on i686-w64-mingw32
24
36
  - c:/msys64/usr/bin/rm /usr/lib/w32api/libws2_32.a
25
37
  # Set up project prerequisits
@@ -0,0 +1,22 @@
1
+ version: '3'
2
+
3
+ networks:
4
+ main-network:
5
+
6
+ services:
7
+ mssql:
8
+ image: metaskills/mssql-server-linux-tinytds:2017-GA
9
+ container_name: sqlserver
10
+ ports:
11
+ - "1433:1433"
12
+ networks:
13
+ - main-network
14
+
15
+ toxiproxy:
16
+ image: shopify/toxiproxy
17
+ container_name: toxiproxy
18
+ ports:
19
+ - "8474:8474"
20
+ - "1234:1234"
21
+ networks:
22
+ - main-network
@@ -24,25 +24,25 @@ VALUE opt_escape_regex, opt_escape_dblquote;
24
24
 
25
25
  // Lib Backend (Helpers)
26
26
 
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) {
28
28
  VALUE e;
29
29
  GET_CLIENT_USERDATA(dbproc);
30
- if (cancel && !dbdead(dbproc) && userdata && !userdata->closed) {
30
+ if (error.cancel && !dbdead(dbproc) && userdata && !userdata->closed) {
31
31
  userdata->dbsqlok_sent = 1;
32
32
  dbsqlok(dbproc);
33
33
  userdata->dbcancel_sent = 1;
34
34
  dbcancel(dbproc);
35
35
  }
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) {
46
46
  VALUE message_handler = userdata && userdata->message_handler ? userdata->message_handler : Qnil;
47
47
  if (message_handler && message_handler != Qnil && rb_respond_to(message_handler, intern_call) != 0) {
48
48
  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
57
57
 
58
58
 
59
59
  // 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
+ }
60
70
 
61
71
  int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, char *dberrstr, char *oserrstr) {
62
72
  static const char *source = "error";
@@ -86,7 +96,13 @@ int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, c
86
96
  but we don't ever want to automatically retry. Instead have the app
87
97
  decide what to do.
88
98
  */
89
- return_value = INT_TIMEOUT;
99
+ if (userdata->timing_out) {
100
+ return INT_CANCEL;
101
+ }
102
+ else {
103
+ userdata->timing_out = 1;
104
+ return_value = INT_TIMEOUT;
105
+ }
90
106
  cancel = 1;
91
107
  break;
92
108
 
@@ -99,6 +115,16 @@ int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, c
99
115
  break;
100
116
  }
101
117
 
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
+
102
128
  /*
103
129
  When in non-blocking mode we need to store the exception data to throw it
104
130
  once the blocking call returns, otherwise we will segfault ruby since part
@@ -110,27 +136,9 @@ int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, c
110
136
  dbcancel(dbproc);
111
137
  userdata->dbcancel_sent = 1;
112
138
  }
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
-
139
+ push_userdata_error(userdata, error_data);
132
140
  } else {
133
- rb_tinytds_raise_error(dbproc, 0, cancel, dberrstr, source, severity, dberr, oserr);
141
+ rb_tinytds_raise_error(dbproc, error_data);
134
142
  }
135
143
 
136
144
  return return_value;
@@ -142,36 +150,72 @@ int tinytds_msg_handler(DBPROCESS *dbproc, DBINT msgno, int msgstate, int severi
142
150
 
143
151
  int is_message_an_error = severity > 10 ? 1 : 0;
144
152
 
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
+
145
163
  // See tinytds_err_handler() for info about why we do this
146
164
  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
- }
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);
157
171
 
158
172
  if (is_message_an_error && !dbdead(dbproc) && !userdata->closed) {
159
173
  dbcancel(dbproc);
160
174
  userdata->dbcancel_sent = 1;
161
175
  }
162
176
  } else {
163
- 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);
164
178
  }
165
179
  return 0;
166
180
  }
167
181
 
182
+ /*
183
+ Used by dbsetinterrupt -
184
+ This gets called periodically while waiting on a read from the server
185
+ Right now, we only care about cases where a read from the server is
186
+ taking longer than the specified timeout and dbcancel is not working.
187
+ In these cases we decide that we actually want to handle the interrupt
188
+ */
189
+ static int check_interrupt(void *ptr) {
190
+ GET_CLIENT_USERDATA((DBPROCESS *)ptr);
191
+ return userdata->timing_out;
192
+ }
193
+
194
+ /*
195
+ Used by dbsetinterrupt -
196
+ This gets called if check_interrupt returns TRUE.
197
+ Right now, this is only used in cases where a read from the server is
198
+ taking longer than the specified timeout and dbcancel is not working.
199
+ Return INT_CANCEL to abort the current command batch.
200
+ */
201
+ static int handle_interrupt(void *ptr) {
202
+ GET_CLIENT_USERDATA((DBPROCESS *)ptr);
203
+ if (userdata->timing_out) {
204
+ return INT_CANCEL;
205
+ }
206
+ return INT_CONTINUE;
207
+ }
208
+
168
209
  static void rb_tinytds_client_reset_userdata(tinytds_client_userdata *userdata) {
169
210
  userdata->timing_out = 0;
170
211
  userdata->dbsql_sent = 0;
171
212
  userdata->dbsqlok_sent = 0;
172
213
  userdata->dbcancel_sent = 0;
173
214
  userdata->nonblocking = 0;
174
- 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;
175
219
  }
176
220
 
177
221
  static void rb_tinytds_client_mark(void *ptr) {
@@ -252,8 +296,8 @@ static VALUE rb_tinytds_execute(VALUE self, VALUE sql) {
252
296
  REQUIRE_OPEN_CLIENT(cwrap);
253
297
  dbcmd(cwrap->client, StringValueCStr(sql));
254
298
  if (dbsqlsend(cwrap->client) == FAIL) {
255
- rb_warn("TinyTds: dbsqlsend() returned FAIL.\n");
256
- return Qfalse;
299
+ rb_raise(cTinyTdsError, "failed to execute statement");
300
+ return self;
257
301
  }
258
302
  cwrap->userdata->dbsql_sent = 1;
259
303
  result = rb_tinytds_new_result_obj(cwrap);
@@ -381,6 +425,7 @@ static VALUE rb_tinytds_connect(VALUE self, VALUE opts) {
381
425
  }
382
426
  }
383
427
  dbsetuserdata(cwrap->client, (BYTE*)cwrap->userdata);
428
+ dbsetinterrupt(cwrap->client, check_interrupt, handle_interrupt);
384
429
  cwrap->userdata->closed = 0;
385
430
  if (!NIL_P(database) && (azure != Qtrue)) {
386
431
  dbuse(cwrap->client, StringValueCStr(database));
@@ -5,9 +5,9 @@
5
5
  void init_tinytds_client();
6
6
 
7
7
  #define ERROR_MSG_SIZE 1024
8
+ #define ERRORS_STACK_INIT_SIZE 2
8
9
 
9
10
  typedef struct {
10
- short int is_set;
11
11
  int is_message;
12
12
  int cancel;
13
13
  char error[ERROR_MSG_SIZE];
@@ -25,7 +25,9 @@ typedef struct {
25
25
  RETCODE dbsqlok_retcode;
26
26
  short int dbcancel_sent;
27
27
  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;
29
31
  VALUE message_handler;
30
32
  } tinytds_client_userdata;
31
33
 
@@ -40,7 +42,7 @@ typedef struct {
40
42
  rb_encoding *encoding;
41
43
  } tinytds_client_wrapper;
42
44
 
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);
44
46
 
45
47
  // Lib Macros
46
48
 
@@ -86,26 +86,38 @@ static void dbcancel_ubf(DBPROCESS *client) {
86
86
  static void nogvl_setup(DBPROCESS *client) {
87
87
  GET_CLIENT_USERDATA(client);
88
88
  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;
89
92
  }
90
93
 
91
94
  static void nogvl_cleanup(DBPROCESS *client) {
92
95
  GET_CLIENT_USERDATA(client);
93
96
  userdata->nonblocking = 0;
97
+ userdata->timing_out = 0;
94
98
  /*
95
99
  Now that the blocking operation is done, we can finally throw any
96
100
  exceptions based on errors from SQL Server.
97
101
  */
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);
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);
108
116
  }
117
+
118
+ free(userdata->nonblocking_errors);
119
+ userdata->nonblocking_errors_length = 0;
120
+ userdata->nonblocking_errors_size = 0;
109
121
  }
110
122
 
111
123
  static RETCODE nogvl_dbsqlok(DBPROCESS *client) {
data/test/client_test.rb CHANGED
@@ -68,6 +68,9 @@ class ClientTest < TinyTds::TestCase
68
68
  end
69
69
 
70
70
  describe 'With in-valid options' do
71
+ before(:all) do
72
+ init_toxiproxy
73
+ end
71
74
 
72
75
  it 'raises an argument error when no :host given and :dataserver is blank' do
73
76
  assert_raises(ArgumentError) { new_connection :dataserver => nil, :host => nil }
@@ -129,30 +132,46 @@ class ClientTest < TinyTds::TestCase
129
132
  end
130
133
  end
131
134
 
132
- it 'must run this test to prove we account for dropped connections' do
133
- skip
135
+ it 'raises TinyTds exception with tcp socket network failure' do
136
+ skip if ENV['CI'] && ENV['APPVEYOR_BUILD_FOLDER'] # only CI using docker
134
137
  begin
135
- client = new_connection :login_timeout => 2, :timeout => 2
138
+ client = new_connection timeout: 2, port: 1234
136
139
  assert_client_works(client)
137
- STDOUT.puts "Disconnect network!"
138
- sleep 10
139
- STDOUT.puts "This should not get stuck past 6 seconds!"
140
- action = lambda { client.execute('SELECT 1 as [one]').each }
141
- assert_raise_tinytds_error(action) do |e|
142
- assert_equal 20003, e.db_error_number
143
- assert_equal 6, e.severity
144
- assert_match %r{timed out}i, e.message, 'ignore if non-english test run'
140
+ action = lambda { client.execute("waitfor delay '00:00:05'").do }
141
+
142
+ # Use toxiproxy to close the TCP socket after 1 second.
143
+ # We want TinyTds to execute the statement, hit the timeout configured above, and then not be able to use the network to cancel
144
+ # the network connection needs to close after the sql batch is sent and before the timeout above is hit
145
+ Toxiproxy[:sqlserver_test].toxic(:slow_close, delay: 1000).apply do
146
+ assert_raise_tinytds_error(action) do |e|
147
+ assert_equal 20003, e.db_error_number
148
+ assert_equal 6, e.severity
149
+ assert_match %r{timed out}i, e.message, 'ignore if non-english test run'
150
+ end
145
151
  end
146
152
  ensure
147
- STDOUT.puts "Reconnect network!"
148
- sleep 10
149
- action = lambda { client.execute('SELECT 1 as [one]').each }
150
- assert_raise_tinytds_error(action) do |e|
151
- assert_equal 20047, e.db_error_number
152
- assert_equal 1, e.severity
153
- assert_match %r{dead or not enabled}i, e.message, 'ignore if non-english test run'
153
+ assert_new_connections_work
154
+ end
155
+ end
156
+
157
+ it 'raises TinyTds exception with dead connection network failure' do
158
+ skip if ENV['CI'] && ENV['APPVEYOR_BUILD_FOLDER'] # only CI using docker
159
+ begin
160
+ client = new_connection timeout: 2, port: 1234
161
+ assert_client_works(client)
162
+ action = lambda { client.execute("waitfor delay '00:00:05'").do }
163
+
164
+ # Use toxiproxy to close the network connection after 1 second.
165
+ # We want TinyTds to execute the statement, hit the timeout configured above, and then not be able to use the network to cancel
166
+ # the network connection needs to close after the sql batch is sent and before the timeout above is hit
167
+ Toxiproxy[:sqlserver_test].toxic(:timeout, timeout: 1000).apply do
168
+ assert_raise_tinytds_error(action) do |e|
169
+ assert_equal 20047, e.db_error_number
170
+ assert_includes [1,9], e.severity
171
+ assert_match %r{dead or not enabled}i, e.message, 'ignore if non-english test run'
172
+ end
154
173
  end
155
- close_client(client)
174
+ ensure
156
175
  assert_new_connections_work
157
176
  end
158
177
  end
data/test/result_test.rb CHANGED
@@ -652,6 +652,48 @@ class ResultTest < TinyTds::TestCase
652
652
  assert_equal 1, messages.length, 'there should be one message after one print statement'
653
653
  assert_equal msg, m.message, 'message text'
654
654
  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
655
697
  end
656
698
 
657
699
  it 'must not raise an error when severity is 10 or less' do
@@ -770,4 +812,3 @@ class ResultTest < TinyTds::TestCase
770
812
  end
771
813
 
772
814
  end
773
-
data/test/test_helper.rb CHANGED
@@ -2,6 +2,7 @@
2
2
  require 'bundler' ; Bundler.require :development, :test
3
3
  require 'tiny_tds'
4
4
  require 'minitest/autorun'
5
+ require 'toxiproxy'
5
6
 
6
7
  TINYTDS_SCHEMAS = ['sqlserver_2000', 'sqlserver_2005', 'sqlserver_2008', 'sqlserver_2014', 'sqlserver_azure', 'sybase_ase'].freeze
7
8
 
@@ -153,6 +154,8 @@ module TinyTds
153
154
  loader.execute(drop_sql).do
154
155
  loader.execute(schema_sql).do
155
156
  loader.execute(sp_sql).do
157
+ loader.execute(sp_error_sql).do
158
+ loader.execute(sp_several_prints_sql).do
156
159
  loader.close
157
160
  true
158
161
  end
@@ -167,7 +170,16 @@ module TinyTds
167
170
  ) DROP TABLE datatypes
168
171
  IF EXISTS(
169
172
  SELECT 1 FROM sysobjects WHERE type = 'P' AND name = 'tinytds_TestReturnCodes'
170
- ) 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|
171
183
  end
172
184
 
173
185
  def drop_sql_microsoft
@@ -181,7 +193,15 @@ module TinyTds
181
193
  IF EXISTS (
182
194
  SELECT name FROM sysobjects
183
195
  WHERE name = 'tinytds_TestReturnCodes' AND type = 'P'
184
- ) 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|
185
205
  end
186
206
 
187
207
  def sp_sql
@@ -191,6 +211,21 @@ module TinyTds
191
211
  RETURN(420) |
192
212
  end
193
213
 
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
+
194
229
  def find_value(id, column, query_options={})
195
230
  query_options[:timezone] ||= :utc
196
231
  sql = "SELECT [#{column}] FROM [datatypes] WHERE [id] = #{id}"
@@ -212,6 +247,25 @@ module TinyTds
212
247
  client.execute("ROLLBACK TRANSACTION").do
213
248
  end
214
249
 
250
+ def init_toxiproxy
251
+ return if ENV['APPVEYOR_BUILD_FOLDER'] # only for CI using docker
252
+
253
+ # In order for toxiproxy to work for local docker instances of mssql, the containers must be on the same network
254
+ # and the host used below must match the mssql container name so toxiproxy knows where to proxy to.
255
+ # localhost from the perspective of toxiproxy's container is its own container an *not* the mssql container it needs to proxy to.
256
+ # docker-compose.yml handles this automatically for us. In instances where someone is using their own local mssql container they'll
257
+ # need to set up the networks manually and set TINYTDS_UNIT_HOST to their mssql container name
258
+ # For anything other than localhost just use the environment config
259
+ env_host = ENV['TINYTDS_UNIT_HOST_TEST'] || ENV['TINYTDS_UNIT_HOST'] || 'localhost'
260
+ host = ['localhost', '127.0.0.1', '0.0.0.0'].include?(env_host) ? 'sqlserver' : env_host
261
+ port = ENV['TINYTDS_UNIT_PORT'] || 1433
262
+ Toxiproxy.populate([
263
+ {
264
+ name: "sqlserver_test",
265
+ listen: "0.0.0.0:1234",
266
+ upstream: "#{host}:#{port}"
267
+ }
268
+ ])
269
+ end
215
270
  end
216
271
  end
217
-
data/tiny_tds.gemspec CHANGED
@@ -26,4 +26,5 @@ Gem::Specification.new do |s|
26
26
  s.add_development_dependency 'rake-compiler-dock', '~> 1.0'
27
27
  s.add_development_dependency 'minitest', '~> 5.6'
28
28
  s.add_development_dependency 'connection_pool', '~> 2.2'
29
+ s.add_development_dependency 'toxiproxy', '~> 2.0.0'
29
30
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tiny_tds
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.3
4
+ version: 2.1.4.pre2
5
5
  platform: x64-mingw32
6
6
  authors:
7
7
  - Ken Collins
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-09-28 00:00:00.000000000 Z
13
+ date: 2021-03-22 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: mini_portile2
@@ -96,6 +96,20 @@ dependencies:
96
96
  - - "~>"
97
97
  - !ruby/object:Gem::Version
98
98
  version: '2.2'
99
+ - !ruby/object:Gem::Dependency
100
+ name: toxiproxy
101
+ requirement: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - "~>"
104
+ - !ruby/object:Gem::Version
105
+ version: 2.0.0
106
+ type: :development
107
+ prerelease: false
108
+ version_requirements: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - "~>"
111
+ - !ruby/object:Gem::Version
112
+ version: 2.0.0
99
113
  description: TinyTDS - A modern, simple and fast FreeTDS library for Ruby using DB-Library.
100
114
  Developed for the ActiveRecord SQL Server adapter.
101
115
  email:
@@ -123,6 +137,7 @@ files:
123
137
  - appveyor.yml
124
138
  - bin/defncopy-ttds
125
139
  - bin/tsql-ttds
140
+ - docker-compose.yml
126
141
  - exe/.keep
127
142
  - ext/tiny_tds/client.c
128
143
  - ext/tiny_tds/client.h
@@ -225,9 +240,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
225
240
  version: 2.8.dev
226
241
  required_rubygems_version: !ruby/object:Gem::Requirement
227
242
  requirements:
228
- - - ">="
243
+ - - ">"
229
244
  - !ruby/object:Gem::Version
230
- version: '0'
245
+ version: 1.3.1
231
246
  requirements: []
232
247
  rubygems_version: 3.1.2
233
248
  signing_key: