diff --git a/ibm_db.c b/ibm_db.c index 3e4fcda5..61cddbd3 100644 --- a/ibm_db.c +++ b/ibm_db.c @@ -1921,6 +1921,8 @@ static PyObject *_python_ibm_db_connect_helper(PyObject *self, PyObject *args, i char server[2048]; int isNewBuffer = 0; PyObject *pid = NULL; + PyObject *pidStr = NULL; + PyObject *threadIdStr = NULL; conn_alive = 1; if (!PyArg_ParseTuple(args, "OOO|OO", &databaseObj, &uidObj, &passwordObj, &options, &literal_replacementObj)) { @@ -1977,9 +1979,24 @@ static PyObject *_python_ibm_db_connect_helper(PyObject *self, PyObject *args, i } snprintf(messageStr, sizeof(messageStr), "Obtain process id: %p", pid); LogMsg(INFO, messageStr); - hKey = PyUnicode_Concat(hKey, PyUnicode_FromFormat("%ld", PyLong_AsLong(pid))); + pidStr = PyUnicode_FromFormat("%ld", PyLong_AsLong(pid)); + if (pidStr == NULL) + { + Py_DECREF(pid); + return NULL; + } + hKey = PyUnicode_Concat(hKey, pidStr); + Py_DECREF(pidStr); Py_DECREF(pid); + threadIdStr = PyUnicode_FromFormat("%ld", (long)PyThread_get_thread_ident()); + if (threadIdStr == NULL) + { + return NULL; + } + hKey = PyUnicode_Concat(hKey, threadIdStr); + Py_DECREF(threadIdStr); + entry = PyDict_GetItem(persistent_list, hKey); if (entry != NULL) @@ -4274,7 +4291,8 @@ static PyObject *ibm_db_bind_param(PyObject *self, PyObject *args) * Specifies an active DB2 client connection. * * ===Return Values - * Returns TRUE on success or FALSE on failure. + * Returns TRUE on success. If the connection is already closed, returns TRUE. + * On failure, an exception is raised and NULL is returned. */ static PyObject *ibm_db_close(PyObject *self, PyObject *args) { @@ -4314,9 +4332,10 @@ static PyObject *ibm_db_close(PyObject *self, PyObject *args) if (!conn_res->handle_active) { - LogMsg(EXCEPTION, "Connection is not active"); - PyErr_SetString(PyExc_Exception, "Connection is not active"); - return NULL; + LogMsg(INFO, "Connection already closed; no action required"); + Py_INCREF(Py_True); + LogMsg(INFO, "exit close()"); + return Py_True; } if (conn_res->handle_active && !conn_res->flag_pconnect) diff --git a/ibm_db_dbi.py b/ibm_db_dbi.py index ece68a6d..ee7e43f6 100644 --- a/ibm_db_dbi.py +++ b/ibm_db_dbi.py @@ -923,20 +923,22 @@ def close(self): """This method closes the Database connection associated with the Connection object. It takes no arguments. + If the connection is already closed, this method is a no-op and + returns True. + """ LogMsg(INFO, "entry close()") - self.rollback() - try: - if self.conn_handler is None: - LogMsg(ERROR, "Connection cannot be closed; connection is no longer active.") - raise ProgrammingError("Connection cannot be closed; " - "connection is no longer active.") - else: + if self.conn_handler is None: + return_value = True + LogMsg(INFO, "Connection already closed; no action required.") + else: + self.rollback() + try: return_value = ibm_db.close(self.conn_handler) LogMsg(INFO, "Connection closed.") - except Exception as inst: - LogMsg(EXCEPTION, f"An exception occurred while closing connection: {inst}") - raise _get_exception(inst) + except Exception as inst: + LogMsg(EXCEPTION, f"An exception occurred while closing connection: {inst}") + raise _get_exception(inst) self.conn_handler = None for index in range(len(self._cursor_list)): if (self._cursor_list[index]() != None): diff --git a/ibm_db_tests/test_072_CloseTwice_DBI.py b/ibm_db_tests/test_072_CloseTwice_DBI.py new file mode 100644 index 00000000..0f8bc254 --- /dev/null +++ b/ibm_db_tests/test_072_CloseTwice_DBI.py @@ -0,0 +1,42 @@ +# +# Licensed Materials - Property of IBM +# +# (c) Copyright IBM Corp. 2026 +# + +from __future__ import print_function +import sys +import unittest +import ibm_db_dbi +import config +from testfunctions import IbmDbTestFunctions + + +class IbmDbTestCase(unittest.TestCase): + + def test_072_CloseTwice_DBI(self): + obj = IbmDbTestFunctions() + obj.assert_expect(self.run_test_072) + + def run_test_072(self): + conn = ibm_db_dbi.connect(config.database, config.user, config.password) + + rc1 = conn.close() + rc2 = conn.close() + + print(rc1) + print(rc2) + +#__END__ +#__LUW_EXPECTED__ +#True +#True +#__ZOS_EXPECTED__ +#True +#True +#__SYSTEMI_EXPECTED__ +#True +#True +#__IDS_EXPECTED__ +#True +#True diff --git a/ibm_db_tests/test_6793_PconnectThreadIsolation.py b/ibm_db_tests/test_6793_PconnectThreadIsolation.py new file mode 100644 index 00000000..ec1528e7 --- /dev/null +++ b/ibm_db_tests/test_6793_PconnectThreadIsolation.py @@ -0,0 +1,90 @@ +# +# Licensed Materials - Property of IBM +# +# (c) Copyright IBM Corp. 2007-2008 +# + +from __future__ import print_function +import sys +import threading +import unittest +import ibm_db +import config +from testfunctions import IbmDbTestFunctions + + +class IbmDbTestCase(unittest.TestCase): + + def test_6793_PconnectThreadIsolation(self): + obj = IbmDbTestFunctions() + obj.assert_expect(self.run_test_6793) + + def run_test_6793(self): + start_event = threading.Event() + lock = threading.Lock() + results = [] + errors = [] + + def worker(): + try: + start_event.wait() + + if sys.platform == 'zos': + conn = ibm_db.pconnect(config.database, '', '') + else: + conn = ibm_db.pconnect(config.database, config.user, config.password) + + if not conn: + with lock: + errors.append("connect_failed") + return + + is_active = ibm_db.active(conn) + conn_id = id(conn) + ibm_db.close(conn) + + with lock: + results.append((conn_id, is_active)) + except Exception: + with lock: + errors.append("worker_exception") + + threads = [threading.Thread(target=worker) for _ in range(2)] + + for t in threads: + t.start() + + start_event.set() + + for t in threads: + t.join() + + if errors: + print("errors:", len(errors)) + return + + unique_conn_handles = len(set([conn_id for conn_id, _ in results])) + active_count = sum(1 for _, is_active in results if is_active) + + print("workers:", len(results)) + print("active_count:", active_count) + print("unique_conn_handles:", unique_conn_handles) + + +#__END__ +#__LUW_EXPECTED__ +#workers: 2 +#active_count: 2 +#unique_conn_handles: 2 +#__ZOS_EXPECTED__ +#workers: 2 +#active_count: 2 +#unique_conn_handles: 2 +#__SYSTEMI_EXPECTED__ +#workers: 2 +#active_count: 2 +#unique_conn_handles: 2 +#__IDS_EXPECTED__ +#workers: 2 +#active_count: 2 +#unique_conn_handles: 2