diff --git a/funcs/func_odbc.c b/funcs/func_odbc.c index 4744ba626f..23930ed4d8 100644 --- a/funcs/func_odbc.c +++ b/funcs/func_odbc.c @@ -42,6 +42,7 @@ ASTERISK_REGISTER_FILE() #include "asterisk/pbx.h" #include "asterisk/config.h" #include "asterisk/res_odbc.h" +#include "asterisk/res_odbc_transaction.h" #include "asterisk/app.h" #include "asterisk/cli.h" #include "asterisk/strings.h" diff --git a/include/asterisk/config.h b/include/asterisk/config.h index bd268a333e..9a899d8d06 100644 --- a/include/asterisk/config.h +++ b/include/asterisk/config.h @@ -495,6 +495,17 @@ int ast_unload_realtime(const char *family); * * \note You should use the constant SENTINEL to terminate arguments, in * order to preserve cross-platform compatibility. + * + * TODO The return value of this function is routinely ignored. Ignoring + * the return value means that it's mostly pointless to be calling this. + * You'll see some warning messages potentially, but that's it. + * + * XXX This function is super useful for detecting configuration problems + * early, but unfortunately, the latest in configuration management, sorcery, + * doesn't work well with this. Users of sorcery are familiar with the fields + * they will need to write but don't know if realtime is being used. Sorcery + * knows what storage mechanism is being used but has no high-level knowledge + * of what sort of data is going to be written. */ int ast_realtime_require_field(const char *family, ...) attribute_sentinel; diff --git a/include/asterisk/res_odbc.h b/include/asterisk/res_odbc.h index 7d9d4a19e8..8c7b549506 100644 --- a/include/asterisk/res_odbc.h +++ b/include/asterisk/res_odbc.h @@ -46,16 +46,11 @@ enum { struct odbc_obj { SQLHDBC con; /*!< ODBC Connection Handle */ struct odbc_class *parent; /*!< Information about the connection is protected */ - struct timeval last_used; /*!< Used by idlecheck to determine if the connection should be renegotiated */ #ifdef DEBUG_THREADS char file[80]; char function[80]; int lineno; #endif - unsigned int used:1; /*!< Is this connection currently in use? */ - unsigned int up:1; - unsigned int tx:1; /*!< Should this connection be unshared, regardless of the class setting? */ - struct odbc_txn_frame *txf; /*!< Reference back to the transaction frame, if applicable */ AST_LIST_ENTRY(odbc_obj) list; }; @@ -102,39 +97,29 @@ int ast_odbc_smart_execute(struct odbc_obj *obj, SQLHSTMT stmt) __attribute__((d /*! * \brief Retrieves a connected ODBC object - * \param name The name of the ODBC class for which a connection is needed. - * \param flags One or more of the following flags: - * \li RES_ODBC_SANITY_CHECK Whether to ensure that a connection is valid before returning the handle. Usually unnecessary. - * \li RES_ODBC_INDEPENDENT_CONNECTION Return a handle which is independent from all others. Usually used when starting a transaction. - * \li RES_ODBC_CONNECTED Only return a connected handle. Intended for use with peers which use idlecheck, which are checked periodically for reachability. - * \param file, function, lineno * - * \return ODBC object - * \retval NULL if there is no connection available with the requested name. + * \deprecated * - * Connection classes may, in fact, contain multiple connection handles. If - * the connection is pooled, then each connection will be dedicated to the - * thread which requests it. Note that all connections should be released - * when the thread is done by calling ast_odbc_release_obj(), below. + * This is only around for backwards-compatibility with older versions of Asterisk. */ struct odbc_obj *_ast_odbc_request_obj2(const char *name, struct ast_flags flags, const char *file, const char *function, int lineno); + +/*! + * \brief Get a ODBC connection object + * + * The "check" parameter is leftover from an earlier implementation where database connections + * were cached by res_odbc. Since connections are managed by unixODBC now, this parameter is + * only kept around for API compatibility. + * + * \param name The name of the res_odbc.conf section describing the database to connect to + * \param check unused + * \return A connection to the database. Call ast_odbc_release_obj() when finished. + */ struct odbc_obj *_ast_odbc_request_obj(const char *name, int check, const char *file, const char *function, int lineno); #define ast_odbc_request_obj2(a, b) _ast_odbc_request_obj2(a, b, __FILE__, __PRETTY_FUNCTION__, __LINE__) #define ast_odbc_request_obj(a, b) _ast_odbc_request_obj(a, b, __FILE__, __PRETTY_FUNCTION__, __LINE__) -/*! - * \brief Retrieve a stored ODBC object, if a transaction has been started. - * \param chan Channel associated with the transaction. - * \param objname Name of the database handle. This name corresponds to the name passed - * to \see ast_odbc_request_obj2 (or formerly, to ast_odbc_request_obj). Note that the - * existence of this parameter name explicitly allows for multiple transactions to be open - * at once, albeit to different databases. - * \retval A stored ODBC object, if a transaction was already started. - * \retval NULL, if no transaction yet exists. - */ -struct odbc_obj *ast_odbc_retrieve_transaction_obj(struct ast_channel *chan, const char *objname); - /*! * \brief Releases an ODBC object previously allocated by ast_odbc_request_obj() * \param obj The ODBC object @@ -223,4 +208,39 @@ int ast_odbc_clear_cache(const char *database, const char *tablename); */ SQLRETURN ast_odbc_ast_str_SQLGetData(struct ast_str **buf, int pmaxlen, SQLHSTMT StatementHandle, SQLUSMALLINT ColumnNumber, SQLSMALLINT TargetType, SQLLEN *StrLen_or_Ind); +/*! + * \brief Shortcut for printing errors to logs after a failed SQL operation. + * + * \param handle_type The type of SQL handle on which to gather diagnostics + * \param handle The SQL handle to gather diagnostics from + * \param operation The name of the failed operation. + * \return The error string that was printed to the logs + */ +struct ast_str *ast_odbc_print_errors(SQLSMALLINT handle_type, SQLHANDLE handle, const char *operation); + +/*! + * \brief Get the transaction isolation setting for an ODBC class + */ +unsigned int ast_odbc_class_get_isolation(struct odbc_class *class); + +/*! + * \brief Get the transaction forcecommit setting for an ODBC class + */ +unsigned int ast_odbc_class_get_forcecommit(struct odbc_class *class); + +/*! + * \brief Get the name of an ODBC class. + */ +const char *ast_odbc_class_get_name(struct odbc_class *class); + +/*! + * \brief Convert from textual transaction isolation values to their numeric constants + */ +int ast_odbc_text2isolation(const char *txt); + +/*! + * \brief Convert from numeric transaction isolation values to their textual counterparts + */ +const char *ast_odbc_isolation2text(int iso); + #endif /* _ASTERISK_RES_ODBC_H */ diff --git a/include/asterisk/res_odbc_transaction.h b/include/asterisk/res_odbc_transaction.h new file mode 100644 index 0000000000..b0f9316a1c --- /dev/null +++ b/include/asterisk/res_odbc_transaction.h @@ -0,0 +1,54 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2016, Digium, Inc. + * + * Mark Michelson + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#ifndef RES_ODBC_TRANSACTION_H +#define RES_ODBC_TRANSACTION_H + +/*! + * \brief + * + * Retrieve an ODBC transaction connection with the given ODBC class name. + * + * \note The name passed here is *not* the name of the transaction but the name of the + * ODBC class defined in res_odbc.conf. + * + * \note Do not call ast_odbc_release_obj() on the retrieved connection. Calling this function + * does not make you the owner of the connection. + * + * XXX This function is majorly flawed because it ignores properties of transactions and simply + * finds one that corresponds to the given DSN. The problem here is that transactions have names + * and they maintain which transaction is "active" for operations like transaction creation, + * commit, and rollback. However, when it comes to intermediary operations to be made on the + * transactions, all that is ignored. It means that if a channel has created multiple transactions + * for the same DSN, it's a crapshoot which of those transactions the operation will be performed + * on. This can potentially lead to baffling errors under the right circumstances. + * + * XXX The semantics of this function make for writing some awkward code. If you use func_odbc as + * an example, it has to first try to retrieve a transactional connection, then failing that, create + * a non-transactional connection. The result is that it has to remember which type of connection it's + * using and know whether to release the connection when completed or not. It would be much better + * if callers did not have to jump through such hoops. + * + * \param chan Channel on which the ODBC transaction was created + * \param objname The name of the ODBC class configured in res_odbc.conf + * \retval NULL Transaction connection could not be found. + * \retval non-NULL A transactional connection + */ +struct odbc_obj *ast_odbc_retrieve_transaction_obj(struct ast_channel *chan, const char *objname); + +#endif /* RES_ODBC_TRANSACTION_H */ diff --git a/res/res_odbc.c b/res/res_odbc.c index 06009a2a5e..609d68a4cf 100644 --- a/res/res_odbc.c +++ b/res/res_odbc.c @@ -64,66 +64,6 @@ ASTERISK_REGISTER_FILE() #include "asterisk/threadstorage.h" #include "asterisk/data.h" -/*** DOCUMENTATION - - - Controls ODBC transaction properties. - - - - - - Gets or sets the active transaction ID. If set, and the transaction ID does not - exist and a database name is specified as an argument, it will be created. - - - Controls whether a transaction will be automatically committed when the channel - hangs up. Defaults to false. If a transaction ID is specified in the optional argument, - the property will be applied to that ID, otherwise to the current active ID. - - - Controls the data isolation on uncommitted transactions. May be one of the - following: read_committed, read_uncommitted, - repeatable_read, or serializable. Defaults to the - database setting in res_odbc.conf or read_committed - if not specified. If a transaction ID is specified as an optional argument, it will be - applied to that ID, otherwise the current active ID. - - - - - - - The ODBC() function allows setting several properties to influence how a connected - database processes transactions. - - - - - Commits a currently open database transaction. - - - - - - Commits the database transaction specified by transaction ID - or the current active transaction, if not specified. - - - - - Rollback a currently open database transaction. - - - - - - Rolls back the database transaction specified by transaction ID - or the current active transaction, if not specified. - - - ***/ - struct odbc_class { AST_LIST_ENTRY(odbc_class) list; @@ -133,21 +73,15 @@ struct odbc_class char *password; char *sanitysql; SQLHENV env; - unsigned int haspool:1; /*!< Boolean - TDS databases need this */ unsigned int delme:1; /*!< Purge the class */ unsigned int backslash_is_escape:1; /*!< On this database, the backslash is a native escape sequence */ unsigned int forcecommit:1; /*!< Should uncommitted transactions be auto-committed on handle release? */ unsigned int isolation; /*!< Flags for how the DB should deal with data in other, uncommitted transactions */ - unsigned int limit; /*!< Maximum number of database handles we will allow */ - int count; /*!< Running count of pooled connections */ - unsigned int idlecheck; /*!< Recheck the connection if it is idle for this long (in seconds) */ unsigned int conntimeout; /*!< Maximum time the connection process should take */ /*! When a connection fails, cache that failure for how long? */ struct timeval negative_connection_cache; /*! When a connection fails, when did that last occur? */ struct timeval last_negative_connect; - /*! List of handles associated with this class */ - struct ao2_container *obj_container; }; static struct ao2_container *class_container; @@ -157,16 +91,9 @@ static AST_RWLIST_HEAD_STATIC(odbc_tables, odbc_cache_tables); static odbc_status odbc_obj_connect(struct odbc_obj *obj); static odbc_status odbc_obj_disconnect(struct odbc_obj *obj); static int odbc_register_class(struct odbc_class *class, int connect); -static void odbc_txn_free(void *data); -static void odbc_release_obj2(struct odbc_obj *obj, struct odbc_txn_frame *tx); AST_THREADSTORAGE(errors_buf); -static const struct ast_datastore_info txn_info = { - .type = "ODBC_Transaction", - .destroy = odbc_txn_free, -}; - struct odbc_txn_frame { AST_LIST_ENTRY(odbc_txn_frame) list; struct ast_channel *owner; @@ -189,13 +116,11 @@ struct odbc_txn_frame { MEMBER(odbc_class, dsn, AST_DATA_STRING) \ MEMBER(odbc_class, username, AST_DATA_STRING) \ MEMBER(odbc_class, password, AST_DATA_PASSWORD) \ - MEMBER(odbc_class, limit, AST_DATA_INTEGER) \ - MEMBER(odbc_class, count, AST_DATA_INTEGER) \ MEMBER(odbc_class, forcecommit, AST_DATA_BOOLEAN) AST_DATA_STRUCTURE(odbc_class, DATA_EXPORT_ODBC_CLASS); -static const char *isolation2text(int iso) +const char *ast_odbc_isolation2text(int iso) { if (iso == SQL_TXN_READ_COMMITTED) { return "read_committed"; @@ -210,7 +135,7 @@ static const char *isolation2text(int iso) } } -static int text2isolation(const char *txt) +int ast_odbc_text2isolation(const char *txt) { if (strncasecmp(txt, "read_", 5) == 0) { if (strncasecmp(txt + 5, "c", 1) == 0) { @@ -229,176 +154,6 @@ static int text2isolation(const char *txt) } } -static struct odbc_txn_frame *find_transaction(struct ast_channel *chan, struct odbc_obj *obj, const char *name, int active) -{ - struct ast_datastore *txn_store; - AST_LIST_HEAD(, odbc_txn_frame) *oldlist; - struct odbc_txn_frame *txn = NULL; - - if (!chan && obj && obj->txf && obj->txf->owner) { - chan = obj->txf->owner; - } else if (!chan) { - /* No channel == no transaction */ - return NULL; - } - - ast_channel_lock(chan); - if ((txn_store = ast_channel_datastore_find(chan, &txn_info, NULL))) { - oldlist = txn_store->data; - } else { - /* Need to create a new datastore */ - if (!(txn_store = ast_datastore_alloc(&txn_info, NULL))) { - ast_log(LOG_ERROR, "Unable to allocate a new datastore. Cannot create a new transaction.\n"); - ast_channel_unlock(chan); - return NULL; - } - - if (!(oldlist = ast_calloc(1, sizeof(*oldlist)))) { - ast_log(LOG_ERROR, "Unable to allocate datastore list head. Cannot create a new transaction.\n"); - ast_datastore_free(txn_store); - ast_channel_unlock(chan); - return NULL; - } - - txn_store->data = oldlist; - AST_LIST_HEAD_INIT(oldlist); - ast_channel_datastore_add(chan, txn_store); - } - - AST_LIST_LOCK(oldlist); - ast_channel_unlock(chan); - - /* Scanning for an object is *fast*. Scanning for a name is much slower. */ - if (obj != NULL || active == 1) { - AST_LIST_TRAVERSE(oldlist, txn, list) { - if (txn->obj == obj || txn->active) { - AST_LIST_UNLOCK(oldlist); - return txn; - } - } - } - - if (name != NULL) { - AST_LIST_TRAVERSE(oldlist, txn, list) { - if (!strcasecmp(txn->name, name)) { - AST_LIST_UNLOCK(oldlist); - return txn; - } - } - } - - /* Nothing found, create one */ - if (name && obj && (txn = ast_calloc(1, sizeof(*txn) + strlen(name) + 1))) { - struct odbc_txn_frame *otxn; - - strcpy(txn->name, name); /* SAFE */ - txn->obj = obj; - txn->isolation = obj->parent->isolation; - txn->forcecommit = obj->parent->forcecommit; - txn->owner = chan; - txn->active = 1; - - /* On creation, the txn becomes active, and all others inactive */ - AST_LIST_TRAVERSE(oldlist, otxn, list) { - otxn->active = 0; - } - AST_LIST_INSERT_TAIL(oldlist, txn, list); - - obj->txf = txn; - obj->tx = 1; - } - AST_LIST_UNLOCK(oldlist); - - return txn; -} - -static struct odbc_txn_frame *release_transaction(struct odbc_txn_frame *tx) -{ - if (!tx) { - return NULL; - } - - ast_debug(2, "release_transaction(%p) called (tx->obj = %p, tx->obj->txf = %p)\n", tx, tx->obj, tx->obj ? tx->obj->txf : NULL); - - /* If we have an owner, disassociate */ - if (tx->owner) { - struct ast_datastore *txn_store; - AST_LIST_HEAD(, odbc_txn_frame) *oldlist; - - ast_channel_lock(tx->owner); - if ((txn_store = ast_channel_datastore_find(tx->owner, &txn_info, NULL))) { - oldlist = txn_store->data; - AST_LIST_LOCK(oldlist); - AST_LIST_REMOVE(oldlist, tx, list); - AST_LIST_UNLOCK(oldlist); - } - ast_channel_unlock(tx->owner); - tx->owner = NULL; - } - - if (tx->obj) { - /* If we have any uncommitted transactions, they are handled when we release the object */ - struct odbc_obj *obj = tx->obj; - /* Prevent recursion during destruction */ - tx->obj->txf = NULL; - tx->obj = NULL; - odbc_release_obj2(obj, tx); - } - ast_free(tx); - return NULL; -} - -static void odbc_txn_free(void *vdata) -{ - struct odbc_txn_frame *tx; - AST_LIST_HEAD(, odbc_txn_frame) *oldlist = vdata; - - ast_debug(2, "odbc_txn_free(%p) called\n", vdata); - - AST_LIST_LOCK(oldlist); - while ((tx = AST_LIST_REMOVE_HEAD(oldlist, list))) { - release_transaction(tx); - } - AST_LIST_UNLOCK(oldlist); - AST_LIST_HEAD_DESTROY(oldlist); - ast_free(oldlist); -} - -static int mark_transaction_active(struct ast_channel *chan, struct odbc_txn_frame *tx) -{ - struct ast_datastore *txn_store; - AST_LIST_HEAD(, odbc_txn_frame) *oldlist; - struct odbc_txn_frame *active = NULL, *txn; - - if (!chan && tx && tx->owner) { - chan = tx->owner; - } - - if (!chan) { - return -1; - } - - ast_channel_lock(chan); - if (!(txn_store = ast_channel_datastore_find(chan, &txn_info, NULL))) { - ast_channel_unlock(chan); - return -1; - } - - oldlist = txn_store->data; - AST_LIST_LOCK(oldlist); - AST_LIST_TRAVERSE(oldlist, txn, list) { - if (txn == tx) { - txn->active = 1; - active = txn; - } else { - txn->active = 0; - } - } - AST_LIST_UNLOCK(oldlist); - ast_channel_unlock(chan); - return active ? 0 : -1; -} - static void odbc_class_destructor(void *data) { struct odbc_class *class = data; @@ -414,7 +169,6 @@ static void odbc_class_destructor(void *data) if (class->sanitysql) { ast_free(class->sanitysql); } - ao2_ref(class->obj_container, -1); SQLFreeHandle(SQL_HANDLE_ENV, class->env); } @@ -451,6 +205,21 @@ static void destroy_table_cache(struct odbc_cache_tables *table) { * \retval A structure describing the table layout, or NULL, if the table is not found or another error occurs. * When a structure is returned, the contained columns list will be * rdlock'ed, to ensure that it will be retained in memory. + * + * XXX This creates a connection and disconnects it. In some situations, the caller of + * this function has its own connection and could donate it to this function instead of + * needing to create another one. + * + * XXX The automatic readlock of the columns is awkward. It's done because it's possible for + * multiple threads to have references to the table, and the table is not refcounted. Possible + * changes here would be + * * Eliminate the table cache entirely. The use of ast_odbc_find_table() is generally + * questionable. The only real good use right now is from ast_realtime_require_field() in + * order to make sure the DB has the expected columns in it. Since that is only used sparingly, + * the need to cache tables is questionable. Instead, the table structure can be fetched from + * the DB directly each time, resulting in a single owner of the data. + * * Make odbc_cache_tables a refcounted object. + * * \since 1.6.1 */ struct odbc_cache_tables *ast_odbc_find_table(const char *database, const char *tablename) @@ -460,7 +229,7 @@ struct odbc_cache_tables *ast_odbc_find_table(const char *database, const char * char columnname[80]; SQLLEN sqlptr; SQLHSTMT stmt = NULL; - int res = 0, error = 0, try = 0; + int res = 0, error = 0; struct odbc_obj *obj; AST_RWLIST_RDLOCK(&odbc_tables); @@ -482,27 +251,16 @@ struct odbc_cache_tables *ast_odbc_find_table(const char *database, const char * } /* Table structure not already cached; build it now. */ - ao2_lock(obj); do { res = SQLAllocHandle(SQL_HANDLE_STMT, obj->con, &stmt); if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) { - if (try == 0) { - try = 1; - ast_odbc_sanity_check(obj); - continue; - } ast_log(LOG_WARNING, "SQL Alloc Handle failed on connection '%s'!\n", database); break; } res = SQLColumns(stmt, NULL, 0, NULL, 0, (unsigned char *)tablename, SQL_NTS, (unsigned char *)"%", SQL_NTS); if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) { - if (try == 0) { - try = 1; - SQLFreeHandle(SQL_HANDLE_STMT, stmt); - ast_odbc_sanity_check(obj); - continue; - } + SQLFreeHandle(SQL_HANDLE_STMT, stmt); ast_log(LOG_ERROR, "Unable to query database columns on connection '%s'.\n", database); break; } @@ -553,7 +311,6 @@ struct odbc_cache_tables *ast_odbc_find_table(const char *database, const char * AST_RWLIST_RDLOCK(&(tableptr->columns)); break; } while (1); - ao2_unlock(obj); AST_RWLIST_UNLOCK(&odbc_tables); @@ -595,125 +352,52 @@ int ast_odbc_clear_cache(const char *database, const char *tablename) SQLHSTMT ast_odbc_direct_execute(struct odbc_obj *obj, SQLHSTMT (*exec_cb)(struct odbc_obj *obj, void *data), void *data) { - int attempt; SQLHSTMT stmt; - ao2_lock(obj); - - for (attempt = 0; attempt < 2; attempt++) { - stmt = exec_cb(obj, data); - - if (stmt) { - break; - } else if (obj->tx) { - ast_log(LOG_WARNING, "Failed to execute, but unable to reconnect, as we're transactional.\n"); - break; - } else if (attempt == 0) { - ast_log(LOG_WARNING, "SQL Execute error! Verifying connection to %s [%s]...\n", obj->parent->name, obj->parent->dsn); - } - if (!ast_odbc_sanity_check(obj)) { - break; - } - } - - ao2_unlock(obj); + stmt = exec_cb(obj, data); return stmt; } SQLHSTMT ast_odbc_prepare_and_execute(struct odbc_obj *obj, SQLHSTMT (*prepare_cb)(struct odbc_obj *obj, void *data), void *data) { - int res = 0, i, attempt; - SQLINTEGER nativeerror=0, numfields=0; - SQLSMALLINT diagbytes=0; - unsigned char state[10], diagnostic[256]; + int res = 0; SQLHSTMT stmt; - ao2_lock(obj); - - for (attempt = 0; attempt < 2; attempt++) { - /* This prepare callback may do more than just prepare -- it may also - * bind parameters, bind results, etc. The real key, here, is that - * when we disconnect, all handles become invalid for most databases. - * We must therefore redo everything when we establish a new - * connection. */ - stmt = prepare_cb(obj, data); - - if (stmt) { - res = SQLExecute(stmt); - if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO) && (res != SQL_NO_DATA)) { - if (res == SQL_ERROR) { - SQLGetDiagField(SQL_HANDLE_STMT, stmt, 1, SQL_DIAG_NUMBER, &numfields, SQL_IS_INTEGER, &diagbytes); - for (i = 0; i < numfields; i++) { - SQLGetDiagRec(SQL_HANDLE_STMT, stmt, i + 1, state, &nativeerror, diagnostic, sizeof(diagnostic), &diagbytes); - ast_log(LOG_WARNING, "SQL Execute returned an error %d: %s: %s (%d)\n", res, state, diagnostic, diagbytes); - if (i > 10) { - ast_log(LOG_WARNING, "Oh, that was good. There are really %d diagnostics?\n", (int)numfields); - break; - } - } - } + /* This prepare callback may do more than just prepare -- it may also + * bind parameters, bind results, etc. The real key, here, is that + * when we disconnect, all handles become invalid for most databases. + * We must therefore redo everything when we establish a new + * connection. */ + stmt = prepare_cb(obj, data); - if (obj->tx) { - ast_log(LOG_WARNING, "SQL Execute error, but unable to reconnect, as we're transactional.\n"); - break; - } else { - ast_log(LOG_WARNING, "SQL Execute error %d! Verifying connection to %s [%s]...\n", res, obj->parent->name, obj->parent->dsn); - SQLFreeHandle(SQL_HANDLE_STMT, stmt); - stmt = NULL; - - obj->up = 0; - /* - * While this isn't the best way to try to correct an error, this won't automatically - * fail when the statement handle invalidates. - */ - if (!ast_odbc_sanity_check(obj)) { - break; - } - continue; - } - } else { - obj->last_used = ast_tvnow(); + if (stmt) { + res = SQLExecute(stmt); + if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO) && (res != SQL_NO_DATA)) { + if (res == SQL_ERROR) { + ast_odbc_print_errors(SQL_HANDLE_STMT, stmt, "SQL Execute"); } - break; - } else if (attempt == 0) { - ast_odbc_sanity_check(obj); + + ast_log(LOG_WARNING, "SQL Execute error %d!\n", res); + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + stmt = NULL; } } - ao2_unlock(obj); - return stmt; } int ast_odbc_smart_execute(struct odbc_obj *obj, SQLHSTMT stmt) { - int res = 0, i; - SQLINTEGER nativeerror=0, numfields=0; - SQLSMALLINT diagbytes=0; - unsigned char state[10], diagnostic[256]; - - ao2_lock(obj); + int res = 0; res = SQLExecute(stmt); if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO) && (res != SQL_NO_DATA)) { if (res == SQL_ERROR) { - SQLGetDiagField(SQL_HANDLE_STMT, stmt, 1, SQL_DIAG_NUMBER, &numfields, SQL_IS_INTEGER, &diagbytes); - for (i = 0; i < numfields; i++) { - SQLGetDiagRec(SQL_HANDLE_STMT, stmt, i + 1, state, &nativeerror, diagnostic, sizeof(diagnostic), &diagbytes); - ast_log(LOG_WARNING, "SQL Execute returned an error %d: %s: %s (%d)\n", res, state, diagnostic, diagbytes); - if (i > 10) { - ast_log(LOG_WARNING, "Oh, that was good. There are really %d diagnostics?\n", (int)numfields); - break; - } - } + ast_odbc_print_errors(SQL_HANDLE_STMT, stmt, "SQL Execute"); } - } else { - obj->last_used = ast_tvnow(); } - ao2_unlock(obj); - return res; } @@ -734,39 +418,47 @@ SQLRETURN ast_odbc_ast_str_SQLGetData(struct ast_str **buf, int pmaxlen, SQLHSTM return res; } -int ast_odbc_sanity_check(struct odbc_obj *obj) -{ - char *test_sql = "select 1"; - SQLHSTMT stmt; - int res = 0; - - if (!ast_strlen_zero(obj->parent->sanitysql)) - test_sql = obj->parent->sanitysql; - - if (obj->up) { - res = SQLAllocHandle(SQL_HANDLE_STMT, obj->con, &stmt); - if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) { - obj->up = 0; - } else { - res = SQLPrepare(stmt, (unsigned char *)test_sql, SQL_NTS); - if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) { - obj->up = 0; - } else { - res = SQLExecute(stmt); - if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) { - obj->up = 0; - } - } +struct ast_str *ast_odbc_print_errors(SQLSMALLINT handle_type, SQLHANDLE handle, const char *operation) +{ + struct ast_str *errors = ast_str_thread_get(&errors_buf, 16); + SQLINTEGER nativeerror = 0; + SQLINTEGER numfields = 0; + SQLSMALLINT diagbytes = 0; + SQLSMALLINT i; + unsigned char state[10]; + unsigned char diagnostic[256]; + + ast_str_reset(errors); + SQLGetDiagField(handle_type, handle, 1, SQL_DIAG_NUMBER, &numfields, + SQL_IS_INTEGER, &diagbytes); + for (i = 0; i < numfields; i++) { + SQLGetDiagRec(handle_type, handle, i + 1, state, &nativeerror, + diagnostic, sizeof(diagnostic), &diagbytes); + ast_str_append(&errors, 0, "%s%s", ast_str_strlen(errors) ? "," : "", state); + ast_log(LOG_WARNING, "%s returned an error: %s: %s\n", operation, state, diagnostic); + /* XXX Why is this here? */ + if (i > 10) { + ast_log(LOG_WARNING, "Oh, that was good. There are really %d diagnostics?\n", (int)numfields); + break; } - SQLFreeHandle (SQL_HANDLE_STMT, stmt); } - if (!obj->up && !obj->tx) { /* Try to reconnect! */ - ast_log(LOG_WARNING, "Connection is down attempting to reconnect...\n"); - odbc_obj_disconnect(obj); - odbc_obj_connect(obj); - } - return obj->up; + return errors; +} + +unsigned int ast_odbc_class_get_isolation(struct odbc_class *class) +{ + return class->isolation; +} + +unsigned int ast_odbc_class_get_forcecommit(struct odbc_class *class) +{ + return class->forcecommit; +} + +const char *ast_odbc_class_get_name(struct odbc_class *class) +{ + return class->name; } static int load_odbc_config(void) @@ -776,9 +468,8 @@ static int load_odbc_config(void) struct ast_variable *v; char *cat; const char *dsn, *username, *password, *sanitysql; - int enabled, pooling, limit, bse, conntimeout, forcecommit, isolation; + int enabled, bse, conntimeout, forcecommit, isolation; struct timeval ncache = { 0, 0 }; - unsigned int idlecheck; int preconnect = 0, res = 0; struct ast_flags config_flags = { 0 }; @@ -799,33 +490,17 @@ static int load_odbc_config(void) /* Reset all to defaults for each class of odbc connections */ dsn = username = password = sanitysql = NULL; enabled = 1; - preconnect = idlecheck = 0; - pooling = 0; - limit = 0; + preconnect = 0; bse = 1; conntimeout = 10; forcecommit = 0; isolation = SQL_TXN_READ_COMMITTED; for (v = ast_variable_browse(config, cat); v; v = v->next) { - if (!strcasecmp(v->name, "pooling")) { - if (ast_true(v->value)) - pooling = 1; - } else if (!strncasecmp(v->name, "share", 5)) { - /* "shareconnections" is a little clearer in meaning than "pooling" */ - if (ast_false(v->value)) - pooling = 1; - } else if (!strcasecmp(v->name, "limit")) { - sscanf(v->value, "%30d", &limit); - if (ast_true(v->value) && !limit) { - ast_log(LOG_WARNING, "Limit should be a number, not a boolean: '%s'. Setting limit to 1023 for ODBC class '%s'.\n", v->value, cat); - limit = 1023; - } else if (ast_false(v->value)) { - ast_log(LOG_WARNING, "Limit should be a number, not a boolean: '%s'. Disabling ODBC class '%s'.\n", v->value, cat); - enabled = 0; - break; - } - } else if (!strcasecmp(v->name, "idlecheck")) { - sscanf(v->value, "%30u", &idlecheck); + if (!strcasecmp(v->name, "pooling") || + !strncasecmp(v->name, "share", 5) || + !strcasecmp(v->name, "limit") || + !strcasecmp(v->name, "idlecheck")) { + ast_log(LOG_WARNING, "The 'pooling', 'shared_connections', 'limit', and 'idlecheck' options are deprecated. Please see UPGRADE.txt for information"); } else if (!strcasecmp(v->name, "enabled")) { enabled = ast_true(v->value); } else if (!strcasecmp(v->name, "pre-connect")) { @@ -859,7 +534,7 @@ static int load_odbc_config(void) } else if (!strcasecmp(v->name, "forcecommit")) { forcecommit = ast_true(v->value); } else if (!strcasecmp(v->name, "isolation")) { - if ((isolation = text2isolation(v->value)) == 0) { + if ((isolation = ast_odbc_text2isolation(v->value)) == 0) { ast_log(LOG_ERROR, "Unrecognized value for 'isolation': '%s' in section '%s'\n", v->value, cat); isolation = SQL_TXN_READ_COMMITTED; } @@ -883,22 +558,9 @@ static int load_odbc_config(void) return res; } - new->obj_container = ao2_container_alloc(1, null_hash_fn, ao2_match_by_addr); - - if (pooling) { - new->haspool = pooling; - if (limit) { - new->limit = limit; - } else { - ast_log(LOG_WARNING, "Pooling without also setting a limit is pointless. Changing limit from 0 to 5.\n"); - new->limit = 5; - } - } - new->backslash_is_escape = bse ? 1 : 0; new->forcecommit = forcecommit ? 1 : 0; new->isolation = isolation; - new->idlecheck = idlecheck; new->conntimeout = conntimeout; new->negative_connection_cache = ncache; @@ -934,7 +596,6 @@ static char *handle_cli_odbc_show(struct ast_cli_entry *e, int cmd, struct ast_c { struct ao2_iterator aoi; struct odbc_class *class; - struct odbc_obj *current; int length = 0; int which = 0; char *ret = NULL; @@ -973,7 +634,6 @@ static char *handle_cli_odbc_show(struct ast_cli_entry *e, int cmd, struct ast_c aoi = ao2_iterator_init(class_container, 0); while ((class = ao2_iterator_next(&aoi))) { if ((a->argc == 2) || (a->argc == 3 && !strcmp(a->argv[2], "all")) || (!strcmp(a->argv[2], class->name))) { - int count = 0; char timestr[80]; struct ast_tm tm; @@ -981,38 +641,6 @@ static char *handle_cli_odbc_show(struct ast_cli_entry *e, int cmd, struct ast_c ast_strftime(timestr, sizeof(timestr), "%Y-%m-%d %T", &tm); ast_cli(a->fd, " Name: %s\n DSN: %s\n", class->name, class->dsn); ast_cli(a->fd, " Last connection attempt: %s\n", timestr); - - if (class->haspool) { - struct ao2_iterator aoi2 = ao2_iterator_init(class->obj_container, 0); - - ast_cli(a->fd, " Pooled: Yes\n Limit: %u\n Connections in use: %d\n", class->limit, class->count); - - while ((current = ao2_iterator_next(&aoi2))) { - ao2_lock(current); -#ifdef DEBUG_THREADS - ast_cli(a->fd, " - Connection %d: %s (%s:%d %s)\n", ++count, - current->used ? "in use" : - current->up && ast_odbc_sanity_check(current) ? "connected" : "disconnected", - current->file, current->lineno, current->function); -#else - ast_cli(a->fd, " - Connection %d: %s\n", ++count, - current->used ? "in use" : - current->up && ast_odbc_sanity_check(current) ? "connected" : "disconnected"); -#endif - ao2_unlock(current); - ao2_ref(current, -1); - } - ao2_iterator_destroy(&aoi2); - } else { - /* Should only ever be one of these (unless there are transactions) */ - struct ao2_iterator aoi2 = ao2_iterator_init(class->obj_container, 0); - while ((current = ao2_iterator_next(&aoi2))) { - ast_cli(a->fd, " Pooled: No\n Connected: %s\n", current->used ? "In use" : - current->up && ast_odbc_sanity_check(current) ? "Yes" : "No"); - ao2_ref(current, -1); - } - ao2_iterator_destroy(&aoi2); - } ast_cli(a->fd, "\n"); } ao2_ref(class, -1); @@ -1048,150 +676,23 @@ static int odbc_register_class(struct odbc_class *class, int preconnect) } } -static void odbc_release_obj2(struct odbc_obj *obj, struct odbc_txn_frame *tx) +void ast_odbc_release_obj(struct odbc_obj *obj) { - SQLINTEGER nativeerror=0, numfields=0; - SQLSMALLINT diagbytes=0, i; - unsigned char state[10], diagnostic[256]; - - ast_debug(2, "odbc_release_obj2(%p) called (obj->txf = %p)\n", obj, obj->txf); - if (tx) { - ast_debug(1, "called on a transactional handle with %s\n", tx->forcecommit ? "COMMIT" : "ROLLBACK"); - if (SQLEndTran(SQL_HANDLE_DBC, obj->con, tx->forcecommit ? SQL_COMMIT : SQL_ROLLBACK) == SQL_ERROR) { - /* Handle possible transaction commit failure */ - SQLGetDiagField(SQL_HANDLE_DBC, obj->con, 1, SQL_DIAG_NUMBER, &numfields, SQL_IS_INTEGER, &diagbytes); - for (i = 0; i < numfields; i++) { - SQLGetDiagRec(SQL_HANDLE_DBC, obj->con, i + 1, state, &nativeerror, diagnostic, sizeof(diagnostic), &diagbytes); - ast_log(LOG_WARNING, "SQLEndTran returned an error: %s: %s\n", state, diagnostic); - if (!strcmp((char *)state, "25S02") || !strcmp((char *)state, "08007")) { - /* These codes mean that a commit failed and a transaction - * is still active. We must rollback, or things will get - * very, very weird for anybody using the handle next. */ - SQLEndTran(SQL_HANDLE_DBC, obj->con, SQL_ROLLBACK); - } - if (i > 10) { - ast_log(LOG_WARNING, "Oh, that was good. There are really %d diagnostics?\n", (int)numfields); - break; - } - } - } - - /* Transaction is done, reset autocommit */ - if (SQLSetConnectAttr(obj->con, SQL_ATTR_AUTOCOMMIT, (void *)SQL_AUTOCOMMIT_ON, 0) == SQL_ERROR) { - SQLGetDiagField(SQL_HANDLE_DBC, obj->con, 1, SQL_DIAG_NUMBER, &numfields, SQL_IS_INTEGER, &diagbytes); - for (i = 0; i < numfields; i++) { - SQLGetDiagRec(SQL_HANDLE_DBC, obj->con, i + 1, state, &nativeerror, diagnostic, sizeof(diagnostic), &diagbytes); - ast_log(LOG_WARNING, "SetConnectAttr (Autocommit) returned an error: %s: %s\n", state, diagnostic); - if (i > 10) { - ast_log(LOG_WARNING, "Oh, that was good. There are really %d diagnostics?\n", (int)numfields); - break; - } - } - } - } + ast_debug(2, "Releasing ODBC handle %p\n", obj); #ifdef DEBUG_THREADS obj->file[0] = '\0'; obj->function[0] = '\0'; obj->lineno = 0; #endif - - /* For pooled connections, this frees the connection to be - * reused. For non-pooled connections, it does nothing. */ - obj->used = 0; - if (obj->txf) { - /* Prevent recursion -- transaction is already closed out. */ - obj->txf->obj = NULL; - obj->txf = release_transaction(obj->txf); - } ao2_ref(obj, -1); } -void ast_odbc_release_obj(struct odbc_obj *obj) -{ - struct odbc_txn_frame *tx = find_transaction(NULL, obj, NULL, 0); - odbc_release_obj2(obj, tx); -} - int ast_odbc_backslash_is_escape(struct odbc_obj *obj) { return obj->parent->backslash_is_escape; } -static int commit_exec(struct ast_channel *chan, const char *data) -{ - struct odbc_txn_frame *tx; - SQLINTEGER nativeerror=0, numfields=0; - SQLSMALLINT diagbytes=0, i; - unsigned char state[10], diagnostic[256]; - - if (ast_strlen_zero(data)) { - tx = find_transaction(chan, NULL, NULL, 1); - } else { - tx = find_transaction(chan, NULL, data, 0); - } - - pbx_builtin_setvar_helper(chan, "COMMIT_RESULT", "OK"); - - if (tx) { - if (SQLEndTran(SQL_HANDLE_DBC, tx->obj->con, SQL_COMMIT) == SQL_ERROR) { - struct ast_str *errors = ast_str_thread_get(&errors_buf, 16); - ast_str_reset(errors); - - /* Handle possible transaction commit failure */ - SQLGetDiagField(SQL_HANDLE_DBC, tx->obj->con, 1, SQL_DIAG_NUMBER, &numfields, SQL_IS_INTEGER, &diagbytes); - for (i = 0; i < numfields; i++) { - SQLGetDiagRec(SQL_HANDLE_DBC, tx->obj->con, i + 1, state, &nativeerror, diagnostic, sizeof(diagnostic), &diagbytes); - ast_str_append(&errors, 0, "%s%s", ast_str_strlen(errors) ? "," : "", state); - ast_log(LOG_WARNING, "SQLEndTran returned an error: %s: %s\n", state, diagnostic); - if (i > 10) { - ast_log(LOG_WARNING, "Oh, that was good. There are really %d diagnostics?\n", (int)numfields); - break; - } - } - pbx_builtin_setvar_helper(chan, "COMMIT_RESULT", ast_str_buffer(errors)); - } - } - return 0; -} - -static int rollback_exec(struct ast_channel *chan, const char *data) -{ - struct odbc_txn_frame *tx; - SQLINTEGER nativeerror=0, numfields=0; - SQLSMALLINT diagbytes=0, i; - unsigned char state[10], diagnostic[256]; - - if (ast_strlen_zero(data)) { - tx = find_transaction(chan, NULL, NULL, 1); - } else { - tx = find_transaction(chan, NULL, data, 0); - } - - pbx_builtin_setvar_helper(chan, "ROLLBACK_RESULT", "OK"); - - if (tx) { - if (SQLEndTran(SQL_HANDLE_DBC, tx->obj->con, SQL_ROLLBACK) == SQL_ERROR) { - struct ast_str *errors = ast_str_thread_get(&errors_buf, 16); - ast_str_reset(errors); - - /* Handle possible transaction commit failure */ - SQLGetDiagField(SQL_HANDLE_DBC, tx->obj->con, 1, SQL_DIAG_NUMBER, &numfields, SQL_IS_INTEGER, &diagbytes); - for (i = 0; i < numfields; i++) { - SQLGetDiagRec(SQL_HANDLE_DBC, tx->obj->con, i + 1, state, &nativeerror, diagnostic, sizeof(diagnostic), &diagbytes); - ast_str_append(&errors, 0, "%s%s", ast_str_strlen(errors) ? "," : "", state); - ast_log(LOG_WARNING, "SQLEndTran returned an error: %s: %s\n", state, diagnostic); - if (i > 10) { - ast_log(LOG_WARNING, "Oh, that was good. There are really %d diagnostics?\n", (int)numfields); - break; - } - } - pbx_builtin_setvar_helper(chan, "ROLLBACK_RESULT", ast_str_buffer(errors)); - } - } - return 0; -} - static int aoro2_class_cb(void *obj, void *arg, int flags) { struct odbc_class *class = obj; @@ -1202,275 +703,41 @@ static int aoro2_class_cb(void *obj, void *arg, int flags) return 0; } -#define USE_TX (void *)(long)1 -#define NO_TX (void *)(long)2 -#define EOR_TX (void *)(long)3 - -static int aoro2_obj_cb(void *vobj, void *arg, int flags) -{ - struct odbc_obj *obj = vobj; - ao2_lock(obj); - if ((arg == NO_TX && !obj->tx) || (arg == EOR_TX && !obj->used) || (arg == USE_TX && obj->tx && !obj->used)) { - obj->used = 1; - ao2_unlock(obj); - return CMP_MATCH | CMP_STOP; - } - ao2_unlock(obj); - return 0; -} - -/* This function should only be called for shared connections. Otherwise, the lack of - * setting vobj->used breaks EOR_TX searching. For nonshared connections, use - * aoro2_obj_cb instead. */ -static int aoro2_obj_notx_cb(void *vobj, void *arg, int flags) -{ - struct odbc_obj *obj = vobj; - if (!obj->tx) { - return CMP_MATCH | CMP_STOP; - } - return 0; -} - struct odbc_obj *_ast_odbc_request_obj2(const char *name, struct ast_flags flags, const char *file, const char *function, int lineno) { struct odbc_obj *obj = NULL; struct odbc_class *class; - SQLINTEGER nativeerror=0, numfields=0; - SQLSMALLINT diagbytes=0, i; - unsigned char state[10], diagnostic[256]; if (!(class = ao2_callback(class_container, 0, aoro2_class_cb, (char *) name))) { ast_debug(1, "Class '%s' not found!\n", name); return NULL; } - ast_assert(ao2_ref(class, 0) > 1); - - if (class->haspool) { - /* Recycle connections before building another */ - obj = ao2_callback(class->obj_container, 0, aoro2_obj_cb, EOR_TX); - - if (obj) { - ast_assert(ao2_ref(obj, 0) > 1); - } - if (!obj && (ast_atomic_fetchadd_int(&class->count, +1) < class->limit)) { - obj = ao2_alloc(sizeof(*obj), odbc_obj_destructor); - if (!obj) { - class->count--; - ao2_ref(class, -1); - ast_debug(3, "Unable to allocate object\n"); - ast_atomic_fetchadd_int(&class->count, -1); - return NULL; - } - ast_assert(ao2_ref(obj, 0) == 1); - /* obj inherits the outstanding reference to class */ - obj->parent = class; - class = NULL; - if (odbc_obj_connect(obj) == ODBC_FAIL) { - ast_log(LOG_WARNING, "Failed to connect to %s\n", name); - ast_assert(ao2_ref(obj->parent, 0) > 0); - /* Because it was never within the container, we have to manually decrement the count here */ - ast_atomic_fetchadd_int(&obj->parent->count, -1); - ao2_ref(obj, -1); - obj = NULL; - } else { - obj->used = 1; - ao2_link(obj->parent->obj_container, obj); - } - } else { - /* If construction fails due to the limit (or negative timecache), reverse our increment. */ - if (!obj) { - ast_atomic_fetchadd_int(&class->count, -1); - } - /* Object is not constructed, so delete outstanding reference to class. */ - ao2_ref(class, -1); - class = NULL; - } - - if (!obj) { - return NULL; - } - - ao2_lock(obj); - - if (ast_test_flag(&flags, RES_ODBC_INDEPENDENT_CONNECTION)) { - /* Ensure this connection has autocommit turned off. */ - if (SQLSetConnectAttr(obj->con, SQL_ATTR_AUTOCOMMIT, (void *)SQL_AUTOCOMMIT_OFF, 0) == SQL_ERROR) { - SQLGetDiagField(SQL_HANDLE_DBC, obj->con, 1, SQL_DIAG_NUMBER, &numfields, SQL_IS_INTEGER, &diagbytes); - for (i = 0; i < numfields; i++) { - SQLGetDiagRec(SQL_HANDLE_DBC, obj->con, i + 1, state, &nativeerror, diagnostic, sizeof(diagnostic), &diagbytes); - ast_log(LOG_WARNING, "SQLSetConnectAttr (Autocommit) returned an error: %s: %s\n", state, diagnostic); - if (i > 10) { - ast_log(LOG_WARNING, "Oh, that was good. There are really %d diagnostics?\n", (int)numfields); - break; - } - } - } - } - } else if (ast_test_flag(&flags, RES_ODBC_INDEPENDENT_CONNECTION)) { - /* Non-pooled connections -- but must use a separate connection handle */ - if (!(obj = ao2_callback(class->obj_container, 0, aoro2_obj_cb, USE_TX))) { - ast_debug(1, "Object not found\n"); - obj = ao2_alloc(sizeof(*obj), odbc_obj_destructor); - if (!obj) { - ao2_ref(class, -1); - ast_debug(3, "Unable to allocate object\n"); - return NULL; - } - /* obj inherits the outstanding reference to class */ - obj->parent = class; - class = NULL; - if (odbc_obj_connect(obj) == ODBC_FAIL) { - ast_log(LOG_WARNING, "Failed to connect to %s\n", name); - ao2_ref(obj, -1); - obj = NULL; - } else { - obj->used = 1; - ao2_link(obj->parent->obj_container, obj); - ast_atomic_fetchadd_int(&obj->parent->count, +1); - } - } - - if (!obj) { - return NULL; - } - - ao2_lock(obj); - - if (SQLSetConnectAttr(obj->con, SQL_ATTR_AUTOCOMMIT, (void *)SQL_AUTOCOMMIT_OFF, 0) == SQL_ERROR) { - SQLGetDiagField(SQL_HANDLE_DBC, obj->con, 1, SQL_DIAG_NUMBER, &numfields, SQL_IS_INTEGER, &diagbytes); - for (i = 0; i < numfields; i++) { - SQLGetDiagRec(SQL_HANDLE_DBC, obj->con, i + 1, state, &nativeerror, diagnostic, sizeof(diagnostic), &diagbytes); - ast_log(LOG_WARNING, "SetConnectAttr (Autocommit) returned an error: %s: %s\n", state, diagnostic); - if (i > 10) { - ast_log(LOG_WARNING, "Oh, that was good. There are really %d diagnostics?\n", (int)numfields); - break; - } - } - } - } else { - /* Non-pooled connection: multiple modules can use the same connection. */ - if ((obj = ao2_callback(class->obj_container, 0, aoro2_obj_notx_cb, NO_TX))) { - /* Object is not constructed, so delete outstanding reference to class. */ - ast_assert(ao2_ref(class, 0) > 1); - ao2_ref(class, -1); - class = NULL; - } else { - /* No entry: build one */ - if (!(obj = ao2_alloc(sizeof(*obj), odbc_obj_destructor))) { - ast_assert(ao2_ref(class, 0) > 1); - ao2_ref(class, -1); - ast_debug(3, "Unable to allocate object\n"); - return NULL; - } - /* obj inherits the outstanding reference to class */ - obj->parent = class; - class = NULL; - if (odbc_obj_connect(obj) == ODBC_FAIL) { - ast_log(LOG_WARNING, "Failed to connect to %s\n", name); - ao2_ref(obj, -1); - obj = NULL; - } else { - ao2_link(obj->parent->obj_container, obj); - ast_assert(ao2_ref(obj, 0) > 1); - } - } - - if (!obj) { - return NULL; - } - - ao2_lock(obj); - - if (SQLSetConnectAttr(obj->con, SQL_ATTR_AUTOCOMMIT, (void *)SQL_AUTOCOMMIT_ON, 0) == SQL_ERROR) { - SQLGetDiagField(SQL_HANDLE_DBC, obj->con, 1, SQL_DIAG_NUMBER, &numfields, SQL_IS_INTEGER, &diagbytes); - for (i = 0; i < numfields; i++) { - SQLGetDiagRec(SQL_HANDLE_DBC, obj->con, i + 1, state, &nativeerror, diagnostic, sizeof(diagnostic), &diagbytes); - ast_log(LOG_WARNING, "SetConnectAttr (Autocommit) returned an error: %s: %s\n", state, diagnostic); - if (i > 10) { - ast_log(LOG_WARNING, "Oh, that was good. There are really %d diagnostics?\n", (int)numfields); - break; - } - } - } - } - - ast_assert(obj != NULL); - - /* Set the isolation property */ - if (SQLSetConnectAttr(obj->con, SQL_ATTR_TXN_ISOLATION, (void *)(long)obj->parent->isolation, 0) == SQL_ERROR) { - SQLGetDiagField(SQL_HANDLE_DBC, obj->con, 1, SQL_DIAG_NUMBER, &numfields, SQL_IS_INTEGER, &diagbytes); - for (i = 0; i < numfields; i++) { - SQLGetDiagRec(SQL_HANDLE_DBC, obj->con, i + 1, state, &nativeerror, diagnostic, sizeof(diagnostic), &diagbytes); - ast_log(LOG_WARNING, "SetConnectAttr (Txn isolation) returned an error: %s: %s\n", state, diagnostic); - if (i > 10) { - ast_log(LOG_WARNING, "Oh, that was good. There are really %d diagnostics?\n", (int)numfields); - break; - } - } - } - - if (ast_test_flag(&flags, RES_ODBC_CONNECTED) && !obj->up) { - odbc_obj_connect(obj); - } else if (ast_test_flag(&flags, RES_ODBC_SANITY_CHECK)) { - ast_odbc_sanity_check(obj); - } else if (obj->parent->idlecheck > 0 && ast_tvdiff_sec(ast_tvnow(), obj->last_used) > obj->parent->idlecheck) { - odbc_obj_connect(obj); + /* XXX ODBC connection objects do not have shared ownership, so there is no reason + * to use refcounted objects here. + */ + obj = ao2_alloc(sizeof(*obj), odbc_obj_destructor); + /* Inherit reference from the ao2_callback from before */ + obj->parent = class; + if (odbc_obj_connect(obj) == ODBC_FAIL) { + ao2_ref(obj, -1); + return NULL; } -#ifdef DEBUG_THREADS - ast_copy_string(obj->file, file, sizeof(obj->file)); - ast_copy_string(obj->function, function, sizeof(obj->function)); - obj->lineno = lineno; -#endif - - /* We had it locked because of the obj_connects we see here. */ - ao2_unlock(obj); - - ast_assert(class == NULL); - - ast_assert(ao2_ref(obj, 0) > 1); return obj; } struct odbc_obj *_ast_odbc_request_obj(const char *name, int check, const char *file, const char *function, int lineno) { struct ast_flags flags = { check ? RES_ODBC_SANITY_CHECK : 0 }; + /* XXX New flow means that the "check" parameter doesn't do anything. We're requesting + * a connection from ODBC. We'll either get a new one, which obviously is already connected, or + * we'll get one from the ODBC connection pool. In that case, it will ensure to only give us a + * live connection + */ return _ast_odbc_request_obj2(name, flags, file, function, lineno); } -struct odbc_obj *ast_odbc_retrieve_transaction_obj(struct ast_channel *chan, const char *objname) -{ - struct ast_datastore *txn_store; - AST_LIST_HEAD(, odbc_txn_frame) *oldlist; - struct odbc_txn_frame *txn = NULL; - - if (!chan) { - /* No channel == no transaction */ - return NULL; - } - - ast_channel_lock(chan); - if ((txn_store = ast_channel_datastore_find(chan, &txn_info, NULL))) { - oldlist = txn_store->data; - } else { - ast_channel_unlock(chan); - return NULL; - } - - AST_LIST_LOCK(oldlist); - ast_channel_unlock(chan); - - AST_LIST_TRAVERSE(oldlist, txn, list) { - if (txn->obj && txn->obj->parent && !strcmp(txn->obj->parent->name, objname)) { - AST_LIST_UNLOCK(oldlist); - return txn->obj; - } - } - AST_LIST_UNLOCK(oldlist); - return NULL; -} - static odbc_status odbc_obj_disconnect(struct odbc_obj *obj) { int res; @@ -1490,20 +757,19 @@ static odbc_status odbc_obj_disconnect(struct odbc_obj *obj) if (obj->parent) { if (res == SQL_SUCCESS || res == SQL_SUCCESS_WITH_INFO) { - ast_debug(1, "Disconnected %d from %s [%s]\n", res, obj->parent->name, obj->parent->dsn); + ast_debug(3, "Disconnected %d from %s [%s](%p)\n", res, obj->parent->name, obj->parent->dsn, obj); } else { - ast_debug(1, "res_odbc: %s [%s] already disconnected\n", obj->parent->name, obj->parent->dsn); + ast_debug(3, "res_odbc: %s [%s](%p) already disconnected\n", obj->parent->name, obj->parent->dsn, obj); } } if ((res = SQLFreeHandle(SQL_HANDLE_DBC, con)) == SQL_SUCCESS) { - ast_debug(1, "Database handle %p deallocated\n", con); + ast_debug(3, "Database handle %p (connection %p) deallocated\n", obj, con); } else { SQLGetDiagRec(SQL_HANDLE_DBC, con, 1, state, &err, msg, 100, &mlen); ast_log(LOG_WARNING, "Unable to deallocate database handle %p? %d errno=%d %s\n", con, res, (int)err, msg); } - obj->up = 0; return ODBC_SUCCESS; } @@ -1520,13 +786,8 @@ static odbc_status odbc_obj_connect(struct odbc_obj *obj) SQLHDBC con; long int negative_cache_expiration; - if (obj->up) { - odbc_obj_disconnect(obj); - ast_log(LOG_NOTICE, "Re-connecting %s\n", obj->parent->name); - } else { - ast_assert(obj->con == NULL); - ast_log(LOG_NOTICE, "Connecting %s\n", obj->parent->name); - } + ast_assert(obj->con == NULL); + ast_debug(3, "Connecting %s(%p)\n", obj->parent->name, obj); /* Dont connect while server is marked as unreachable via negative_connection_cache */ negative_cache_expiration = obj->parent->last_negative_connect.tv_sec + obj->parent->negative_connection_cache.tv_sec; @@ -1564,155 +825,13 @@ static odbc_status odbc_obj_connect(struct odbc_obj *obj) } return ODBC_FAIL; } else { - ast_log(LOG_NOTICE, "res_odbc: Connected to %s [%s]\n", obj->parent->name, obj->parent->dsn); - obj->up = 1; - obj->last_used = ast_tvnow(); + ast_debug(3, "res_odbc: Connected to %s [%s (%p)]\n", obj->parent->name, obj->parent->dsn, obj); } obj->con = con; return ODBC_SUCCESS; } -static int acf_transaction_read(struct ast_channel *chan, const char *cmd, char *data, char *buf, size_t len) -{ - AST_DECLARE_APP_ARGS(args, - AST_APP_ARG(property); - AST_APP_ARG(opt); - ); - struct odbc_txn_frame *tx; - - AST_STANDARD_APP_ARGS(args, data); - if (strcasecmp(args.property, "transaction") == 0) { - if ((tx = find_transaction(chan, NULL, NULL, 1))) { - ast_copy_string(buf, tx->name, len); - return 0; - } - } else if (strcasecmp(args.property, "isolation") == 0) { - if (!ast_strlen_zero(args.opt)) { - tx = find_transaction(chan, NULL, args.opt, 0); - } else { - tx = find_transaction(chan, NULL, NULL, 1); - } - if (tx) { - ast_copy_string(buf, isolation2text(tx->isolation), len); - return 0; - } - } else if (strcasecmp(args.property, "forcecommit") == 0) { - if (!ast_strlen_zero(args.opt)) { - tx = find_transaction(chan, NULL, args.opt, 0); - } else { - tx = find_transaction(chan, NULL, NULL, 1); - } - if (tx) { - ast_copy_string(buf, tx->forcecommit ? "1" : "0", len); - return 0; - } - } - return -1; -} - -static int acf_transaction_write(struct ast_channel *chan, const char *cmd, char *s, const char *value) -{ - AST_DECLARE_APP_ARGS(args, - AST_APP_ARG(property); - AST_APP_ARG(opt); - ); - struct odbc_txn_frame *tx; - SQLINTEGER nativeerror=0, numfields=0; - SQLSMALLINT diagbytes=0, i; - unsigned char state[10], diagnostic[256]; - - AST_STANDARD_APP_ARGS(args, s); - if (strcasecmp(args.property, "transaction") == 0) { - /* Set active transaction */ - struct odbc_obj *obj; - if ((tx = find_transaction(chan, NULL, value, 0))) { - mark_transaction_active(chan, tx); - } else { - /* No such transaction, create one */ - struct ast_flags flags = { RES_ODBC_INDEPENDENT_CONNECTION }; - if (ast_strlen_zero(args.opt) || !(obj = ast_odbc_request_obj2(args.opt, flags))) { - ast_log(LOG_ERROR, "Could not create transaction: invalid database specification '%s'\n", S_OR(args.opt, "")); - pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "INVALID_DB"); - return -1; - } - if (!find_transaction(chan, obj, value, 0)) { - pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "FAILED_TO_CREATE"); - return -1; - } - obj->tx = 1; - } - pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "OK"); - return 0; - } else if (strcasecmp(args.property, "forcecommit") == 0) { - /* Set what happens when an uncommitted transaction ends without explicit Commit or Rollback */ - if (ast_strlen_zero(args.opt)) { - tx = find_transaction(chan, NULL, NULL, 1); - } else { - tx = find_transaction(chan, NULL, args.opt, 0); - } - if (!tx) { - pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "FAILED_TO_CREATE"); - return -1; - } - if (ast_true(value)) { - tx->forcecommit = 1; - } else if (ast_false(value)) { - tx->forcecommit = 0; - } else { - ast_log(LOG_ERROR, "Invalid value for forcecommit: '%s'\n", S_OR(value, "")); - pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "INVALID_VALUE"); - return -1; - } - - pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "OK"); - return 0; - } else if (strcasecmp(args.property, "isolation") == 0) { - /* How do uncommitted transactions affect reads? */ - int isolation = text2isolation(value); - if (ast_strlen_zero(args.opt)) { - tx = find_transaction(chan, NULL, NULL, 1); - } else { - tx = find_transaction(chan, NULL, args.opt, 0); - } - if (!tx) { - pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "FAILED_TO_CREATE"); - return -1; - } - if (isolation == 0) { - pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "INVALID_VALUE"); - ast_log(LOG_ERROR, "Invalid isolation specification: '%s'\n", S_OR(value, "")); - } else if (SQLSetConnectAttr(tx->obj->con, SQL_ATTR_TXN_ISOLATION, (void *)(long)isolation, 0) == SQL_ERROR) { - pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "SQL_ERROR"); - SQLGetDiagField(SQL_HANDLE_DBC, tx->obj->con, 1, SQL_DIAG_NUMBER, &numfields, SQL_IS_INTEGER, &diagbytes); - for (i = 0; i < numfields; i++) { - SQLGetDiagRec(SQL_HANDLE_DBC, tx->obj->con, i + 1, state, &nativeerror, diagnostic, sizeof(diagnostic), &diagbytes); - ast_log(LOG_WARNING, "SetConnectAttr (Txn isolation) returned an error: %s: %s\n", state, diagnostic); - if (i > 10) { - ast_log(LOG_WARNING, "Oh, that was good. There are really %d diagnostics?\n", (int)numfields); - break; - } - } - } else { - pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "OK"); - tx->isolation = isolation; - } - return 0; - } else { - ast_log(LOG_ERROR, "Unknown property: '%s'\n", args.property); - return -1; - } -} - -static struct ast_custom_function odbc_function = { - .name = "ODBC", - .read = acf_transaction_read, - .write = acf_transaction_write, -}; - -static const char * const app_commit = "ODBC_Commit"; -static const char * const app_rollback = "ODBC_Rollback"; - /*! * \internal * \brief Implements the channels provider. @@ -1720,12 +839,10 @@ static const char * const app_rollback = "ODBC_Rollback"; static int data_odbc_provider_handler(const struct ast_data_search *search, struct ast_data *root) { - struct ao2_iterator aoi, aoi2; + struct ao2_iterator aoi; struct odbc_class *class; - struct odbc_obj *current; - struct ast_data *data_odbc_class, *data_odbc_connections, *data_odbc_connection; + struct ast_data *data_odbc_class, *data_odbc_connections; struct ast_data *enum_node; - int count; aoi = ao2_iterator_init(class_container, 0); while ((class = ao2_iterator_next(&aoi))) { @@ -1737,18 +854,12 @@ static int data_odbc_provider_handler(const struct ast_data_search *search, ast_data_add_structure(odbc_class, data_odbc_class, class); - if (!ao2_container_count(class->obj_container)) { - ao2_ref(class, -1); - continue; - } - data_odbc_connections = ast_data_add_node(data_odbc_class, "connections"); if (!data_odbc_connections) { ao2_ref(class, -1); continue; } - ast_data_add_bool(data_odbc_class, "shared", !class->haspool); /* isolation */ enum_node = ast_data_add_node(data_odbc_class, "isolation"); if (!enum_node) { @@ -1756,30 +867,7 @@ static int data_odbc_provider_handler(const struct ast_data_search *search, continue; } ast_data_add_int(enum_node, "value", class->isolation); - ast_data_add_str(enum_node, "text", isolation2text(class->isolation)); - - count = 0; - aoi2 = ao2_iterator_init(class->obj_container, 0); - while ((current = ao2_iterator_next(&aoi2))) { - data_odbc_connection = ast_data_add_node(data_odbc_connections, "connection"); - if (!data_odbc_connection) { - ao2_ref(current, -1); - continue; - } - - ao2_lock(current); - ast_data_add_str(data_odbc_connection, "status", current->used ? "in use" : - current->up && ast_odbc_sanity_check(current) ? "connected" : "disconnected"); - ast_data_add_bool(data_odbc_connection, "transactional", current->tx); - ao2_unlock(current); - - if (class->haspool) { - ast_data_add_int(data_odbc_connection, "number", ++count); - } - - ao2_ref(current, -1); - } - ao2_iterator_destroy(&aoi2); + ast_data_add_str(enum_node, "text", ast_odbc_isolation2text(class->isolation)); ao2_ref(class, -1); if (!ast_data_search_match(search, data_odbc_class)) { @@ -1807,7 +895,6 @@ static int reload(void) { struct odbc_cache_tables *table; struct odbc_class *class; - struct odbc_obj *current; struct ao2_iterator aoi = ao2_iterator_init(class_container, 0); /* First, mark all to be purged */ @@ -1819,51 +906,12 @@ static int reload(void) load_odbc_config(); - /* Purge remaining classes */ - - /* Note on how this works; this is a case of circular references, so we - * explicitly do NOT want to use a callback here (or we wind up in - * recursive hell). - * - * 1. Iterate through all the classes. Note that the classes will currently - * contain two classes of the same name, one of which is marked delme and - * will be purged when all remaining objects of the class are released, and - * the other, which was created above when we re-parsed the config file. - * 2. On each class, there is a reference held by the master container and - * a reference held by each connection object. There are two cases for - * destruction of the class, noted below. However, in all cases, all O-refs - * (references to objects) will first be freed, which will cause the C-refs - * (references to classes) to be decremented (but never to 0, because the - * class container still has a reference). - * a) If the class has outstanding objects, the C-ref by the class - * container will then be freed, which leaves only C-refs by any - * outstanding objects. When the final outstanding object is released - * (O-refs held by applications and dialplan functions), it will in turn - * free the final C-ref, causing class destruction. - * b) If the class has no outstanding objects, when the class container - * removes the final C-ref, the class will be destroyed. - */ aoi = ao2_iterator_init(class_container, 0); - while ((class = ao2_iterator_next(&aoi))) { /* C-ref++ (by iterator) */ + while ((class = ao2_iterator_next(&aoi))) { if (class->delme) { - struct ao2_iterator aoi2 = ao2_iterator_init(class->obj_container, 0); - while ((current = ao2_iterator_next(&aoi2))) { /* O-ref++ (by iterator) */ - ao2_unlink(class->obj_container, current); /* unlink O-ref from class (reference handled implicitly) */ - ao2_ref(current, -1); /* O-ref-- (by iterator) */ - /* At this point, either - * a) there's an outstanding O-ref, or - * b) the object has already been destroyed. - */ - } - ao2_iterator_destroy(&aoi2); - ao2_unlink(class_container, class); /* unlink C-ref from container (reference handled implicitly) */ - /* At this point, either - * a) there's an outstanding O-ref, which holds an outstanding C-ref, or - * b) the last remaining C-ref is held by the iterator, which will be - * destroyed in the next step. - */ + ao2_unlink(class_container, class); } - ao2_ref(class, -1); /* C-ref-- (by iterator) */ + ao2_ref(class, -1); } ao2_iterator_destroy(&aoi); @@ -1901,9 +949,6 @@ static int load_module(void) return AST_MODULE_LOAD_DECLINE; ast_cli_register_multiple(cli_odbc, ARRAY_LEN(cli_odbc)); ast_data_register_multiple(odbc_providers, ARRAY_LEN(odbc_providers)); - ast_register_application_xml(app_commit, commit_exec); - ast_register_application_xml(app_rollback, rollback_exec); - ast_custom_function_register(&odbc_function); ast_log(LOG_NOTICE, "res_odbc loaded.\n"); return 0; } diff --git a/res/res_odbc.exports.in b/res/res_odbc.exports.in index ad674beb17..84bbc48dd7 100644 --- a/res/res_odbc.exports.in +++ b/res/res_odbc.exports.in @@ -12,9 +12,13 @@ LINKER_SYMBOL_PREFIX_ast_odbc_request_obj; LINKER_SYMBOL_PREFIXast_odbc_request_obj2; LINKER_SYMBOL_PREFIX_ast_odbc_request_obj2; - LINKER_SYMBOL_PREFIXast_odbc_retrieve_transaction_obj; LINKER_SYMBOL_PREFIXast_odbc_sanity_check; LINKER_SYMBOL_PREFIXast_odbc_smart_execute; + LINKER_SYMBOL_PREFIXast_odbc_class_get_isolation; + LINKER_SYMBOL_PREFIXast_odbc_class_get_forcecommit; + LINKER_SYMBOL_PREFIXast_odbc_class_get_name; + LINKER_SYMBOL_PREFIXast_odbc_text2isolation; + LINKER_SYMBOL_PREFIXast_odbc_isolation2text; local: *; }; diff --git a/res/res_odbc_transaction.c b/res/res_odbc_transaction.c new file mode 100644 index 0000000000..33800c3ce3 --- /dev/null +++ b/res/res_odbc_transaction.c @@ -0,0 +1,529 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2016, Digium, Inc. + * + * Mark Michelson + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#include "asterisk.h" + +#include "asterisk/res_odbc.h" +#include "asterisk/res_odbc_transaction.h" +#include "asterisk/channel.h" +#include "asterisk/pbx.h" +#include "asterisk/app.h" +#include "asterisk/module.h" + +/*** MODULEINFO + res_odbc + core + ***/ + +/*** DOCUMENTATION + + + Controls ODBC transaction properties. + + + + + + Gets or sets the active transaction ID. If set, and the transaction ID does not + exist and a database name is specified as an argument, it will be created. + + + Controls whether a transaction will be automatically committed when the channel + hangs up. Defaults to false. If a transaction ID is specified in the optional argument, + the property will be applied to that ID, otherwise to the current active ID. + + + Controls the data isolation on uncommitted transactions. May be one of the + following: read_committed, read_uncommitted, + repeatable_read, or serializable. Defaults to the + database setting in res_odbc.conf or read_committed + if not specified. If a transaction ID is specified as an optional argument, it will be + applied to that ID, otherwise the current active ID. + + + + + + + The ODBC() function allows setting several properties to influence how a connected + database processes transactions. + + + + + Commits a currently open database transaction. + + + + + + Commits the database transaction specified by transaction ID + or the current active transaction, if not specified. + + + + + Rollback a currently open database transaction. + + + + + + Rolls back the database transaction specified by transaction ID + or the current active transaction, if not specified. + + + ***/ + +struct odbc_txn_frame { + AST_LIST_ENTRY(odbc_txn_frame) list; + struct odbc_obj *obj; /*!< Database handle within which transacted statements are run */ + /*!\brief Is this record the current active transaction within the channel? + * Note that the active flag is really only necessary for statements which + * are triggered from the dialplan, as there isn't a direct correlation + * between multiple statements. Applications wishing to use transactions + * may simply perform each statement on the same odbc_obj, which keeps the + * transaction persistent. + */ + unsigned int active:1; + unsigned int forcecommit:1; /*!< Should uncommitted transactions be auto-committed on handle release? */ + unsigned int isolation; /*!< Flags for how the DB should deal with data in other, uncommitted transactions */ + char name[0]; /*!< Name of this transaction ID */ +}; + +static struct odbc_txn_frame *release_transaction(struct odbc_txn_frame *tx); + +static void odbc_txn_free(void *vdata) +{ + struct odbc_txn_frame *tx; + AST_LIST_HEAD(, odbc_txn_frame) *oldlist = vdata; + + ast_debug(2, "odbc_txn_free(%p) called\n", vdata); + + AST_LIST_LOCK(oldlist); + while ((tx = AST_LIST_REMOVE_HEAD(oldlist, list))) { + release_transaction(tx); + } + AST_LIST_UNLOCK(oldlist); + AST_LIST_HEAD_DESTROY(oldlist); + ast_free(oldlist); +} + +static const struct ast_datastore_info txn_info = { + .type = "ODBC_Transaction", + .destroy = odbc_txn_free, +}; + +static struct odbc_txn_frame *create_transaction(struct ast_channel *chan, const char *name, const char *dsn) +{ + struct ast_datastore *txn_store; + AST_LIST_HEAD(, odbc_txn_frame) *oldlist; + struct odbc_txn_frame *txn = NULL; + struct odbc_txn_frame *otxn; + + if (ast_strlen_zero(dsn)) { + return NULL; + } + + ast_channel_lock(chan); + if ((txn_store = ast_channel_datastore_find(chan, &txn_info, NULL))) { + oldlist = txn_store->data; + } else { + if (!(txn_store = ast_datastore_alloc(&txn_info, NULL))) { + ast_log(LOG_ERROR, "Unable to allocate a new datastore. Cannot create a new transaction.\n"); + ast_channel_unlock(chan); + return NULL; + } + + if (!(oldlist = ast_calloc(1, sizeof(*oldlist)))) { + ast_log(LOG_ERROR, "Unable to allocate datastore list head. Cannot create a new transaction.\n"); + ast_datastore_free(txn_store); + ast_channel_unlock(chan); + return NULL; + } + + txn_store->data = oldlist; + AST_LIST_HEAD_INIT(oldlist); + ast_channel_datastore_add(chan, txn_store); + } + ast_channel_unlock(chan); + + txn = ast_calloc(1, sizeof(*txn) + strlen(name) + 1); + if (!txn) { + return NULL; + } + + strcpy(txn->name, name); /* SAFE */ + txn->obj = ast_odbc_request_obj(dsn, 0); + if (!txn->obj) { + ast_free(txn); + return NULL; + } + txn->isolation = ast_odbc_class_get_isolation(txn->obj->parent); + txn->forcecommit = ast_odbc_class_get_isolation(txn->obj->parent); + txn->active = 1; + + if (SQLSetConnectAttr(txn->obj->con, SQL_ATTR_AUTOCOMMIT, (void *)SQL_AUTOCOMMIT_OFF, 0) == SQL_ERROR) { + ast_odbc_print_errors(SQL_HANDLE_DBC, txn->obj->con, "SetConnectAttr (Autocommit)"); + ast_odbc_release_obj(txn->obj); + ast_free(txn); + return NULL; + } + + /* Set the isolation property */ + if (SQLSetConnectAttr(txn->obj->con, SQL_ATTR_TXN_ISOLATION, (void *)(long)txn->isolation, 0) == SQL_ERROR) { + ast_odbc_print_errors(SQL_HANDLE_DBC, txn->obj->con, "SetConnectAttr"); + ast_odbc_release_obj(txn->obj); + ast_free(txn); + return NULL; + } + + /* On creation, the txn becomes active, and all others inactive */ + AST_LIST_LOCK(oldlist); + AST_LIST_TRAVERSE(oldlist, otxn, list) { + otxn->active = 0; + } + AST_LIST_INSERT_TAIL(oldlist, txn, list); + AST_LIST_UNLOCK(oldlist); + + return txn; +} + +static struct odbc_txn_frame *find_transaction(struct ast_channel *chan, const char *name, int active) +{ + struct ast_datastore *txn_store; + AST_LIST_HEAD(, odbc_txn_frame) *oldlist; + struct odbc_txn_frame *txn = NULL; + + if (!chan || (!active && !name)) { + return NULL; + } + + ast_channel_lock(chan); + txn_store = ast_channel_datastore_find(chan, &txn_info, NULL); + ast_channel_unlock(chan); + + if (!txn_store) { + /* No datastore? Definitely no transaction then */ + return NULL; + } + + oldlist = txn_store->data; + AST_LIST_LOCK(oldlist); + + AST_LIST_TRAVERSE(oldlist, txn, list) { + if (active) { + if (txn->active) { + break; + } + } else if (!strcasecmp(txn->name, name)) { + break; + } + } + AST_LIST_UNLOCK(oldlist); + + return txn; +} + +static struct odbc_txn_frame *release_transaction(struct odbc_txn_frame *tx) +{ + if (!tx) { + return NULL; + } + + ast_debug(2, "release_transaction(%p) called (tx->obj = %p\n", tx, tx->obj); + + ast_debug(1, "called on a transactional handle with %s\n", tx->forcecommit ? "COMMIT" : "ROLLBACK"); + if (SQLEndTran(SQL_HANDLE_DBC, tx->obj->con, tx->forcecommit ? SQL_COMMIT : SQL_ROLLBACK) == SQL_ERROR) { + ast_odbc_print_errors(SQL_HANDLE_DBC, tx->obj->con, "SQLEndTran"); + } + + /* Transaction is done, reset autocommit + * + * XXX I'm unsure if this is actually necessary, since we're releasing + * the connection back to unixODBC. However, if unixODBC pooling is enabled, + * it can't hurt to do just in case. + */ + if (SQLSetConnectAttr(tx->obj->con, SQL_ATTR_AUTOCOMMIT, (void *)SQL_AUTOCOMMIT_ON, 0) == SQL_ERROR) { + ast_odbc_print_errors(SQL_HANDLE_DBC, tx->obj->con, "SQLSetAttr"); + } + + ast_odbc_release_obj(tx->obj); + ast_free(tx); + return NULL; +} + +static int commit_exec(struct ast_channel *chan, const char *data) +{ + struct odbc_txn_frame *tx; + + if (ast_strlen_zero(data)) { + tx = find_transaction(chan, NULL, 1); + } else { + tx = find_transaction(chan, data, 0); + } + + /* XXX COMMIT_RESULT is set to OK even if no transaction was found. Very misleading */ + pbx_builtin_setvar_helper(chan, "COMMIT_RESULT", "OK"); + + if (tx) { + if (SQLEndTran(SQL_HANDLE_DBC, tx->obj->con, SQL_COMMIT) == SQL_ERROR) { + struct ast_str *errors = ast_odbc_print_errors(SQL_HANDLE_DBC, tx->obj->con, "SQLEndTran"); + pbx_builtin_setvar_helper(chan, "COMMIT_RESULT", ast_str_buffer(errors)); + } + } + return 0; +} + +static int rollback_exec(struct ast_channel *chan, const char *data) +{ + struct odbc_txn_frame *tx; + + if (ast_strlen_zero(data)) { + tx = find_transaction(chan, NULL, 1); + } else { + tx = find_transaction(chan, data, 0); + } + + /* XXX ROLLBACK_RESULT is set to OK even if no transaction was found. Very misleading */ + pbx_builtin_setvar_helper(chan, "ROLLBACK_RESULT", "OK"); + + if (tx) { + if (SQLEndTran(SQL_HANDLE_DBC, tx->obj->con, SQL_ROLLBACK) == SQL_ERROR) { + struct ast_str *errors = ast_odbc_print_errors(SQL_HANDLE_DBC, tx->obj->con, "SQLEndTran"); + pbx_builtin_setvar_helper(chan, "ROLLBACK_RESULT", ast_str_buffer(errors)); + } + } + return 0; +} + +static int acf_transaction_read(struct ast_channel *chan, const char *cmd, char *data, char *buf, size_t len) +{ + AST_DECLARE_APP_ARGS(args, + AST_APP_ARG(property); + AST_APP_ARG(opt); + ); + struct odbc_txn_frame *tx; + + AST_STANDARD_APP_ARGS(args, data); + if (strcasecmp(args.property, "transaction") == 0) { + if ((tx = find_transaction(chan, NULL, 1))) { + ast_copy_string(buf, tx->name, len); + return 0; + } + } else if (strcasecmp(args.property, "isolation") == 0) { + if (!ast_strlen_zero(args.opt)) { + tx = find_transaction(chan, args.opt, 0); + } else { + tx = find_transaction(chan, NULL, 1); + } + if (tx) { + ast_copy_string(buf, ast_odbc_isolation2text(tx->isolation), len); + return 0; + } + } else if (strcasecmp(args.property, "forcecommit") == 0) { + if (!ast_strlen_zero(args.opt)) { + tx = find_transaction(chan, args.opt, 0); + } else { + tx = find_transaction(chan, NULL, 1); + } + if (tx) { + ast_copy_string(buf, tx->forcecommit ? "1" : "0", len); + return 0; + } + } + return -1; +} + +/* XXX The idea of "active" transactions is silly and makes things + * more prone to error. It would be much better if the transaction + * always had to be specified by name so that no implicit behavior + * occurred. + */ +static int mark_transaction_active(struct ast_channel *chan, struct odbc_txn_frame *tx) +{ + struct ast_datastore *txn_store; + AST_LIST_HEAD(, odbc_txn_frame) *oldlist; + struct odbc_txn_frame *active = NULL, *txn; + + if (!chan) { + return -1; + } + + ast_channel_lock(chan); + if (!(txn_store = ast_channel_datastore_find(chan, &txn_info, NULL))) { + ast_channel_unlock(chan); + return -1; + } + + oldlist = txn_store->data; + AST_LIST_LOCK(oldlist); + AST_LIST_TRAVERSE(oldlist, txn, list) { + if (txn == tx) { + txn->active = 1; + active = txn; + } else { + txn->active = 0; + } + } + AST_LIST_UNLOCK(oldlist); + ast_channel_unlock(chan); + return active ? 0 : -1; +} + +static int acf_transaction_write(struct ast_channel *chan, const char *cmd, char *s, const char *value) +{ + AST_DECLARE_APP_ARGS(args, + AST_APP_ARG(property); + AST_APP_ARG(opt); + ); + struct odbc_txn_frame *tx; + + AST_STANDARD_APP_ARGS(args, s); + if (strcasecmp(args.property, "transaction") == 0) { + /* Set active transaction */ + if ((tx = find_transaction(chan, value, 0))) { + mark_transaction_active(chan, tx); + } else if (!create_transaction(chan, value, args.opt)) { + pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "FAILED_TO_CREATE"); + return -1; + } + pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "OK"); + return 0; + } else if (strcasecmp(args.property, "forcecommit") == 0) { + /* Set what happens when an uncommitted transaction ends without explicit Commit or Rollback */ + if (ast_strlen_zero(args.opt)) { + tx = find_transaction(chan, NULL, 1); + } else { + tx = find_transaction(chan, args.opt, 0); + } + if (!tx) { + pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "FAILED_TO_CREATE"); + return -1; + } + if (ast_true(value)) { + tx->forcecommit = 1; + } else if (ast_false(value)) { + tx->forcecommit = 0; + } else { + ast_log(LOG_ERROR, "Invalid value for forcecommit: '%s'\n", S_OR(value, "")); + pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "INVALID_VALUE"); + return -1; + } + + pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "OK"); + return 0; + } else if (strcasecmp(args.property, "isolation") == 0) { + /* How do uncommitted transactions affect reads? */ + /* XXX This is completely useless. The problem is that setting the isolation here + * does not actually alter the connection. The only time the isolation gets set is + * when the transaction is created. The only way to set isolation is to set it on + * the ODBC class's configuration in res_odbc.conf. + */ + int isolation = ast_odbc_text2isolation(value); + if (ast_strlen_zero(args.opt)) { + tx = find_transaction(chan, NULL, 1); + } else { + tx = find_transaction(chan, args.opt, 0); + } + if (!tx) { + pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "FAILED_TO_CREATE"); + return -1; + } + if (isolation == 0) { + pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "INVALID_VALUE"); + ast_log(LOG_ERROR, "Invalid isolation specification: '%s'\n", S_OR(value, "")); + } else if (SQLSetConnectAttr(tx->obj->con, SQL_ATTR_TXN_ISOLATION, (void *)(long)isolation, 0) == SQL_ERROR) { + pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "SQL_ERROR"); + ast_odbc_print_errors(SQL_HANDLE_DBC, tx->obj->con, "SetConnectAttr (Txn isolation)"); + } else { + pbx_builtin_setvar_helper(chan, "ODBC_RESULT", "OK"); + tx->isolation = isolation; + } + return 0; + } else { + ast_log(LOG_ERROR, "Unknown property: '%s'\n", args.property); + return -1; + } +} + +struct odbc_obj *ast_odbc_retrieve_transaction_obj(struct ast_channel *chan, const char *objname) +{ + struct ast_datastore *txn_store; + AST_LIST_HEAD(, odbc_txn_frame) *oldlist; + struct odbc_txn_frame *txn = NULL; + + if (!chan || !objname) { + /* No channel == no transaction */ + return NULL; + } + + ast_channel_lock(chan); + if ((txn_store = ast_channel_datastore_find(chan, &txn_info, NULL))) { + oldlist = txn_store->data; + } else { + ast_channel_unlock(chan); + return NULL; + } + + AST_LIST_LOCK(oldlist); + ast_channel_unlock(chan); + + AST_LIST_TRAVERSE(oldlist, txn, list) { + if (txn->obj && txn->obj->parent && !strcmp(ast_odbc_class_get_name(txn->obj->parent), objname)) { + AST_LIST_UNLOCK(oldlist); + return txn->obj; + } + } + AST_LIST_UNLOCK(oldlist); + return NULL; +} + +static struct ast_custom_function odbc_function = { + .name = "ODBC", + .read = acf_transaction_read, + .write = acf_transaction_write, +}; + +static const char * const app_commit = "ODBC_Commit"; +static const char * const app_rollback = "ODBC_Rollback"; + +/* XXX res_odbc takes the path of disallowing unloads from happening. + * It's not a great precedent, but since trying to deal with unloading the module + * while transactions are active seems like a huge pain to deal with, we'll go + * the same way here. + */ +static int unload_module(void) +{ + return -1; +} + +static int load_module(void) +{ + ast_register_application_xml(app_commit, commit_exec); + ast_register_application_xml(app_rollback, rollback_exec); + ast_custom_function_register(&odbc_function); + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "ODBC transaction resource", + .support_level = AST_MODULE_SUPPORT_CORE, + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_REALTIME_DEPEND, + ); diff --git a/res/res_odbc_transaction.exports.in b/res/res_odbc_transaction.exports.in new file mode 100644 index 0000000000..5b06155471 --- /dev/null +++ b/res/res_odbc_transaction.exports.in @@ -0,0 +1,6 @@ +{ + global: + LINKER_SYMBOL_PREFIXast_odbc_retrieve_transaction_obj; + local: + *; +};