Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cancel results for user automatically #563

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Drop support for SQL Server < 2017
* Drop support for FreeTDS < 1.0
* Raise error if FreeTDS is unable to sent command buffer to the server
* Cancel previous query results automatically when invoking a new query

## 2.1.7
* Add Ruby 3.3 to the cross compile list
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ result = client.execute("SELECT * FROM [datatypes]")

## TinyTds::Result Usage

A result object is returned by the client's execute command. It is important that you either return the data from the query, most likely with the #each method, or that you cancel the results before asking the client to execute another SQL batch. Failing to do so will yield an error.
A result object is returned by the client's execute command. It is important that you either return the data from the query, most likely with the #each method. You can manually cancel the results early using `#cancel`, otherwise tiny_tds will also automatically do so when running a new query with `#execute`.

Calling #each on the result will lazily load each row from the database.

Expand All @@ -171,7 +171,6 @@ result.do

result = client.execute("SELECT [id] FROM [datatypes]")
result.fields # => ["id"]
result.cancel
result = client.execute("SELECT [id] FROM [datatypes]")
result.each(:symbolize_keys => true)
result.fields # => [:id]
Expand Down
33 changes: 26 additions & 7 deletions ext/tiny_tds/client.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <tiny_tds_ext.h>
#include <errno.h>
#include <nogvl.h>

VALUE cTinyTdsClient;
extern VALUE mTinyTds, cTinyTdsError;
Expand All @@ -15,12 +16,6 @@ VALUE opt_escape_regex, opt_escape_dblquote;
tinytds_client_wrapper *cwrap; \
Data_Get_Struct(self, tinytds_client_wrapper, cwrap)

#define REQUIRE_OPEN_CLIENT(cwrap) \
if (cwrap->closed || cwrap->userdata->closed) { \
rb_raise(cTinyTdsError, "closed connection"); \
return Qnil; \
}


// Lib Backend (Helpers)

Expand Down Expand Up @@ -295,8 +290,32 @@ static VALUE rb_tinytds_execute(VALUE self, VALUE sql) {
VALUE result;

GET_CLIENT_WRAPPER(self);

if (cwrap->closed || cwrap->userdata->closed) {
rb_raise(cTinyTdsError, "closed connection");
return Qnil;
}

if (rb_tinytds_dead(self) == Qtrue) {
rb_raise(cTinyTdsError, "client is dead, please create a new instance");
return Qnil;
}

// user is coming back from an each loop, make sure we cancel the pending results
if (cwrap->userdata->dbsql_sent) {
// if we do not run dbsqlok, FreeTDS will throw an error
// Attempt to initiate a new Adaptive Server operation with results pending
if (cwrap->userdata->dbsqlok_sent == 0) {
if(nogvl_dbsqlok(cwrap->client) != SUCCEED) {
rb_raise(cTinyTdsError, "unable to acknowledge previous results with server");
}
}

dbcancel(cwrap->client);
}

rb_tinytds_client_reset_userdata(cwrap->userdata);
REQUIRE_OPEN_CLIENT(cwrap);

dbcmd(cwrap->client, StringValueCStr(sql));
if (dbsqlsend(cwrap->client) == FAIL) {
rb_raise(cTinyTdsError, "failed dbsqlsend() function");
Expand Down
81 changes: 81 additions & 0 deletions ext/tiny_tds/nogvl.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#include <tiny_tds_ext.h>

void nogvl_setup(DBPROCESS *client) {
GET_CLIENT_USERDATA(client);
userdata->nonblocking = 1;
userdata->nonblocking_errors_length = 0;
userdata->nonblocking_errors = malloc(ERRORS_STACK_INIT_SIZE * sizeof(tinytds_errordata));
userdata->nonblocking_errors_size = ERRORS_STACK_INIT_SIZE;
}

void nogvl_cleanup(DBPROCESS *client) {
GET_CLIENT_USERDATA(client);
userdata->nonblocking = 0;
userdata->timing_out = 0;
/*
Now that the blocking operation is done, we can finally throw any
exceptions based on errors from SQL Server.
*/
short int i;
for (i = 0; i < userdata->nonblocking_errors_length; i++) {
tinytds_errordata error = userdata->nonblocking_errors[i];

// lookahead to drain any info messages ahead of raising error
if (!error.is_message) {
short int j;
for (j = i; j < userdata->nonblocking_errors_length; j++) {
tinytds_errordata msg_error = userdata->nonblocking_errors[j];
if (msg_error.is_message) {
rb_tinytds_raise_error(client, msg_error);
}
}
}

rb_tinytds_raise_error(client, error);
}

free(userdata->nonblocking_errors);
userdata->nonblocking_errors_length = 0;
userdata->nonblocking_errors_size = 0;
}

void dbcancel_ubf(DBPROCESS *client) {
GET_CLIENT_USERDATA(client);
dbcancel(client);
userdata->dbcancel_sent = 1;
}

// No GVL Helpers
RETCODE nogvl_dbsqlexec(DBPROCESS *client) {
int retcode = FAIL;
nogvl_setup(client);
retcode = NOGVL_DBCALL(dbsqlexec, client);
nogvl_cleanup(client);
return retcode;
}

RETCODE nogvl_dbsqlok(DBPROCESS *client) {
int retcode = FAIL;
GET_CLIENT_USERDATA(client);
nogvl_setup(client);
retcode = NOGVL_DBCALL(dbsqlok, client);
nogvl_cleanup(client);
userdata->dbsqlok_sent = 1;
return retcode;
}

RETCODE nogvl_dbresults(DBPROCESS *client) {
int retcode = FAIL;
nogvl_setup(client);
retcode = NOGVL_DBCALL(dbresults, client);
nogvl_cleanup(client);
return retcode;
}

RETCODE nogvl_dbnextrow(DBPROCESS * client) {
int retcode = FAIL;
nogvl_setup(client);
retcode = NOGVL_DBCALL(dbnextrow, client);
nogvl_cleanup(client);
return retcode;
}
16 changes: 16 additions & 0 deletions ext/tiny_tds/nogvl.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#ifndef TINYTDS_NOGVL_H
#define TINYTDS_NOGVL_H

#define NOGVL_DBCALL(_dbfunction, _client) ( \
(RETCODE)(intptr_t)rb_thread_call_without_gvl( \
(void *(*)(void *))_dbfunction, _client, \
(rb_unblock_function_t*)dbcancel_ubf, _client ) \
)

void dbcancel_ubf(DBPROCESS *client);
RETCODE nogvl_dbnextrow(DBPROCESS * client);
RETCODE nogvl_dbresults(DBPROCESS *client);
RETCODE nogvl_dbsqlexec(DBPROCESS *client);
RETCODE nogvl_dbsqlok(DBPROCESS *client);

#endif
87 changes: 0 additions & 87 deletions ext/tiny_tds/result.c
Original file line number Diff line number Diff line change
Expand Up @@ -69,93 +69,6 @@ VALUE rb_tinytds_new_result_obj(tinytds_client_wrapper *cwrap) {
return obj;
}

// No GVL Helpers

#define NOGVL_DBCALL(_dbfunction, _client) ( \
(RETCODE)(intptr_t)rb_thread_call_without_gvl( \
(void *(*)(void *))_dbfunction, _client, \
(rb_unblock_function_t*)dbcancel_ubf, _client ) \
)

static void dbcancel_ubf(DBPROCESS *client) {
GET_CLIENT_USERDATA(client);
dbcancel(client);
userdata->dbcancel_sent = 1;
}

static void nogvl_setup(DBPROCESS *client) {
GET_CLIENT_USERDATA(client);
userdata->nonblocking = 1;
userdata->nonblocking_errors_length = 0;
userdata->nonblocking_errors = malloc(ERRORS_STACK_INIT_SIZE * sizeof(tinytds_errordata));
userdata->nonblocking_errors_size = ERRORS_STACK_INIT_SIZE;
}

static void nogvl_cleanup(DBPROCESS *client) {
GET_CLIENT_USERDATA(client);
userdata->nonblocking = 0;
userdata->timing_out = 0;
/*
Now that the blocking operation is done, we can finally throw any
exceptions based on errors from SQL Server.
*/
short int i;
for (i = 0; i < userdata->nonblocking_errors_length; i++) {
tinytds_errordata error = userdata->nonblocking_errors[i];

// lookahead to drain any info messages ahead of raising error
if (!error.is_message) {
short int j;
for (j = i; j < userdata->nonblocking_errors_length; j++) {
tinytds_errordata msg_error = userdata->nonblocking_errors[j];
if (msg_error.is_message) {
rb_tinytds_raise_error(client, msg_error);
}
}
}

rb_tinytds_raise_error(client, error);
}

free(userdata->nonblocking_errors);
userdata->nonblocking_errors_length = 0;
userdata->nonblocking_errors_size = 0;
}

static RETCODE nogvl_dbsqlok(DBPROCESS *client) {
int retcode = FAIL;
GET_CLIENT_USERDATA(client);
nogvl_setup(client);
retcode = NOGVL_DBCALL(dbsqlok, client);
nogvl_cleanup(client);
userdata->dbsqlok_sent = 1;
return retcode;
}

static RETCODE nogvl_dbsqlexec(DBPROCESS *client) {
int retcode = FAIL;
nogvl_setup(client);
retcode = NOGVL_DBCALL(dbsqlexec, client);
nogvl_cleanup(client);
return retcode;
}

static RETCODE nogvl_dbresults(DBPROCESS *client) {
int retcode = FAIL;
nogvl_setup(client);
retcode = NOGVL_DBCALL(dbresults, client);
nogvl_cleanup(client);
return retcode;
}

static RETCODE nogvl_dbnextrow(DBPROCESS * client) {
int retcode = FAIL;
nogvl_setup(client);
retcode = NOGVL_DBCALL(dbnextrow, client);
nogvl_cleanup(client);
return retcode;
}

// Lib Backend (Helpers)

static RETCODE rb_tinytds_result_dbresults_retcode(VALUE self) {
Expand Down
1 change: 1 addition & 0 deletions ext/tiny_tds/tiny_tds_ext.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <sybfront.h>
#include <sybdb.h>

#include <nogvl.h>
#include <client.h>
#include <result.h>

Expand Down
41 changes: 41 additions & 0 deletions test/client_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -269,4 +269,45 @@ class ClientTest < TinyTds::TestCase
).must_equal 'user'
end
end

describe "#execute" do
it "cancels pending select query" do
client = new_connection
client.execute("SELECT 1 as [one]")

assert client.sqlsent?
assert !client.canceled?

result = client.execute("SELECT 1 as [one]")
assert_equal [{"one"=>1}], result.to_a
assert_client_works(client)
end

it "cancels pending wait query" do
client = new_connection
client.execute("WaitFor Delay '00:00:05'")

assert client.sqlsent?
assert !client.canceled?

result = client.execute("SELECT 1 as [one]")
assert_equal [{"one"=>1}], result.to_a
assert_client_works(client)
end

# this requires to not send another `dbsqlok` compared to the previous to test cases
it "cancel partially retrieved results" do
client = new_connection
result = client.execute("SELECT 1 as [one]; SELECT 2 as [two]; SELECT 3 as [three]")
result.each { |r| break if r.key?("two") }

assert_equal 1, result.count
assert client.sqlsent?
assert !client.canceled?

result = client.execute("SELECT 1 as [one]")
assert_equal [{"one"=>1}], result.to_a
assert_client_works(client)
end
end
end