Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions UnleashClient/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,12 @@ def feature_definitions(self) -> dict:

def destroy(self) -> None:
"""
Gracefully shuts down the Unleash client by stopping jobs, stopping the scheduler, and deleting the cache.
Gracefully shuts down the Unleash client by stopping jobs and stopping
the scheduler.

For cache teardown:
- Default disk-backed FileCache instances are preserved on disk.
- Custom non-FileCache implementations will have destroy() called.

You shouldn't need this too much!
"""
Expand Down Expand Up @@ -487,10 +492,13 @@ def destroy(self) -> None:
except Exception as exc:
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

destroy()'s docstring mentions deleting the cache, but cache teardown is no longer performed here. Please update the docstring to match the new behavior (e.g., preserve FileCache on disk while optionally destroying custom caches).

Copilot uses AI. Check for mistakes.
LOGGER.warning("Exception during scheduler teardown: %s", exc)

Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UnleashClient.destroy() no longer calls self.cache.destroy(), which breaks the expected lifecycle contract for custom caches (and contradicts test_destroy_calls_custom_cache_destroy). Consider restoring cache teardown for non-FileCache caches (e.g., if self.cache and not isinstance(self.cache, FileCache): ...) so custom implementations can release resources while keeping FileCache on disk.

Suggested change
# Tear down custom caches while keeping FileCache contents on disk.
cache = getattr(self, "cache", None)
if cache and not isinstance(cache, FileCache):
try:
cache.destroy()
except Exception as exc:
LOGGER.warning("Exception during cache teardown: %s", exc)

Copilot uses AI. Check for mistakes.
try:
self.cache.destroy()
except Exception as exc:
LOGGER.warning("Exception during cache teardown: %s", exc)
# Disk-backed FileCache instances can be shared across processes.
# Avoid deleting them during shutdown to prevent cache races.
if not isinstance(self.cache, FileCache):
try:
self.cache.destroy()
except Exception as exc:
LOGGER.warning("Exception during cache teardown: %s", exc)

@staticmethod
def _get_fallback_value(
Expand Down
53 changes: 51 additions & 2 deletions tests/unit_tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
URL,
)
from UnleashClient import INSTANCES, UnleashClient
from UnleashClient.cache import FileCache
from UnleashClient.cache import BaseCache, FileCache
from UnleashClient.constants import FEATURES_URL, METRICS_URL, REGISTER_URL
from UnleashClient.events import BaseEvent, UnleashEvent, UnleashEventType
from UnleashClient.utils import InstanceAllowType
Expand Down Expand Up @@ -93,7 +93,8 @@ def before_each():

@pytest.fixture
def cache(tmpdir):
return FileCache(APP_NAME, directory=tmpdir.dirname)
# Keep cache isolated per test; client.destroy() no longer clears FileCache.
return FileCache(APP_NAME, directory=tmpdir.strpath)


@pytest.fixture()
Expand Down Expand Up @@ -1545,3 +1546,51 @@ def remove_all_jobs(self, *args, **kwargs):
unleash_client.destroy()

assert scheduler.shutdown_called == 1


def test_destroy_skips_default_file_cache_destroy(monkeypatch):
unleash_client = UnleashClient(
URL, APP_NAME, disable_metrics=True, disable_registration=True
)
destroy_calls = 0

def count_destroy():
nonlocal destroy_calls
destroy_calls += 1

monkeypatch.setattr(unleash_client.cache, "destroy", count_destroy)

unleash_client.destroy()

assert destroy_calls == 0


def test_destroy_calls_custom_cache_destroy():
class CustomCache(BaseCache):
def __init__(self):
self.destroy_calls = 0
self.store = {}

def set(self, key: str, value):
self.store[key] = value

def mset(self, data: dict):
self.store.update(data)

def get(self, key: str, default=None):
return self.store.get(key, default)

def exists(self, key: str):
return key in self.store

def destroy(self):
self.destroy_calls += 1

cache = CustomCache()
unleash_client = UnleashClient(
URL, APP_NAME, cache=cache, disable_metrics=True, disable_registration=True
)

unleash_client.destroy()

assert cache.destroy_calls == 1
Loading