99"""
1010
1111import asyncio
12+ import math
13+ import time
1214import uuid
13- from collections .abc import Awaitable , Mapping , Sequence
14- from dataclasses import dataclass
15+ from collections .abc import Awaitable , Callable , Mapping , Sequence
16+ from dataclasses import dataclass , field
1517from typing import TYPE_CHECKING , Annotated , Final , Literal , Protocol , TypeAlias
1618
1719from pydantic import BaseModel , ConfigDict , Field , TypeAdapter , ValidationError
@@ -85,6 +87,7 @@ class BatchEnqueuedTokenReservation:
8587 scopes : tuple [BatchEnqueuedTokenScope , ...]
8688 backend : ReservationBackend = "redis"
8789 owner : str = ""
90+ reserved_at_monotonic : float = field (default_factory = time .monotonic , compare = False )
8891
8992
9093@dataclass (frozen = True , slots = True )
@@ -180,11 +183,18 @@ class BatchEnqueuedTokenStore:
180183 granted them, and in-memory grants also remember the granting worker, so a
181184 refund never debits counters the grant did not charge. Everything expires after
182185 ``BATCH_ENQUEUED_TOKEN_TTL_SECONDS`` so a crash between submission and the
183- terminal-state refund can never leak tokens forever.
186+ terminal-state refund can never leak tokens forever, and reservation records
187+ expire no later than the counters they would refund, so a stale record can
188+ never debit an allowance re-granted after its counters expired.
184189 """
185190
186- def __init__ (self , internal_usage_cache : "InternalUsageCache" ) -> None :
191+ def __init__ (
192+ self ,
193+ internal_usage_cache : "InternalUsageCache" ,
194+ monotonic : Callable [[], float ] = time .monotonic ,
195+ ) -> None :
187196 self .internal_usage_cache = internal_usage_cache
197+ self ._monotonic : Final = monotonic
188198 self ._lock = asyncio .Lock ()
189199 self ._owner_token = uuid .uuid4 ().hex
190200 redis_cache = internal_usage_cache .dual_cache .redis_cache
@@ -235,6 +245,7 @@ async def _reserve_via_redis(
235245 tokens : int ,
236246 scopes : tuple [BatchEnqueuedTokenScope , ...],
237247 ) -> BatchEnqueuedTokenOutcome :
248+ started : Final = self ._monotonic ()
238249 for index , scope in enumerate (scopes ):
239250 result = await self ._run_reserve_script (
240251 reserve_script ,
@@ -246,7 +257,9 @@ async def _reserve_via_redis(
246257 if result [0 ] != 1 :
247258 await self ._rollback_partial_reserve (refund_script , tokens = tokens , scopes = scopes [:index ])
248259 return BatchEnqueuedTokenOverLimit (scope = scope , enqueued = result [1 ])
249- return BatchEnqueuedTokenReservation (tokens = tokens , scopes = scopes , backend = "redis" )
260+ return BatchEnqueuedTokenReservation (
261+ tokens = tokens , scopes = scopes , backend = "redis" , reserved_at_monotonic = started
262+ )
250263
251264 async def _run_reserve_script (
252265 self ,
@@ -295,14 +308,17 @@ async def _reserve_in_memory(
295308 scopes : tuple [BatchEnqueuedTokenScope , ...],
296309 span : "Span | None" ,
297310 ) -> BatchEnqueuedTokenOutcome :
311+ started : Final = self ._monotonic ()
298312 async with self ._lock :
299313 currents : Final = tuple ([await self ._get_local_counter (scope , span ) for scope in scopes ])
300314 for scope , current in zip (scopes , currents ):
301315 if current + tokens > scope .limit :
302316 return BatchEnqueuedTokenOverLimit (scope = scope , enqueued = current )
303317 for scope , current in zip (scopes , currents ):
304318 await self ._set_local_counter (scope , current + tokens , span )
305- return BatchEnqueuedTokenReservation (tokens = tokens , scopes = scopes , backend = "memory" , owner = self ._owner_token )
319+ return BatchEnqueuedTokenReservation (
320+ tokens = tokens , scopes = scopes , backend = "memory" , owner = self ._owner_token , reserved_at_monotonic = started
321+ )
306322
307323 async def refund (
308324 self ,
@@ -349,11 +365,13 @@ async def save_reservation(
349365 litellm_parent_otel_span : "Span | None" = None ,
350366 ) -> None :
351367 serialized : Final = _RESERVATION_ADAPTER .dump_json (reservation ).decode ("utf-8" )
368+ elapsed : Final = self ._monotonic () - reservation .reserved_at_monotonic
369+ ttl : Final = max (1 , BATCH_ENQUEUED_TOKEN_TTL_SECONDS - math .ceil (elapsed ))
352370 if self ._save_script is not None :
353371 try :
354372 await self ._save_script (
355373 (self ._record_key (batch_id ),),
356- (serialized , BATCH_ENQUEUED_TOKEN_TTL_SECONDS ),
374+ (serialized , ttl ),
357375 )
358376 except Exception as e : # noqa: BLE001 # any Redis failure must fall back to the in-memory record
359377 verbose_proxy_logger .warning (
@@ -364,7 +382,7 @@ async def save_reservation(
364382 await self .internal_usage_cache .async_set_cache (
365383 key = self ._record_key (batch_id ),
366384 value = serialized ,
367- ttl = BATCH_ENQUEUED_TOKEN_TTL_SECONDS ,
385+ ttl = ttl ,
368386 litellm_parent_otel_span = litellm_parent_otel_span ,
369387 local_only = True ,
370388 )
0 commit comments