diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 7cb2954c10e..92d98606b23 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -7630,7 +7630,10 @@ class ProxyConfig: db_values: Mapping[str, SettingsJsonValue], previous_retention_values: tuple[SettingsJsonValue | None, ...], ) -> None: - if previous_retention_values != self._resolved_retention_values(): + resolved: Final = self._resolved_retention_values() + wants_job: Final = any(value is not None for value in resolved) + has_job: Final = scheduler is not None and scheduler.get_job("spend_log_cleanup_job") is not None + if previous_retention_values != resolved or wants_job != has_job: await self._reschedule_spend_log_cleanup_job() async def _apply_ssrf_settings(self, db_values: Mapping[str, SettingsJsonValue]) -> None: diff --git a/tests/test_litellm/proxy/proxy_server/test_proxy_config.py b/tests/test_litellm/proxy/proxy_server/test_proxy_config.py index 10e02634023..6d7b89dfd09 100644 --- a/tests/test_litellm/proxy/proxy_server/test_proxy_config.py +++ b/tests/test_litellm/proxy/proxy_server/test_proxy_config.py @@ -3808,6 +3808,7 @@ async def test_ProxyConfig__reschedule_spend_log_cleanup_job_daily_tag_spend_ret async def test_ProxyConfig__update_general_settings_updates_daily_tag_spend_retention(monkeypatch): settings = {} monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", settings) + monkeypatch.setattr("litellm.proxy.proxy_server.scheduler", None) pc = ProxyConfig() reschedule = AsyncMock() monkeypatch.setattr(pc, "_reschedule_spend_log_cleanup_job", reschedule) @@ -3818,6 +3819,22 @@ async def test_ProxyConfig__update_general_settings_updates_daily_tag_spend_rete reschedule.assert_awaited_once() +@pytest.mark.asyncio +async def test_ProxyConfig__update_general_settings_schedules_cleanup_when_db_row_was_already_applied(monkeypatch): + """A config reload applies the db row to the store before the side effects run, so the + before/after snapshot is equal; the job must still be scheduled when none is running.""" + fake_scheduler = MagicMock() + fake_scheduler.get_job.return_value = None + monkeypatch.setattr("litellm.proxy.proxy_server.scheduler", fake_scheduler) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None) + pc = ProxyConfig() + pc.settings.apply_db_row("general_settings", {"maximum_daily_tag_spend_retention_period": "90d"}) + monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", pc.settings) + await pc._update_general_settings({"maximum_daily_tag_spend_retention_period": "90d"}) + assert fake_scheduler.add_job.call_count == 1 + assert fake_scheduler.add_job.call_args.kwargs["id"] == "spend_log_cleanup_job" + + # --------------------------------------------------------------------------- # ProxyConfig._update_general_settings # --------------------------------------------------------------------------- @@ -3949,6 +3966,7 @@ async def test_ProxyConfig__update_general_settings_skips_redundant_retention_re pc = ProxyConfig() reschedule: Final = AsyncMock() monkeypatch.setattr(proxy_server, "general_settings", {}) + monkeypatch.setattr(proxy_server, "scheduler", MagicMock()) monkeypatch.setattr(pc, "_reschedule_spend_log_cleanup_job", reschedule) await pc._update_general_settings({"maximum_health_check_retention_period": "30d"})