cosmo_backend/app/services/cache_preheat.py

241 lines
9.5 KiB
Python
Raw Permalink Normal View History

2025-12-02 06:29:38 +00:00
"""
Cache preheating service
Loads data from database to Redis on startup
"""
import logging
from datetime import datetime, timedelta
from typing import List, Dict, Any
from app.database import get_db
from app.services.redis_cache import redis_cache, make_cache_key, get_ttl_seconds
from app.services.db_service import celestial_body_service, position_service
logger = logging.getLogger(__name__)
async def preheat_current_positions():
"""
Preheat current positions from database to Redis
Loads the most recent single-point position for all bodies
Strategy: Get the latest position for each body (should be current hour or most recent)
"""
logger.info("=" * 60)
logger.info("Starting cache preheat: Current positions")
logger.info("=" * 60)
try:
async for db in get_db():
# Get all celestial bodies
all_bodies = await celestial_body_service.get_all_bodies(db)
logger.info(f"Found {len(all_bodies)} celestial bodies")
# Get current time rounded to the hour
now = datetime.utcnow()
current_hour = now.replace(minute=0, second=0, microsecond=0)
# Define time window: current hour ± 1 hour
start_window = current_hour - timedelta(hours=1)
end_window = current_hour + timedelta(hours=1)
# Collect positions for all bodies
bodies_data = []
successful_bodies = 0
for body in all_bodies:
try:
# Get position closest to current hour
recent_positions = await position_service.get_positions(
body_id=body.id,
start_time=start_window,
end_time=end_window,
session=db
)
if recent_positions and len(recent_positions) > 0:
# Use the position closest to current hour
# Find the one with time closest to current_hour
closest_pos = min(
recent_positions,
key=lambda p: abs((p.time - current_hour).total_seconds())
)
body_dict = {
"id": body.id,
"name": body.name,
"name_zh": body.name_zh,
"type": body.type,
"description": body.description,
"positions": [{
"time": closest_pos.time.isoformat(),
"x": closest_pos.x,
"y": closest_pos.y,
"z": closest_pos.z,
}]
}
bodies_data.append(body_dict)
successful_bodies += 1
logger.debug(f" ✓ Loaded position for {body.name} at {closest_pos.time}")
else:
logger.warning(f" ⚠ No position found for {body.name} near {current_hour}")
except Exception as e:
logger.warning(f" ✗ Failed to load position for {body.name}: {e}")
continue
# Write to Redis if we have data
if bodies_data:
# Cache key for current hour
time_str = current_hour.isoformat()
redis_key = make_cache_key("positions", time_str, time_str, "1h")
ttl = get_ttl_seconds("current_positions")
success = await redis_cache.set(redis_key, bodies_data, ttl)
if success:
logger.info(f"✅ Preheated current positions: {successful_bodies}/{len(all_bodies)} bodies")
logger.info(f" Time: {current_hour}")
logger.info(f" Redis key: {redis_key}")
logger.info(f" TTL: {ttl}s ({ttl // 3600}h)")
else:
logger.error("❌ Failed to write to Redis")
else:
logger.warning("⚠ No position data available to preheat")
break # Only process first database session
except Exception as e:
logger.error(f"❌ Cache preheat failed: {e}")
import traceback
traceback.print_exc()
logger.info("=" * 60)
async def preheat_historical_positions(days: int = 3):
"""
Preheat historical positions for timeline mode
Strategy: For each day, cache the position at 00:00:00 UTC (single point per day)
Args:
days: Number of days to preheat (default: 3)
"""
logger.info("=" * 60)
logger.info(f"Starting cache preheat: Historical positions ({days} days)")
logger.info("=" * 60)
try:
async for db in get_db():
# Get all celestial bodies
all_bodies = await celestial_body_service.get_all_bodies(db)
logger.info(f"Found {len(all_bodies)} celestial bodies")
# Define time window
end_date = datetime.utcnow()
start_date = end_date - timedelta(days=days)
logger.info(f"Time range: {start_date.date()} to {end_date.date()}")
# Preheat each day separately (single point at 00:00:00 per day)
cached_days = 0
for day_offset in range(days):
# Target time: midnight (00:00:00) of this day
target_day = start_date + timedelta(days=day_offset)
target_midnight = target_day.replace(hour=0, minute=0, second=0, microsecond=0)
# Search window: ±30 minutes around midnight
search_start = target_midnight - timedelta(minutes=30)
search_end = target_midnight + timedelta(minutes=30)
# Collect positions for all bodies for this specific time
bodies_data = []
successful_bodies = 0
for body in all_bodies:
try:
# Query positions near midnight of this day
positions = await position_service.get_positions(
body_id=body.id,
start_time=search_start,
end_time=search_end,
session=db
)
if positions and len(positions) > 0:
# Find the position closest to midnight
closest_pos = min(
positions,
key=lambda p: abs((p.time - target_midnight).total_seconds())
)
body_dict = {
"id": body.id,
"name": body.name,
"name_zh": body.name_zh,
"type": body.type,
"description": body.description,
"positions": [
{
"time": closest_pos.time.isoformat(),
"x": closest_pos.x,
"y": closest_pos.y,
"z": closest_pos.z,
}
]
}
bodies_data.append(body_dict)
successful_bodies += 1
except Exception as e:
logger.warning(f" ✗ Failed to load {body.name} for {target_midnight.date()}: {e}")
continue
# Write to Redis if we have complete data
if bodies_data and successful_bodies == len(all_bodies):
# Cache key for this specific midnight timestamp
time_str = target_midnight.isoformat()
redis_key = make_cache_key("positions", time_str, time_str, "1d")
ttl = get_ttl_seconds("historical_positions")
success = await redis_cache.set(redis_key, bodies_data, ttl)
if success:
cached_days += 1
logger.info(f" ✓ Cached {target_midnight.date()} 00:00 UTC: {successful_bodies} bodies")
else:
logger.warning(f" ✗ Failed to cache {target_midnight.date()}")
else:
logger.warning(f" ⚠ Incomplete data for {target_midnight.date()}: {successful_bodies}/{len(all_bodies)} bodies")
logger.info(f"✅ Preheated {cached_days}/{days} days of historical data")
break # Only process first database session
except Exception as e:
logger.error(f"❌ Historical cache preheat failed: {e}")
import traceback
traceback.print_exc()
logger.info("=" * 60)
async def preheat_all_caches():
"""
Preheat all caches on startup
Priority:
1. Current positions (most important)
2. Historical positions for timeline (3 days)
"""
logger.info("")
logger.info("🔥 Starting full cache preheat...")
logger.info("")
# 1. Preheat current positions
await preheat_current_positions()
# 2. Preheat historical positions (3 days)
await preheat_historical_positions(days=3)
logger.info("")
logger.info("🔥 Cache preheat completed!")
logger.info("")