From 42ff09bb73c6c145237790518820fe8193d46630 Mon Sep 17 00:00:00 2001 From: Abhinav Rastogi Date: Mon, 12 Oct 2026 09:06:20 +0530 Subject: [PATCH 1/2] Fix ApiClient finalizer during interpreter shutdown --- influxdb_client/_sync/api_client.py | 29 ++++++++++++++++++----- tests/test_InfluxDBClientAuthorization.py | 14 ++++++++++- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/influxdb_client/_sync/api_client.py b/influxdb_client/_sync/api_client.py index f61ef26c..7100fba2 100644 --- a/influxdb_client/_sync/api_client.py +++ b/influxdb_client/_sync/api_client.py @@ -81,13 +81,30 @@ def __init__(self, configuration=None, header_name=None, header_value=None, def __del__(self): """Dispose pools.""" - self._signout() - if self._pool: - self._pool.close() - self._pool.join() + # During interpreter shutdown, attributes and imported services can already be cleared. + # A finalizer must not let those teardown details escape as an ignored exception. + try: + if getattr(self, 'configuration', None) is not None: + self._signout() + except Exception: + pass + + pool = getattr(self, '_pool', None) + if pool: + try: + pool.close() + pool.join() + except Exception: + pass self._pool = None - if self.rest_client and self.rest_client.pool_manager and hasattr(self.rest_client.pool_manager, 'clear'): - self.rest_client.pool_manager.clear() + + rest_client = getattr(self, 'rest_client', None) + pool_manager = getattr(rest_client, 'pool_manager', None) + if hasattr(pool_manager, 'clear'): + try: + pool_manager.clear() + except Exception: + pass @property def pool(self): diff --git a/tests/test_InfluxDBClientAuthorization.py b/tests/test_InfluxDBClientAuthorization.py index f828748d..ab4e2390 100644 --- a/tests/test_InfluxDBClientAuthorization.py +++ b/tests/test_InfluxDBClientAuthorization.py @@ -3,6 +3,7 @@ import httpretty from influxdb_client import InfluxDBClient +from influxdb_client._sync.api_client import ApiClient class InfluxDBClientAuthorization(unittest.TestCase): @@ -10,6 +11,7 @@ class InfluxDBClientAuthorization(unittest.TestCase): def setUp(self) -> None: httpretty.enable() httpretty.reset() + self.influxdb_client = None def tearDown(self) -> None: if self.influxdb_client: @@ -35,7 +37,10 @@ def test_session_request(self): self.assertEqual(2, len(httpretty.httpretty.latest_requests)) # basic auth header - self.assertEqual('Basic bXktdXNlcm5hbWU6bXktcGFzc3dvcmQ=', httpretty.httpretty.latest_requests[0].headers['Authorization']) + self.assertEqual( + 'Basic bXktdXNlcm5hbWU6bXktcGFzc3dvcmQ=', + httpretty.httpretty.latest_requests[0].headers['Authorization'], + ) # cookie header self.assertEqual('session=xyz', httpretty.httpretty.latest_requests[1].headers['Cookie']) self.assertIsNotNone(self.influxdb_client.api_client.cookie) @@ -44,3 +49,10 @@ def test_session_request(self): self.influxdb_client.close() self.assertEqual(3, len(httpretty.httpretty.latest_requests)) + + def test_api_client_finalizer_ignores_partially_torn_down_state(self): + api_client = ApiClient() + del api_client.configuration + del api_client.rest_client + + api_client.__del__() From 4bc8d3455d860e5597e98b60fe1340b323ac5579 Mon Sep 17 00:00:00 2001 From: Abhinav Rastogi Date: Mon, 12 Oct 2026 12:27:12 +0530 Subject: [PATCH 2/2] Test finalizer cleanup after service failures --- tests/test_InfluxDBClientAuthorization.py | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/test_InfluxDBClientAuthorization.py b/tests/test_InfluxDBClientAuthorization.py index ab4e2390..2775f07f 100644 --- a/tests/test_InfluxDBClientAuthorization.py +++ b/tests/test_InfluxDBClientAuthorization.py @@ -1,4 +1,5 @@ import unittest +from unittest.mock import Mock import httpretty @@ -56,3 +57,42 @@ def test_api_client_finalizer_ignores_partially_torn_down_state(self): del api_client.rest_client api_client.__del__() + + def test_api_client_finalizer_cleans_pools_when_signout_fails(self): + api_client = ApiClient() + api_client._signout = Mock(side_effect=RuntimeError('session service unavailable')) + pool = api_client._pool = Mock() + manager = api_client.rest_client.pool_manager = Mock() + + api_client.__del__() + + api_client._signout.assert_called_once_with() + pool.close.assert_called_once_with() + pool.join.assert_called_once_with() + self.assertIsNone(api_client._pool) + manager.clear.assert_called_once_with() + + def test_api_client_finalizer_clears_http_pool_when_thread_pool_cleanup_fails(self): + for operation in ('close', 'join'): + with self.subTest(operation=operation): + api_client = ApiClient() + api_client._signout = Mock() + pool = api_client._pool = Mock() + getattr(pool, operation).side_effect = RuntimeError('pool already shut down') + manager = api_client.rest_client.pool_manager = Mock() + + api_client.__del__() + + getattr(pool, operation).assert_called_once_with() + self.assertIsNone(api_client._pool) + manager.clear.assert_called_once_with() + + def test_api_client_finalizer_ignores_http_pool_cleanup_failure(self): + api_client = ApiClient() + api_client._signout = Mock() + manager = api_client.rest_client.pool_manager = Mock() + manager.clear.side_effect = RuntimeError('HTTP pool already shut down') + + api_client.__del__() + + manager.clear.assert_called_once_with()