Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 117 additions & 4 deletions app/api/pricing/cache.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import json
from datetime import datetime, timedelta

from cachetools import TTLCache

from app.api.common.models import CoinType
from app.core.cache import Cache

from .models import (
BatchTokenPriceRequests,
CacheStatus,
CoingeckoPlatform,
TokenPriceRequest,
TokenPriceResponse,
CacheStatus,
BatchTokenPriceRequests,
VsCurrency,
)

Expand All @@ -32,7 +36,7 @@ async def get(
# Generate cache keys for all tokens
cache_keys = [
cls._get_cache_key(param=request, vs_currency=batch.vs_currency)
for request in batch
for request in batch.requests
]

async with Cache.get_client() as redis:
Expand All @@ -41,7 +45,7 @@ async def get(
cached_responses: list[TokenPriceResponse] = []

# Process results
for request, cached_value in zip(batch, cached_values):
for request, cached_value in zip(batch.requests, cached_values):
if cached_value:
data = json.loads(cached_value)
cached_responses.append(
Expand Down Expand Up @@ -95,3 +99,112 @@ def _get_cache_key(
return f"{cls.CACHE_PREFIX}:{param.coin_type.value.lower()}:{param.chain_id.value}:{param.address.lower()}:{vs_currency.value.lower()}"

return f"{cls.CACHE_PREFIX}:{param.coin_type.value.lower()}:{param.chain_id.value}:{vs_currency.value.lower()}"


class PlatformMapCache:
"""Two-level cache for CoinGecko platform mapping data.

Level 1: Memory cache (TTLCache)
- 1-minute TTL, cleared on restart
- First line of defense to reduce Redis load

Level 2: Redis cache
- 1-day TTL, persistent across restarts
- Source of truth, populates memory cache on miss

Both caches are updated on set() and memory cache is populated from Redis on miss.
"""

CACHE_KEY = "coingecko:platform_map"
REDIS_TTL = timedelta(days=1)

MEMCACHE_KEY = "platform_map"
MEMCACHE_TTL = timedelta(hours=6)
memcache = TTLCache(maxsize=1, ttl=MEMCACHE_TTL, timer=datetime.now)

@classmethod
async def get(cls) -> dict[str, CoingeckoPlatform] | None:
# Check memory cache first
if cls.MEMCACHE_KEY in cls.memcache:
return cls.memcache[cls.MEMCACHE_KEY]

# If memory cache is empty or expired, try Redis
async with Cache.get_client() as redis:
data_json = await redis.get(cls.CACHE_KEY)
if not data_json:
return None

data = json.loads(data_json)
platform_map = {
platform_id: CoingeckoPlatform.model_validate(data)
for platform_id, data in data.items()
}

# Update memcache
cls.memcache[cls.MEMCACHE_KEY] = platform_map
return platform_map

@classmethod
async def set(
cls, platform_map: dict[str, CoingeckoPlatform], ttl: timedelta = REDIS_TTL
) -> None:
# Update memcache
cls.memcache[cls.MEMCACHE_KEY] = platform_map

# Update Redis cache
async with Cache.get_client() as redis:
data = {
platform_id: data.model_dump()
for platform_id, data in platform_map.items()
}
await redis.setex(cls.CACHE_KEY, ttl, json.dumps(data))


class CoinMapCache:
"""Two-level cache for CoinGecko coin mapping data.

Level 1: Memory cache (TTLCache)
- 5-minute TTL, cleared on restart
- First line of defense to reduce Redis load

Level 2: Redis cache
- 1-day TTL, persistent across restarts
- Source of truth, populates memory cache on miss

Both caches are updated on set() and memory cache is populated from Redis on miss.
"""

CACHE_KEY = "coingecko:coin_map"
REDIS_TTL = timedelta(days=1)

MEMCACHE_KEY = "coin_map"
MEMCACHE_TTL = timedelta(hours=6)
memcache = TTLCache(maxsize=1, ttl=MEMCACHE_TTL, timer=datetime.now)

@classmethod
async def get(cls) -> dict[str, dict[str, str]] | None:
# Check memory cache first
if cls.MEMCACHE_KEY in cls.memcache:
return cls.memcache[cls.MEMCACHE_KEY]

# If memory cache is empty or expired, try Redis
async with Cache.get_client() as redis:
data = await redis.get(cls.CACHE_KEY)
if not data:
return None

coin_map = json.loads(data)
# Update memory cache
cls.memcache[cls.MEMCACHE_KEY] = coin_map
return coin_map

@classmethod
async def set(
cls, coin_map: dict[str, dict[str, str]], ttl: timedelta = REDIS_TTL
) -> None:
# Update memcache
cls.memcache[cls.MEMCACHE_KEY] = coin_map

# Update Redis cache
async with Cache.get_client() as redis:
await redis.setex(cls.CACHE_KEY, ttl, json.dumps(coin_map))
Loading