Merge pull request #13933 from michal-otmianowski/ignore-expired-s3-cache-entries

Verify if cache entry has expired prior to serving it to client
This commit is contained in:
Krish Dholakia 2025-08-25 23:10:48 -07:00 committed by GitHub
commit 22934907ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 58 additions and 5 deletions

View file

@ -13,6 +13,7 @@ import asyncio
import json
from functools import partial
from typing import Optional
from datetime import datetime, timezone, timedelta
from litellm._logging import print_verbose, verbose_logger
@ -69,11 +70,9 @@ class S3Cache(BaseCache):
if ttl is not None:
cache_control = f"immutable, max-age={ttl}, s-maxage={ttl}"
import datetime
# Calculate expiration time
expiration_time = datetime.datetime.now() + datetime.timedelta(seconds=ttl)
expiration_time = datetime.now(timezone.utc) + timedelta(seconds=ttl)
# Upload the data to S3 with the calculated expiration time
self.s3_client.put_object(
Bucket=self.bucket_name,
@ -126,6 +125,13 @@ class S3Cache(BaseCache):
)
if cached_response is not None:
if "Expires" in cached_response:
expires_time = cached_response['Expires']
current_time = datetime.now(expires_time.tzinfo)
if current_time > expires_time:
return None
# cached_response is in `b{} convert it to ModelResponse
cached_response = (
cached_response["Body"].read().decode("utf-8")

View file

@ -56,7 +56,7 @@ def test_s3_cache_set_cache_with_ttl(mock_s3_dependencies):
assert "max-age=3600" in call_args[1]["CacheControl"]
def test_s3_cache_get_cache(mock_s3_dependencies):
def test_s3_cache_get_cache_no_expires_info_in_response(mock_s3_dependencies):
"""Test basic get_cache functionality"""
cache = S3Cache("test-bucket")
@ -75,6 +75,54 @@ def test_s3_cache_get_cache(mock_s3_dependencies):
assert result == {"key": "value", "number": 42}
def test_s3_cache_get_cache_with_expires_valid(mock_s3_dependencies):
"""Test get_cache when response contains Expires and cache entry is still valid"""
cache = S3Cache("test-bucket")
# Create a future expiration time (1 hour from now)
future_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=1)
mock_response = {
"Body": MagicMock(),
"Expires": future_time
}
mock_response["Body"].read.return_value = b'{"key": "value", "number": 42}'
cache.s3_client.get_object.return_value = mock_response
result = cache.get_cache("test_key")
cache.s3_client.get_object.assert_called_once_with(
Bucket="test-bucket",
Key="test_key"
)
# Should return the cached value since it's not expired
assert result == {"key": "value", "number": 42}
def test_s3_cache_get_cache_with_expires_expired(mock_s3_dependencies):
"""Test get_cache when response contains Expires and cache entry is no longer valid"""
cache = S3Cache("test-bucket")
# Create a past expiration time (1 hour ago)
past_time = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(hours=1)
mock_response = {
"Body": MagicMock(),
"Expires": past_time
}
mock_response["Body"].read.return_value = b'{"key": "value", "number": 42}'
cache.s3_client.get_object.return_value = mock_response
result = cache.get_cache("test_key")
cache.s3_client.get_object.assert_called_once_with(
Bucket="test-bucket",
Key="test_key"
)
# Should return None since the cache entry is expired
assert result is None
def test_s3_cache_get_cache_not_found(mock_s3_dependencies):
"""Test get_cache when key is not found"""
@ -126,7 +174,6 @@ def test_s3_cache_initialization():
cache_with_path = S3Cache("test-bucket", s3_path="my/cache/path")
assert cache_with_path.key_prefix == "my/cache/path/"
# ============================================================================
# ASYNC TESTS
# ============================================================================