mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
fix(roi): normalize scheduler timestamps to UTC
This commit is contained in:
parent
3cf6cd7a5c
commit
b3aacaf49d
4 changed files with 42 additions and 5 deletions
|
|
@ -549,7 +549,11 @@ def _next_update(settings: ROISettings, status: ROISyncStatus, report: ROIReport
|
|||
):
|
||||
return None
|
||||
anchor: Final = status.finished_at or status.started_at or report["synced_at"]
|
||||
return datetime.fromisoformat(anchor.replace("Z", "+00:00")) + timedelta(minutes=settings.update_interval_minutes)
|
||||
parsed: Final = datetime.fromisoformat(anchor.replace("Z", "+00:00"))
|
||||
utc_anchor: Final = (
|
||||
parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc)
|
||||
)
|
||||
return utc_anchor + timedelta(minutes=settings.update_interval_minutes)
|
||||
|
||||
|
||||
async def run_scheduled_sync() -> None:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from types import MappingProxyType
|
||||
from typing import Final, Protocol, cast # noqa: TID251 - PrismaWrapper dynamically delegates database methods
|
||||
|
||||
|
|
@ -117,7 +117,7 @@ class SyncStore:
|
|||
{
|
||||
"running": False,
|
||||
"phase": "error",
|
||||
"finished_at": rows[0].last_run_at.isoformat(),
|
||||
"finished_at": rows[0].last_run_at.replace(tzinfo=timezone.utc).isoformat(),
|
||||
"stage": "Sync interrupted",
|
||||
"error": "The worker stopped responding. Run analysis again to resume saved estimates.",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,18 @@ async def test_roi_cache_survives_scope_changes_and_uses_writer(monkeypatch: pyt
|
|||
) == ("roi_calculator_pull_new", "roi_calculator_pull_outside-window")
|
||||
assert not await store.acquire("scheduled", running, 1440)
|
||||
assert await store.acquire("manual", running)
|
||||
assert await store.finish("manual", complete, empty)
|
||||
write_rows(
|
||||
"UPDATE \"LiteLLM_Config\" SET last_run_at = NOW() - INTERVAL '2 minutes' WHERE param_name = %s",
|
||||
("roi_calculator_sync",),
|
||||
database_url=writer_url,
|
||||
)
|
||||
expired: Final = await store.status()
|
||||
assert expired is not None and expired.phase == "error" and expired.finished_at is not None
|
||||
assert datetime.fromisoformat(expired.finished_at).tzinfo == timezone.utc
|
||||
assert not await store.heartbeat("manual", running)
|
||||
assert await store.acquire("replacement", running)
|
||||
assert not await store.finish("manual", complete, empty)
|
||||
assert await store.finish("replacement", complete, empty)
|
||||
assert (
|
||||
len(
|
||||
read_rows(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
from collections.abc import Mapping
|
||||
from datetime import datetime, timezone
|
||||
from types import MappingProxyType
|
||||
from typing import Final, cast
|
||||
|
||||
|
|
@ -12,11 +13,13 @@ from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
|
|||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.management_endpoints.roi_calculator_endpoints import (
|
||||
_estimator_models_from_deployments,
|
||||
_next_update,
|
||||
get_roi_config_repository,
|
||||
router,
|
||||
)
|
||||
from litellm.proxy.roi_calculator.estimator import estimator_options
|
||||
from litellm.types.roi_calculator import ROISettings
|
||||
from litellm.proxy.roi_calculator.sample import sample_report
|
||||
from litellm.types.roi_calculator import ROISettings, ROISyncStatus
|
||||
|
||||
_JSON_HEADERS: Final = MappingProxyType({"content-type": "application/json"})
|
||||
|
||||
|
|
@ -187,3 +190,22 @@ def test_sample_preview_does_not_change_live_settings_or_report() -> None:
|
|||
def test_schedule_rejects_intervals_under_five_minutes(interval: float) -> None:
|
||||
client: Final = _client(LitellmUserRoles.PROXY_ADMIN, _ConfigRepository())
|
||||
assert client.put("/roi-calculator/settings", json={"update_interval_minutes": interval}).status_code == 422
|
||||
|
||||
|
||||
@pytest.mark.parametrize("anchor", ("2026-09-30T12:00:00", "2026-09-30T12:00:00Z", "2026-09-30T14:00:00+02:00"))
|
||||
def test_schedule_normalizes_legacy_and_offset_timestamps(anchor: str) -> None:
|
||||
settings: Final = ROISettings(repos=("example/repo",), estimator_model="estimator", update_interval_minutes=60)
|
||||
status: Final = ROISyncStatus(
|
||||
running=False,
|
||||
phase="error",
|
||||
stage="Interrupted",
|
||||
done=0,
|
||||
total=0,
|
||||
estimated=0,
|
||||
reused=0,
|
||||
needs_attention=0,
|
||||
error=None,
|
||||
finished_at=anchor,
|
||||
)
|
||||
report: Final = sample_report(datetime(2026, 9, 30, tzinfo=timezone.utc))
|
||||
assert _next_update(settings, status, report) == datetime(2026, 9, 30, 13, tzinfo=timezone.utc)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue