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..2775f07f 100644 --- a/tests/test_InfluxDBClientAuthorization.py +++ b/tests/test_InfluxDBClientAuthorization.py @@ -1,8 +1,10 @@ import unittest +from unittest.mock import Mock import httpretty from influxdb_client import InfluxDBClient +from influxdb_client._sync.api_client import ApiClient class InfluxDBClientAuthorization(unittest.TestCase): @@ -10,6 +12,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 +38,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 +50,49 @@ 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__() + + 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()