diff --git a/README.md b/README.md index 7559fa1..dd33ca1 100644 --- a/README.md +++ b/README.md @@ -640,6 +640,66 @@ odbc.connect(`${process.env.CONNECTION_STRING}`, (error, connection) => { --- +### `.cancel(callback?)` + +Cancels all operations currently running on the connection (queries and procedure calls) by calling `SQLCancel` on their statement handles. The cancelled operations return with SQLSTATE HY008 ("Operation canceled"), so their promises reject (or their callbacks are called with an error). If no operations are running, `.cancel` is a no-op. + +**Note:** `SQLCancel` only _requests_ cancellation — when the operation actually aborts depends on the driver and on the database engine reaching a cancellation checkpoint. Row-producing operations (scans, fetches) usually abort promptly, but some operations never check for cancellation and only fail with HY008 once they finish on their own. Known examples: Impala's `sleep()` function completes its full wait before honoring the cancel, and calls to stored procedures on Db2 for IBM i are not cancelable at all. + +#### Parameters: +* **callback?**: The function called when `.cancel` has finished execution. If no callback function is given, `.cancel` will return a native JavaScript `Promise`. Callback signature is: + * error: The error that occured in execution, or `null` if no error + +#### Examples: + +**Promises** + +```javascript +const odbc = require('odbc'); + +// can only use await keyword in an async function +async function cancelExample() { + const connection = await odbc.connect(`${process.env.CONNECTION_STRING}`); + + // cancel the query if it is still running after 10 seconds + const timer = setTimeout(() => { + connection.cancel(); + }, 10000); + + try { + const result = await connection.query('SELECT * FROM HUGE_TABLE'); + console.log(result); + } catch (error) { + // if cancelled, error contains an odbcError with state 'HY008' + } finally { + clearTimeout(timer); + } +} + +cancelExample(); +``` + +**Callbacks** + +```javascript +const odbc = require('odbc'); + +odbc.connect(`${process.env.CONNECTION_STRING}`, (error, connection) => { + const timer = setTimeout(() => { + connection.cancel((cancelError) => { + if (cancelError) { return; } // handle + }); + }, 10000); + + connection.query('SELECT * FROM HUGE_TABLE', (queryError, result) => { + clearTimeout(timer); + // if cancelled, queryError contains an odbcError with state 'HY008' + }); +}); +``` + +--- + ### `.close(callback?)` Closes an open connection. Any transactions on the connection that have not been ended will be rolledback. @@ -1027,6 +1087,74 @@ odbc.connect(`${process.env.CONNECTION_STRING}`, (error, connection) => { --- +### `.cancel(callback?)` + +Cancels any operation currently running on the Statement (e.g. a long `.execute()`) by calling `SQLCancel` on its handle. The cancelled operation returns with SQLSTATE HY008 ("Operation canceled"). + +**Note:** the cancellation-checkpoint caveat described in [connection.cancel()](#cancelcallback) applies here as well. + +#### Parameters: +* **callback?**: The function called when `.cancel` has finished execution. If no callback function is given, `.cancel` will return a native JavaScript `Promise`. Callback signature is: + * error: The error that occured in execution, or `null` if no error + +#### Examples: + +**Promises** + +```javascript +const odbc = require('odbc'); + +// can only use await keyword in an async function +async function cancelExample() { + const connection = await odbc.connect(`${process.env.CONNECTION_STRING}`); + const statement = await connection.createStatement(); + await statement.prepare('SELECT * FROM HUGE_TABLE WHERE FIELD_1 = ?'); + await statement.bind([1]); + + // cancel the execution if it is still running after 10 seconds + const timer = setTimeout(() => { + statement.cancel(); + }, 10000); + + try { + const result = await statement.execute(); + console.log(result); + } catch (error) { + // if cancelled, error contains an odbcError with state 'HY008' + } finally { + clearTimeout(timer); + } +} + +cancelExample(); +``` + +**Callbacks** + +```javascript +const odbc = require('odbc'); + +odbc.connect(`${process.env.CONNECTION_STRING}`, (error, connection) => { + connection.createStatement((error1, statement) => { + if (error1) { return; } // handle + statement.prepare('SELECT * FROM HUGE_TABLE', (error2) => { + if (error2) { return; } // handle + const timer = setTimeout(() => { + statement.cancel((cancelError) => { + if (cancelError) { return; } // handle + }); + }, 10000); + statement.execute((error3, result) => { + clearTimeout(timer); + // if cancelled, error3 contains an odbcError with state 'HY008' + }); + }); + }); +}); +``` + +--- + ### `.close(callback?)` Closes the Statement, freeing the statement handle. Running functions on the statement after closing will result in an error. diff --git a/lib/Connection.js b/lib/Connection.js index a9efd3a..735849c 100644 --- a/lib/Connection.js +++ b/lib/Connection.js @@ -295,6 +295,44 @@ class Connection { }); } + /** + * Cancels all operations currently running on this connection (queries and + * procedure calls) by calling SQLCancel on their statement handles. The + * cancelled operations return with SQLSTATE HY008 ("Operation canceled"). + * Asynchronous, can be used either with a callback function or a Promise. + * @param {function} [callback] - Callback function. If not passed, a Promise will be returned. + */ + cancel(callback = undefined) { + // type-checking + if (typeof callback !== 'function' && typeof callback !== 'undefined') { + throw new TypeError('[node-odbc]: Incorrect function signature for call to connection.cancel({function}[optional]).'); + } + + // promise... + if (callback === undefined) { + if (!this.odbcConnection) + { + throw new Error(Connection.CONNECTION_CLOSED_ERROR); + } + return new Promise((resolve, reject) => { + this.odbcConnection.cancel((error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); + } + + // ...or callback + if (!this.odbcConnection) + { + return callback(new Error(Connection.CONNECTION_CLOSED_ERROR)); + } + return this.odbcConnection.cancel((error) => callback(error)); + } + // TODO: Documentation primaryKeys(catalog, schema, table, callback = undefined) { // promise... diff --git a/lib/Statement.js b/lib/Statement.js index 34ea9ad..053ef9d 100644 --- a/lib/Statement.js +++ b/lib/Statement.js @@ -168,10 +168,43 @@ class Statement { } } + /** + * Cancels any operation currently running on this statement (e.g. a long + * execute()) by calling SQLCancel on its handle. The cancelled operation + * returns with SQLSTATE HY008 ("Operation canceled"). + * @param {function} [callback] - The callback function that returns the result. If omitted, uses a Promise. + */ + cancel(callback = undefined) { + if (typeof callback !== 'function' && typeof callback !== 'undefined') { + throw new TypeError('[node-odbc]: Incorrect function signature for call to statement.cancel({function}[optional]).'); + } + + if (typeof callback === 'undefined') { + if (!this.odbcStatement) { + throw new Error(Statement.STATEMENT_CLOSED_ERROR); + } + return new Promise((resolve, reject) => { + this.odbcStatement.cancel((error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); + } + + // ...or callback + if (!this.odbcStatement) { + return callback(new Error(Statement.STATEMENT_CLOSED_ERROR)); + } + return this.odbcStatement.cancel((error) => callback(error)); + } + /** * Closes the statement, deleting the prepared statement and freeing the handle, making further * calls on the object invalid. - * @param {function} [callback] - The callback function that returns the result. If omitted, uses a Promise. + * @param {function} [callback] - The callback function that returns the result. If omitted, uses a Promise. */ close(callback = undefined) { if (typeof callback !== 'function' && typeof callback !== 'undefined') { diff --git a/lib/odbc.d.ts b/lib/odbc.d.ts index 3fab1a0..f058fd4 100644 --- a/lib/odbc.d.ts +++ b/lib/odbc.d.ts @@ -39,6 +39,8 @@ declare namespace odbc { execute(callback: (error: NodeOdbcError, result: Result) => undefined): undefined; + cancel(callback: (error: NodeOdbcError) => undefined): undefined; + close(callback: (error: NodeOdbcError) => undefined): undefined; //////////////////////////////////////////////////////////////////////////// @@ -51,6 +53,8 @@ declare namespace odbc { execute(): Promise>; + cancel(): Promise; + close(): Promise; } @@ -112,6 +116,8 @@ declare namespace odbc { rollback(callback: (error: NodeOdbcError) => undefined): undefined; + cancel(callback: (error: NodeOdbcError) => undefined): undefined; + close(callback: (error: NodeOdbcError) => undefined): undefined; //////////////////////////////////////////////////////////////////////////// @@ -142,6 +148,8 @@ declare namespace odbc { rollback(): Promise; + cancel(): Promise; + close(): Promise; connected(): boolean; diff --git a/src/odbc.h b/src/odbc.h index 4d5e7c5..3c0a3a5 100644 --- a/src/odbc.h +++ b/src/odbc.h @@ -190,7 +190,7 @@ typedef struct StatementData { SQLHENV henv; SQLHDBC hdbc; - SQLHSTMT hstmt; + SQLHSTMT hstmt = SQL_NULL_HANDLE; QueryOptions query_options; diff --git a/src/odbc_connection.cpp b/src/odbc_connection.cpp index 4a6e2e8..f20f8aa 100644 --- a/src/odbc_connection.cpp +++ b/src/odbc_connection.cpp @@ -47,6 +47,7 @@ Napi::Object ODBCConnection::Init(Napi::Env env, Napi::Object exports) { InstanceMethod("close", &ODBCConnection::Close), InstanceMethod("createStatement", &ODBCConnection::CreateStatement), InstanceMethod("query", &ODBCConnection::Query), + InstanceMethod("cancel", &ODBCConnection::Cancel), InstanceMethod("beginTransaction", &ODBCConnection::BeginTransaction), InstanceMethod("commit", &ODBCConnection::Commit), InstanceMethod("rollback", &ODBCConnection::Rollback), @@ -167,12 +168,27 @@ ODBCConnection::ODBCConnection(const Napi::CallbackInfo& info) : Napi::ObjectWra this->hDBC = *(info[1].As>().Data()); this->connectionOptions = *(info[2].As>().Data()); this->getInfoResults = *(info[3].As>().Data()); + + uv_mutex_init(&this->activeStatementsMutex); } ODBCConnection::~ODBCConnection() { this->Free(); + uv_mutex_destroy(&this->activeStatementsMutex); +} + +void ODBCConnection::RegisterActiveStatement(SQLHSTMT hstmt) { + uv_mutex_lock(&this->activeStatementsMutex); + this->activeStatements.insert(hstmt); + uv_mutex_unlock(&this->activeStatementsMutex); +} + +void ODBCConnection::UnregisterActiveStatement(SQLHSTMT hstmt) { + uv_mutex_lock(&this->activeStatementsMutex); + this->activeStatements.erase(hstmt); + uv_mutex_unlock(&this->activeStatementsMutex); } SQLRETURN ODBCConnection::Free() { @@ -743,6 +759,10 @@ class QueryAsyncWorker : public ODBCAsyncWorker { return; } + // make the handle reachable by connection.cancel() while this worker + // is executing + odbcConnectionObject->RegisterActiveStatement(data->hstmt); + // set SQL_ATTR_QUERY_TIMEOUT if (data->query_options.timeout > 0) { return_code = @@ -992,6 +1012,7 @@ class QueryAsyncWorker : public ODBCAsyncWorker { } ~QueryAsyncWorker() { + odbcConnectionObject->UnregisterActiveStatement(data->hstmt); if (!data->query_options.use_cursor) { uv_mutex_lock(&ODBC::g_odbcMutex); @@ -1151,6 +1172,68 @@ Napi::Value ODBCConnection::Query(const Napi::CallbackInfo& info) { return env.Undefined(); } +/* + * ODBCConnection::Cancel + * + * Description: Calls SQLCancel on the statement handles of all in-flight + * operations on this connection (queries and procedure + * calls). The cancelled operations will return with SQLSTATE + * HY008 ("Operation canceled"), rejecting their promises or + * calling their callbacks with an error. + * + * SQLCancel is called synchronously on the main thread + * instead of through an AsyncWorker: cancel() is most needed + * precisely when the libuv thread pool is saturated by the + * operations being cancelled, so queueing the cancel behind + * them could delay it indefinitely. Calling SQLCancel from a + * different thread than the one running the statement is the + * documented multithreaded-cancel use of the function, and it + * returns quickly. + * + * Parameters: + * const Napi::CallbackInfo& info: + * The information passed from the JavaSript environment, including the + * function arguments for 'cancel'. + * + * info[0]: Function: callback function: + * function(error) + * error: An error object if one or more statements could not be + * cancelled, or null if the operation was successful. + * + * Return: + * Napi::Value: + * Undefined (results returned in callback) + */ +Napi::Value ODBCConnection::Cancel(const Napi::CallbackInfo& info) { + + Napi::Env env = info.Env(); + Napi::HandleScope scope(env); + + Napi::Function callback = info[0].As(); + + size_t failure_count = 0; + + uv_mutex_lock(&this->activeStatementsMutex); + for (SQLHSTMT hstmt : this->activeStatements) { + SQLRETURN return_code = SQLCancel(hstmt); + if (!SQL_SUCCEEDED(return_code)) { + failure_count++; + } + } + uv_mutex_unlock(&this->activeStatementsMutex); + + std::vector callbackArguments; + if (failure_count > 0) { + Napi::Error error = Napi::Error::New(env, "[odbc] Error canceling one or more active statements"); + callbackArguments.push_back(error.Value()); + } else { + callbackArguments.push_back(env.Null()); + } + callback.Call(callbackArguments); + + return env.Undefined(); +} + // If we have a parameter with input/output params (e.g. calling a procedure), // then we need to take the Parameter structures of the StatementData and create // a Napi::Array from those that were overwritten. @@ -1282,6 +1365,10 @@ class CallProcedureAsyncWorker : public ODBCAsyncWorker { return; } + // make the handle reachable by connection.cancel() while this worker + // is executing + odbcConnectionObject->RegisterActiveStatement(data->hstmt); + return_code = set_fetch_size ( @@ -1924,6 +2011,7 @@ class CallProcedureAsyncWorker : public ODBCAsyncWorker { } ~CallProcedureAsyncWorker() { + odbcConnectionObject->UnregisterActiveStatement(data->hstmt); delete[] overwriteParams; delete data; data = NULL; diff --git a/src/odbc_connection.h b/src/odbc_connection.h index afd7442..b568832 100644 --- a/src/odbc_connection.h +++ b/src/odbc_connection.h @@ -20,6 +20,7 @@ #define _SRC_ODBC_CONNECTION_H #include +#include class ODBCConnection : public Napi::ObjectWrap { @@ -63,6 +64,7 @@ class ODBCConnection : public Napi::ObjectWrap { Napi::Value Close(const Napi::CallbackInfo& info); Napi::Value CreateStatement(const Napi::CallbackInfo& info); Napi::Value Query(const Napi::CallbackInfo& info); + Napi::Value Cancel(const Napi::CallbackInfo& info); Napi::Value CallProcedure(const Napi::CallbackInfo& info); Napi::Value BeginTransaction(const Napi::CallbackInfo& info); @@ -99,6 +101,15 @@ class ODBCConnection : public Napi::ObjectWrap { SQLHENV hENV; SQLHDBC hDBC; + // Statement handles for in-flight operations, registered by the + // AsyncWorkers so that cancel() can call SQLCancel on them from the main + // thread while a worker thread is blocked on the ODBC call. + uv_mutex_t activeStatementsMutex; + std::set activeStatements; + + void RegisterActiveStatement(SQLHSTMT hstmt); + void UnregisterActiveStatement(SQLHSTMT hstmt); + ConnectionOptions connectionOptions; GetInfoResults getInfoResults; diff --git a/src/odbc_statement.cpp b/src/odbc_statement.cpp index b180244..e7b79f9 100644 --- a/src/odbc_statement.cpp +++ b/src/odbc_statement.cpp @@ -34,6 +34,7 @@ Napi::Object ODBCStatement::Init(Napi::Env env, Napi::Object exports) { InstanceMethod("prepare", &ODBCStatement::Prepare), InstanceMethod("bind", &ODBCStatement::Bind), InstanceMethod("execute", &ODBCStatement::Execute), + InstanceMethod("cancel", &ODBCStatement::Cancel), InstanceMethod("close", &ODBCStatement::Close), }); @@ -546,6 +547,62 @@ Napi::Value ODBCStatement::Execute(const Napi::CallbackInfo& info) { return env.Undefined(); } +/****************************************************************************** + ********************************** CANCEL ************************************ + *****************************************************************************/ + +/* + * ODBCStatement::Cancel + * + * Description: Calls SQLCancel on this statement's handle, aborting any + * operation currently running on it (e.g. a long execute()). + * The cancelled operation will return with SQLSTATE HY008 + * ("Operation canceled"). + * + * SQLCancel is called synchronously on the main thread + * instead of through an AsyncWorker: cancel() is most needed + * precisely when the libuv thread pool is saturated by the + * operations being cancelled, so queueing the cancel behind + * them could delay it indefinitely. Calling SQLCancel from a + * different thread than the one running the statement is the + * documented multithreaded-cancel use of the function, and it + * returns quickly. + * + * Parameters: + * const Napi::CallbackInfo& info: + * The information passed from the JavaSript environment, including the + * function arguments for 'cancel'. + * + * info[0]: Function: callback function: + * function(error) + * error: An error object if the statement could not be + * cancelled, or null if the operation was successful. + * + * Return: + * Napi::Value: + * Undefined (results returned in callback) + */ +Napi::Value ODBCStatement::Cancel(const Napi::CallbackInfo& info) { + + Napi::Env env = info.Env(); + Napi::HandleScope scope(env); + + Napi::Function callback = info[0].As(); + + SQLRETURN return_code = SQLCancel(this->data->hstmt); + + std::vector callbackArguments; + if (!SQL_SUCCEEDED(return_code)) { + Napi::Error error = Napi::Error::New(env, "[odbc] Error canceling the statement"); + callbackArguments.push_back(error.Value()); + } else { + callbackArguments.push_back(env.Null()); + } + callback.Call(callbackArguments); + + return env.Undefined(); +} + /****************************************************************************** ********************************** CLOSE ************************************* *****************************************************************************/ diff --git a/src/odbc_statement.h b/src/odbc_statement.h index 6f9325a..c23e30f 100644 --- a/src/odbc_statement.h +++ b/src/odbc_statement.h @@ -38,6 +38,7 @@ class ODBCStatement : public Napi::ObjectWrap { Napi::Value Prepare(const Napi::CallbackInfo& info); Napi::Value Bind(const Napi::CallbackInfo& info); Napi::Value Execute(const Napi::CallbackInfo& info); + Napi::Value Cancel(const Napi::CallbackInfo& info); Napi::Value Close(const Napi::CallbackInfo& info); }; #endif