Skip to content

Commit

Permalink
Merge pull request #217 from Krukov/fix/etag-for-early-cache
Browse files Browse the repository at this point in the history
fix: use early ttl with early cache in etag middleware
  • Loading branch information
Krukov authored May 5, 2024
2 parents bf31f06 + ed3b749 commit 6ddbaae
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 10 deletions.
33 changes: 23 additions & 10 deletions cashews/contrib/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import contextlib
from contextlib import nullcontext
from contextvars import ContextVar
from datetime import datetime
from hashlib import blake2s
from typing import Any, ContextManager, Sequence

Expand Down Expand Up @@ -130,7 +131,7 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
if etag and await self._cache.exists(etag):
return Response(status_code=304)

set_key = None
set_key: None | str = None

def set_callback(key: str, result: Any):
nonlocal set_key
Expand All @@ -140,28 +141,40 @@ def set_callback(key: str, result: Any):
response = await call_next(request)
calls = detector.calls_list
if not calls:
if set_key:
etag = await self._get_etag(set_key)
if etag:
response.headers[_ETAG_HEADER] = etag
if set_key is not None:
_etag = await self._get_etag(set_key)
if _etag == etag:
return Response(status_code=304)
if _etag:
response.headers[_ETAG_HEADER] = _etag
return response

key, _ = calls[0]
etag = await self._get_etag(key)
if etag:
response.headers[_ETAG_HEADER] = etag
_etag = await self._get_etag(key)
if _etag == etag:
return Response(status_code=304)
if _etag:
response.headers[_ETAG_HEADER] = _etag
return response

async def _get_etag(self, key: str) -> str:
data = await self._cache.get_raw(key)
expire = await self._cache.get_expire(key)
data = await self._cache.get(key)
if _is_early_cache(data):
expire = (data[0] - datetime.utcnow()).total_seconds() # type: ignore[index]
data = data[1] # type: ignore[index]
else:
expire = await self._cache.get_expire(key)
if not isinstance(data, bytes):
data = data.body if isinstance(data, Response) else DEFAULT_PICKLER.dumps(data)
etag = blake2s(data).hexdigest()
await self._cache.set(etag, True, expire=expire)
return etag


def _is_early_cache(data: Any) -> bool:
return isinstance(data, list) and isinstance(data[0], datetime)


class CacheDeleteMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
if request.headers.get(_CLEAR_CACHE_HEADER) == _CLEAR_CACHE_HEADER_VALUE:
Expand Down
21 changes: 21 additions & 0 deletions tests/test_intergations/test_fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,27 @@ async def rand():
assert etag == response3.headers["ETag"]


def test_cache_etag_early(client_with_middleware, app, cache):
from cashews.contrib.fastapi import CacheEtagMiddleware

@app.get("/to_cache")
@cache.early(ttl="10s", early_ttl="7s", key="to_cache")
async def rand():
return str(random()).encode()

with client_with_middleware(CacheEtagMiddleware, cache_instance=cache) as client:
response = client.get("/to_cache")
etag = response.headers["ETag"]

response2 = client.get("/to_cache", headers={"If-None-Match": etag})
assert response2.status_code == 304

response3 = client.get("/to_cache", headers={"If-None-Match": str(random())})
assert response3.status_code == 200
assert response.content == response3.content
assert etag == response3.headers["ETag"]


@pytest.fixture(name="app_with_cache_control")
def _app_with_cache_control(cache, app):
from cashews.contrib.fastapi import CacheRequestControlMiddleware, cache_control_ttl
Expand Down

0 comments on commit 6ddbaae

Please sign in to comment.