fix(proxy): cap the opt-in startup cleanup delay at the configured retention interval
Some checks failed
LiteLLM Rust / rust-lint (push) Has been cancelled
LiteLLM Rust / rust-test (push) Has been cancelled
LiteLLM Rust / rust-wheel (push) Has been cancelled
Terraform Modules / fmt, validate, test (aws) (push) Has been cancelled
Terraform Modules / fmt, validate, test (gcp) (push) Has been cancelled
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
jesus 2026-10-12 20:58:26 +00:00
parent 69f30e26c2
commit 5f7f66d50b
3 changed files with 55 additions and 2 deletions

View file

@ -108,16 +108,24 @@ def first_cleanup_run_kwargs(settings: Mapping[str, object], now: datetime) -> F
"""
Extra ``add_job`` kwargs for the interval cleanup job. Empty unless
``maximum_spend_logs_cleanup_run_on_startup`` is true, so the first run otherwise stays one interval out.
The startup delay is capped at the configured interval so opting in never makes the first run later.
"""
if (
settings.get("maximum_spend_logs_cleanup_cron")
or settings.get("maximum_spend_logs_cleanup_run_on_startup") is not True
):
return {}
delay_seconds: Final = SPEND_LOG_CLEANUP_STARTUP_DELAY_SECONDS + random.randint(
retention_interval: Final = settings.get("maximum_spend_logs_retention_interval", "1d")
if not isinstance(retention_interval, str):
return {}
try:
interval_seconds: Final = duration_in_seconds(retention_interval)
except (ValueError, OverflowError):
return {}
startup_delay_seconds: Final = SPEND_LOG_CLEANUP_STARTUP_DELAY_SECONDS + random.randint(
0, SPEND_LOG_CLEANUP_STARTUP_JITTER_SECONDS
)
return {"next_run_time": now + timedelta(seconds=delay_seconds)}
return {"next_run_time": now + timedelta(seconds=min(startup_delay_seconds, interval_seconds))}
class SpendLogCleanup:

View file

@ -4259,6 +4259,28 @@ async def test_ProxyConfig__reschedule_spend_log_cleanup_job_first_run_only_move
real_scheduler.shutdown(wait=False)
@pytest.mark.asyncio
async def test_ProxyConfig__reschedule_spend_log_cleanup_job_opt_in_never_delays_a_short_interval(
monkeypatch: pytest.MonkeyPatch,
):
real_scheduler: Final = _paused_scheduler(monkeypatch)
monkeypatch.setattr(
"litellm.proxy.proxy_server.general_settings",
{
"maximum_health_check_retention_period": "90d",
"maximum_spend_logs_retention_interval": "30s",
"maximum_spend_logs_cleanup_run_on_startup": True,
},
)
before: Final = datetime.now(timezone.utc)
try:
await ProxyConfig()._reschedule_spend_log_cleanup_job()
delay: Final = (real_scheduler.get_job("spend_log_cleanup_job").next_run_time - before).total_seconds()
assert delay <= 30 + 1, delay
finally:
real_scheduler.shutdown(wait=False)
def _scheduler_whose_first_add_job_raises(monkeypatch):
from apscheduler.schedulers.asyncio import AsyncIOScheduler

View file

@ -61,6 +61,29 @@ def test_first_cleanup_run_kwargs_runs_soon_after_startup_when_opted_in():
assert len(delays) > 1
@pytest.mark.parametrize("interval, interval_seconds", [("30s", 30), ("2m", 120)])
def test_first_cleanup_run_kwargs_never_runs_later_than_a_short_interval(interval: str, interval_seconds: int):
now: Final = datetime(2026, 1, 1, tzinfo=timezone.utc)
settings: Final = {
"maximum_spend_logs_cleanup_run_on_startup": True,
"maximum_spend_logs_retention_interval": interval,
}
delays: Final = {
(first_cleanup_run_kwargs(settings, now)["next_run_time"] - now).total_seconds() for _ in range(200)
}
assert max(delays) <= interval_seconds, delays
assert min(delays) >= min(SPEND_LOG_CLEANUP_STARTUP_DELAY_SECONDS, interval_seconds), delays
@pytest.mark.parametrize("interval", ["not-a-duration", 3600])
def test_first_cleanup_run_kwargs_leaves_an_invalid_interval_to_the_caller(interval: object):
settings: Final = {
"maximum_spend_logs_cleanup_run_on_startup": True,
"maximum_spend_logs_retention_interval": interval,
}
assert dict(first_cleanup_run_kwargs(settings, datetime(2026, 1, 1, tzinfo=timezone.utc))) == {}
def _far_deadline() -> float:
"""A run deadline far enough out that only the other bounds can stop a batch loop."""
return time.monotonic() + 3600