/* KInterbasDB Python Package - Implementation of Transactional Operations ** ** Version 3.1 ** ** The following contributors hold Copyright (C) over their respective ** portions of code (see license.txt for details): ** ** [Original Author (maintained through version 2.0-0.3.1):] ** 1998-2001 [alex] Alexander Kuznetsov ** [Maintainers (after version 2.0-0.3.1):] ** 2001-2002 [maz] Marek Isalski ** 2002-2004 [dsr] David Rushby ** [Contributors:] ** 2001 [eac] Evgeny A. Cherkashin ** 2001-2002 [janez] Janez Jere */ /* Distributed transaction support added 2003.04.27. */ /*************************** DECLARATIONS : begin *****************************/ typedef enum { OP_COMMIT = 1, OP_ROLLBACK = 0 } WhichTransactionOperation; typedef enum { OP_RESULT_OK = 0, OP_RESULT_ERROR = -1 } TransactionalOperationResult; #define CONSTRAIN_BOOLEAN(integer_var, error_msg) \ if (integer_var != FALSE && integer_var != TRUE) { \ PyErr_SetString(PyExc_TypeError, error_msg); \ return NULL; \ } /**************************** DECLARATIONS : end ******************************/ /************************ CORE FUNCTIONALITY : begin **************************/ #define CON_GET_TRANS_HANDLE(con) ( \ (con->trans_handle != NULL) ? \ con->trans_handle \ : _con_get_transaction_handle_from_group(con) \ ) static isc_tr_handle _con_get_transaction_handle_from_group(ConnectionObject *con) { PyObject *group = con->group; isc_tr_handle native_handle = NULL; /* This function should never be called if the con has its own trans_handle ** (use CON_GET_TRANS_HANDLE for situations with that potential). */ assert (con->trans_handle == NULL); if (group != NULL) { PyObject *py_trans_handle = PyObject_GetAttrString(group, "_trans_handle"); if (py_trans_handle == NULL) { /* The Python layer will never destroy the _trans_handle attribute of a ** ConnectionGroup and then allow us to reach this point, so a NULL must ** indicate a low memory condition rather than a missing attribute. ** YYY: Callers of this functions won't actually be testing for an ** exception, so the following is kinda useless: */ return PyErr_NoMemory(); } else if (py_trans_handle != Py_None) { /* Python layer shouldn't set ConnectionGroup._trans_handle to anything ** other than a TransactionHandleObject or None; enforce this. */ assert (py_trans_handle->ob_type == &TransactionHandleType); native_handle = ((TransactionHandleObject *) py_trans_handle)->native_handle; } Py_DECREF(py_trans_handle); } return native_handle; } /* _con_get_transaction_handle_from_group */ static isc_tr_handle *CON_GET_TRANS_HANDLE_ADDR(ConnectionObject *con) { if (con->trans_handle != NULL) { return &con->trans_handle; } else { PyObject *group = con->group; isc_tr_handle *native_handle = NULL; if (group != NULL) { PyObject *py_trans_handle = PyObject_GetAttrString(group, "_trans_handle"); if (py_trans_handle == NULL) { /* YYY: caller won't actually detect exception: */ return (isc_tr_handle *) PyErr_NoMemory(); } /* The Python layer should not allow this function to be called if the ** ConnectionGroup has not yet established a transaction handle. */ assert (py_trans_handle != Py_None); assert (py_trans_handle->ob_type == &TransactionHandleType); native_handle = &((TransactionHandleObject *) py_trans_handle)->native_handle; Py_DECREF(py_trans_handle); } return native_handle; } } /* CON_GET_TRANS_HANDLE_ADDR */ isc_tr_handle begin_transaction( /* Either: */ isc_db_handle db_handle, char *tpb, long tpb_len, /* Or: */ ISC_TEB *tebs, short teb_count, ISC_STATUS *status_vector ) { isc_tr_handle trans_handle = NULL; /* (db_handle+tpb+tpb_len) and (tebs+teb_count) are mutually exclusive ** parameters. */ if (db_handle != NULL) { assert (tebs == NULL); } else { assert (tebs != NULL); assert (tpb == NULL); } /* 2003.02.21: A huge TPB such as 'con.begin(tpb='x'*50000)' crashes the ** FB 1.0.2 server process, but responsibly raises an error with FB 1.5b2. ** Since kinterbasdb only exposes some 20 TPB component values, many of which ** are mutually exclusive, I decided to impose a reasonable limit right here. */ if (tpb_len > 31) { raise_exception(ProgrammingError, "Transaction parameter buffer (TPB) too" " large. len(tpb) must be <= 31." ); return NULL; } ENTER_DB if (tebs == NULL) { isc_start_transaction( status_vector, &trans_handle, /* Only one database handle is being passed. */ 1, &db_handle, tpb_len, tpb ); } else { isc_start_multiple( status_vector, &trans_handle, teb_count, tebs ); } LEAVE_DB if ( DB_API_ERROR(status_vector) ) { raise_sql_exception( OperationalError, "begin transaction: ", status_vector ); return NULL; } assert (trans_handle != NULL); return trans_handle; } /* begin_transaction */ /* 2003.08.28: Added option for manual control over phases of 2PC. */ static TransactionalOperationResult prepare_transaction( isc_tr_handle trans_handle, ISC_STATUS *status_vector ) { /* commit_transaction and rollback_transaction are required by the DB API to ** accept a nonexistent transaction without complaint; this function behaves ** consistently. */ if (trans_handle == NULL) { return OP_RESULT_OK; } ENTER_DB isc_prepare_transaction( status_vector, &trans_handle ); LEAVE_DB if ( DB_API_ERROR(status_vector) ) { raise_sql_exception( OperationalError, "prepare: ", status_vector ); return OP_RESULT_ERROR; } return OP_RESULT_OK; } /* prepare_transaction */ /* 2003.01.21: added option for retaining commit */ static TransactionalOperationResult commit_transaction( isc_tr_handle trans_handle, boolean retaining, ISC_STATUS *status_vector ) { if (trans_handle == NULL) { /* 2003.02.17: As discussed on the Python DB-SIG in message: ** http://mail.python.org/pipermail/db-sig/2003-February/003158.html ** , allow a transaction to be committed even if its existence is only ** implicit. */ return OP_RESULT_OK; } ENTER_DB if (!retaining) { isc_commit_transaction( status_vector, &trans_handle ); } else { isc_commit_retaining( status_vector, &trans_handle ); assert (trans_handle != NULL); } LEAVE_DB if ( DB_API_ERROR(status_vector) ) { raise_sql_exception( OperationalError, "commit: ", status_vector ); return OP_RESULT_ERROR; } return OP_RESULT_OK; } /* commit_transaction */ static TransactionalOperationResult rollback_transaction( isc_tr_handle trans_handle, boolean retaining, boolean allowed_to_raise_exception, ISC_STATUS *status_vector ) { /* If there is not an active transaction, rolling back is meaningless. */ if (trans_handle == NULL) { return OP_RESULT_OK; } ENTER_DB if (!retaining) { isc_rollback_transaction( status_vector, &trans_handle ); assert (trans_handle == NULL); } else { #ifdef INTERBASE6_OR_LATER /* IB 5.5 lacks isc_rollback_retaining. */ isc_rollback_retaining( status_vector, &trans_handle ); assert (trans_handle != NULL); #else raise_exception( OperationalError, "Versions of Interbase prior to 6.0" " do not support retaining rollback." ); return OP_RESULT_ERROR; #endif } LEAVE_DB if ( DB_API_ERROR(status_vector) ) { if (allowed_to_raise_exception) { raise_sql_exception( OperationalError, "rollback: ", status_vector ); } return OP_RESULT_ERROR; } return OP_RESULT_OK; } /* rollback_transaction */ static TransactionalOperationResult rollback_ANY_transaction( ConnectionObject *con, boolean allowed_to_raise_exception ) { /* Given a connection, this function rolls back either the connection's own ** non-distributed transaction or the connection's group's distributed ** transaction. ** Note that this function never performs a retaining rollback. */ TransactionalOperationResult result = OP_RESULT_ERROR; if (con->group == NULL) { result = rollback_transaction( con->trans_handle, FALSE /* not retaining */, allowed_to_raise_exception, con->status_vector ); con->trans_handle = NULL; /* 2003.09.01 */ } else { assert (con->trans_handle == NULL); { PyObject *py_result = PyObject_CallMethod(con->group, "rollback", NULL); if (py_result == NULL) { if (!allowed_to_raise_exception) { PyErr_Clear(); } } else { Py_DECREF(py_result); result = OP_RESULT_OK; } } } return result; } /* rollback_ANY_transaction */ /************************* CORE FUNCTIONALITY : end ***************************/ /********** PYTHON WRAPPERS FOR NON-DISTRIBUTED TRANS OPS : begin *************/ static PyObject *pyob_begin( PyObject *self, PyObject *args ) { ConnectionObject *con; char *tpb = NULL; int tpb_len = 0; if ( !PyArg_ParseTuple( args, "O!s#", &ConnectionType, &con, &tpb, &tpb_len ) ) { return NULL; } CONN_REQUIRE_OPEN(con); /* Raise a more informative error message if the previous transaction is ** still active when the client attempts to start another. The old approach ** was to go ahead and try to start the new transaction regardless. If there ** was already an active transaction, the resulting exception made no mention ** of it, which was very confusing. */ if (CON_GET_TRANS_HANDLE(con) != NULL) { /* 2003.10.15a:OK */ raise_exception_with_numeric_error_code(ProgrammingError, -901, "Previous transaction still active; cannot start new transaction." " Use commit() or rollback() to resolve the old transaction first." ); return NULL; } if ( (con->trans_handle = begin_transaction( con->db_handle, tpb, tpb_len, NULL, -1, /* all TEB-related params are null */ con->status_vector ) ) == NULL ) { return NULL; } RETURN_PY_NONE; } /* pyob_begin */ /* 2003.08.28: Added option for manual control over phases of 2PC. */ static PyObject *pyob_prepare( PyObject *self, PyObject *args ) { ConnectionObject *con; if ( !PyArg_ParseTuple( args, "O!", &ConnectionType, &con ) ) { return NULL; } CONN_REQUIRE_OPEN(con); if ( prepare_transaction(con->trans_handle, con->status_vector) != OP_RESULT_OK ) { return NULL; } RETURN_PY_NONE; } /* pyob_prepare */ static PyObject *_pyob_commit_or_rollback( WhichTransactionOperation op, PyObject *self, PyObject *args ) { ConnectionObject *con; boolean retaining; TransactionalOperationResult action_result; { PyObject *retaining_asObj; if ( !PyArg_ParseTuple( args, "O!O", &ConnectionType, &con, &retaining_asObj ) ) { return NULL; } CONN_REQUIRE_OPEN(con); retaining = (boolean) PyObject_IsTrue(retaining_asObj); } if (op == OP_COMMIT) { action_result = commit_transaction( CON_GET_TRANS_HANDLE(con), /* 2003.10.15a:OK */ retaining, con->status_vector ); } else { /* op == OP_ROLLBACK */ action_result = rollback_transaction( CON_GET_TRANS_HANDLE(con), /* 2003.10.15a:OK */ retaining, TRUE, con->status_vector ); } if (action_result != OP_RESULT_OK) { return NULL; } if (!retaining) { con->trans_handle = NULL; } RETURN_PY_NONE; } /* _pyob_commit_or_rollback */ static PyObject *pyob_commit( PyObject *self, PyObject *args ) { return _pyob_commit_or_rollback( OP_COMMIT, self, args ); } /* pyob_commit */ static PyObject *pyob_rollback( PyObject *self, PyObject *args ) { return _pyob_commit_or_rollback( OP_ROLLBACK, self, args ); } /* pyob_rollback */ /*********** PYTHON WRAPPERS FOR NON-DISTRIBUTED TRANS OPS : end **************/ /************ PYTHON WRAPPERS FOR DISTRIBUTED TRANS OPS : begin ***************/ static TransactionHandleObject *new_transaction_handle(void) { TransactionHandleObject *trans_handle = PyObject_New(TransactionHandleObject, &TransactionHandleType); if (trans_handle == NULL) { return (TransactionHandleObject *) PyErr_NoMemory(); } trans_handle->native_handle = NULL; return trans_handle; } /* new_transaction_handle */ static void pyob_transaction_handle_del( PyObject *obj ) { TransactionHandleObject *trans_handle = (TransactionHandleObject *) obj; /* Normally, the database client library will have already set the ** native_handle to NULL when it either committed or rolled back the ** transaction. If the client library could do neither of those, we simply ** free the handle's memory and forget about it. */ if (trans_handle->native_handle != NULL) { kimem_db_client_free(trans_handle->native_handle); } PyObject_Del(trans_handle); } /* pyob_transaction_handle_del */ static ISC_TEB *build_teb_buffer(PyObject *cons) { ISC_TEB *tebs = NULL; int teb_count; int tebs_size; ConnectionObject *con = NULL; PyObject *tpb = NULL; int i; /* The caller should have already ensured this: */ assert (PyList_Check(cons)); teb_count = PyList_GET_SIZE(cons); tebs_size = sizeof(ISC_TEB) * teb_count; tebs = kimem_main_malloc(tebs_size); if (tebs == NULL) { PyErr_NoMemory(); goto BUILD_TEB_FAILURE; } for (i = 0; i < teb_count; i++) { ISC_TEB *t = tebs + i; PyObject *py_con = PyList_GET_ITEM(cons, i); /* borrowed ref */ /* PyObject_GetAttrString returns a new reference. These new references ** are are released at the end of each iteration of this loop (normally), ** or in the BUILD_TEB_FAILURE clause in case of error. */ con = (ConnectionObject *) PyObject_GetAttrString(py_con, "_C_con"); if (con == NULL) { goto BUILD_TEB_FAILURE; } tpb = PyObject_GetAttrString(py_con, "default_tpb"); if (tpb == NULL) { goto BUILD_TEB_FAILURE; } /* The Python layer should have already ensured this: */ assert (con->db_handle != NULL); t->db_ptr = (long *) &con->db_handle; if (tpb == Py_None) { t->tpb_len = 0; t->tpb_ptr = NULL; } else if (PyString_Check(tpb)) { t->tpb_len = PyString_GET_SIZE(tpb); t->tpb_ptr = PyString_AS_STRING(tpb); } else { PyErr_SetString(PyExc_TypeError, "Connection.default_tpb must be a raw binary buffer (string) or None." ); goto BUILD_TEB_FAILURE; } Py_DECREF(con); Py_DECREF(tpb); /* Set to NULL to prevent double-decref in case of error during next ** iteration. */ con = NULL; tpb = NULL; } /* Upon successful exit, all references will have been released. */ return tebs; BUILD_TEB_FAILURE: Py_XDECREF(con); Py_XDECREF(tpb); kimem_main_free(tebs); return NULL; } /* build_teb_buffer */ static PyObject *pyob_distributed_begin( PyObject *self, PyObject *args ) { /* $cons, the input from the Python level, is a list of instances of Python ** class kinterbasdb.Connection, not a list of instances of C type ** ConnectionType. */ PyObject *cons; TransactionHandleObject *trans_handle = NULL; ISC_TEB *tebs = NULL; int teb_count; ISC_STATUS status_vector[STATUS_VECTOR_SIZE]; if ( !PyArg_ParseTuple(args, "O!", &PyList_Type, &cons) ) { return NULL; } teb_count = PyList_GET_SIZE(cons); /* The Python layer should prevent the programmer from starting a distributed ** transaction with an empty group. */ assert (teb_count > 0); /* The Python layer (ConnectionGroup class) should prevent the programmer ** from exceeding the database engine's limits on the number of database ** handles that can participate in a single distributed transaction. */ assert (teb_count <= DIST_TRANS_MAX_DATABASES); tebs = build_teb_buffer(cons); if (tebs == NULL) { goto PYOB_DISTRIBUTED_FINISH; } trans_handle = new_transaction_handle(); if (trans_handle == NULL) { goto PYOB_DISTRIBUTED_FINISH; } trans_handle->native_handle = begin_transaction( NULL, NULL, -1, /* All parameters for singleton transactions are null. */ tebs, (short) teb_count, status_vector ); if (trans_handle->native_handle == NULL) { goto PYOB_DISTRIBUTED_FINISH; } PYOB_DISTRIBUTED_FINISH: kimem_main_free(tebs); if (trans_handle == NULL) { return NULL; } else if (trans_handle->native_handle == NULL) { Py_DECREF(trans_handle); return NULL; } else { return (PyObject *) trans_handle; } } /* pyob_distributed_begin */ /* 2003.08.28: Added option for manual control over phases of 2PC. */ static PyObject *pyob_distributed_prepare( PyObject *self, PyObject *args ) { TransactionHandleObject *py_handle; ISC_STATUS status_vector[STATUS_VECTOR_SIZE]; if ( !PyArg_ParseTuple( args, "O!", &TransactionHandleType, &py_handle ) ) { return NULL; } if ( prepare_transaction(py_handle->native_handle, status_vector) != OP_RESULT_OK ) { return NULL; } RETURN_PY_NONE; } /* pyob_distributed_prepare */ static PyObject *_pyob_distributed_commit_or_rollback( WhichTransactionOperation op, PyObject *self, PyObject *args ) { TransactionHandleObject *trans_handle; boolean retaining; ISC_STATUS status_vector[STATUS_VECTOR_SIZE]; TransactionalOperationResult action_result; { PyObject *retaining_asObj; if ( !PyArg_ParseTuple( args, "O!O", &TransactionHandleType, &trans_handle, &retaining_asObj ) ) { return NULL; } retaining = (boolean) PyObject_IsTrue(retaining_asObj); } if (op == OP_COMMIT) { action_result = commit_transaction( trans_handle->native_handle, retaining, status_vector ); } else { /* op == OP_ROLLBACK */ action_result = rollback_transaction( trans_handle->native_handle, retaining, TRUE, status_vector ); } if (action_result != OP_RESULT_OK) { return NULL; } if (retaining) { assert (trans_handle->native_handle != NULL); } else { trans_handle->native_handle = NULL; } RETURN_PY_NONE; } /* _pyob_distributed_commit_or_rollback */ static PyObject *pyob_distributed_commit( PyObject *self, PyObject *args ) { return _pyob_distributed_commit_or_rollback(OP_COMMIT, self, args); } /* pyob_distributed_commit */ static PyObject *pyob_distributed_rollback( PyObject *self, PyObject *args ) { return _pyob_distributed_commit_or_rollback(OP_ROLLBACK, self, args); } /* pyob_distributed_rollback */ /************* PYTHON WRAPPERS FOR DISTRIBUTED TRANS OPS : end ****************/ /* 2003.02.17b: */ static PyObject *pyob_has_transaction( PyObject *self, PyObject *args ) { ConnectionObject *con; if ( !PyArg_ParseTuple( args, "O!", &ConnectionType, &con ) ) { return NULL; } return PyBool_FromLong(CON_GET_TRANS_HANDLE(con) != NULL); /* 2003.10.15a:OK */ } /* pyob_has_transaction */