From 7d2e03cb40f93035e0ac2345fd461ab8d1edbf3d Mon Sep 17 00:00:00 2001 From: "Samuele E. Locatelli" Date: Wed, 3 Sep 2025 07:19:53 +0000 Subject: [PATCH] Update backend con TTL e sliding --- backend/api/v1/sessions.py | 35 ++++++++++-- backend/services/redis_service.py | 93 +++++++++++++++---------------- 2 files changed, 77 insertions(+), 51 deletions(-) diff --git a/backend/api/v1/sessions.py b/backend/api/v1/sessions.py index 62c88e8..ddc45de 100644 --- a/backend/api/v1/sessions.py +++ b/backend/api/v1/sessions.py @@ -20,6 +20,7 @@ async def create_session_endpoint( meta = redis_service.create_session(user_id, first_message) return meta + @router.get("/sessions", response_model=List[dict]) async def list_sessions_endpoint( user_id: str = Query(..., description="User ID") @@ -29,6 +30,7 @@ async def list_sessions_endpoint( """ return redis_service.get_sessions(user_id) + @router.get("/sessions/{session_id}", response_model=dict) async def get_session_meta_endpoint( user_id: str = Query(..., description="User ID"), @@ -39,6 +41,7 @@ async def get_session_meta_endpoint( """ return redis_service.get_session_meta(user_id, session_id) or {} + @router.patch("/sessions/{session_id}", response_model=dict) async def update_session_endpoint( user_id: str = Query(..., description="User ID"), @@ -50,6 +53,7 @@ async def update_session_endpoint( """ return redis_service.update_session_meta(user_id, session_id, session_name=session_name) or {} + @router.delete("/sessions/{session_id}", response_model=dict) async def delete_session_endpoint( user_id: str = Query(..., description="User ID"), @@ -61,13 +65,36 @@ async def delete_session_endpoint( redis_service.delete_session(user_id, session_id) return {"status": "deleted"} -# estensione TTL sessioni + +# ------------------------- +# Gestione TTL sessioni +# ------------------------- + @router.post("/sessions/{session_id}/extend_ttl", response_model=dict) async def extend_ttl_endpoint( user_id: str = Query(..., description="User ID"), - session_id: str = Path(..., description="Session ID") - extra_seconds: int = Body(..., embed=True) + session_id: str = Path(..., description="Session ID"), + extra_seconds: int = Body(..., embed=True, description="Seconds to extend the TTL") ): + """ + Extend the TTL (time-to-live) for a given session and its related data. + """ redis_service.extend_session_ttl(user_id, session_id, extra_seconds) - return {"status": "ttl_extended"} + return {"status": "ttl_extended", "extra_seconds": extra_seconds} + + +@router.post("/sessions/{session_id}/refresh_ttl", response_model=dict) +async def refresh_ttl_endpoint( + user_id: str = Query(..., description="User ID"), + session_id: str = Path(..., description="Session ID") +): + """ + Reset the TTL for a given session and its related data back to the default value. + """ + redis_service.refresh_session_ttl(user_id, session_id) + return { + "status": "ttl_refreshed", + "default_ttl_seconds": redis_service.DEFAULT_TTL_SECONDS + } + diff --git a/backend/services/redis_service.py b/backend/services/redis_service.py index cb7e17e..6682a4e 100644 --- a/backend/services/redis_service.py +++ b/backend/services/redis_service.py @@ -1,4 +1,5 @@ # services/redis_service.py + import redis import json import uuid @@ -6,6 +7,9 @@ from config import settings from datetime import datetime from typing import List, Optional +# TTL di default: 30 giorni +DEFAULT_TTL_SECONDS = 30 * 24 * 60 * 60 + r = redis.Redis( host=settings.REDIS_HOST, port=settings.REDIS_PORT, @@ -13,34 +17,24 @@ r = redis.Redis( decode_responses=True ) -# setup defaults -DEFAULT_TTL_SECONDS = 30 * 24 * 3600 # 30 gg - # ------------------------- # Chat message operations # ------------------------- def save_chat(user_id: str, session_id: str, message: dict): - """ - Save a message for a user/session, adding a UTC ISO timestamp. - Also updates session metadata (message_count, history_size_bytes). + """Save a message for a user/session, adding a UTC ISO timestamp. + Also updates session metadata and TTL. """ key = f"chatHistory:{user_id}:{session_id}" - enriched = { - **message, - "timestamp": datetime.utcnow().isoformat() + "Z" - } + enriched = {**message, "timestamp": datetime.utcnow().isoformat() + "Z"} r.rpush(key, json.dumps(enriched)) - r.expire(key, DEFAULT_TTL_SECONDS) # TTL anche per la history - - # Update metadata after adding + r.expire(key, DEFAULT_TTL_SECONDS) # rinnova TTL history _update_session_stats(user_id, session_id) + _refresh_ttl(user_id, session_id) # rinnova TTL meta e indice + def get_chat(user_id: str, session_id: str, limit: Optional[int] = None): - """ - Retrieve messages for a user/session, sorted by timestamp. - Optionally limit to the last N messages. - """ + """Retrieve messages for a user/session, sorted by timestamp.""" key = f"chatHistory:{user_id}:{session_id}" data = r.lrange(key, 0, -1) messages = [json.loads(item) for item in data] @@ -51,13 +45,11 @@ def get_chat(user_id: str, session_id: str, limit: Optional[int] = None): def clear_chat(user_id: str, session_id: str): - """ - Delete all messages for a user/session. - Also resets metadata stats (message_count, history_size_bytes). - """ + """Delete all messages for a user/session and reset stats.""" key = f"chatHistory:{user_id}:{session_id}" r.delete(key) _update_session_stats(user_id, session_id, reset=True) + _refresh_ttl(user_id, session_id) # ------------------------- @@ -65,9 +57,7 @@ def clear_chat(user_id: str, session_id: str): # ------------------------- def create_session(user_id: str, first_message: str) -> dict: - """ - Create a new session with initial metadata. - """ + """Create a new session with initial metadata and TTL.""" session_id = str(uuid.uuid4()) created_at = datetime.utcnow().isoformat() + "Z" session_name = (first_message.strip()[:50] or "New Chat") @@ -83,14 +73,13 @@ def create_session(user_id: str, first_message: str) -> dict: r.set(meta_key, json.dumps(meta), ex=DEFAULT_TTL_SECONDS) r.zadd(index_key, {session_id: datetime.utcnow().timestamp()}) - r.expire(index_key, DEFAULT_TTL_SECONDS) # TTL anche per l’indice utente + r.expire(index_key, DEFAULT_TTL_SECONDS) return meta + def get_sessions(user_id: str) -> List[dict]: - """ - Return all sessions for a user, newest first. - """ + """Return all sessions for a user, newest first.""" session_ids = r.zrevrangebyscore(f"chatSessionsIndex:{user_id}", "+inf", "-inf") sessions = [] for sid in session_ids: @@ -106,31 +95,35 @@ def get_session_meta(user_id: str, session_id: str) -> Optional[dict]: def update_session_meta(user_id: str, session_id: str, **updates) -> Optional[dict]: - """ - Update metadata fields (e.g., session_name) for a session. - """ + """Update metadata fields (e.g., session_name) for a session.""" meta = get_session_meta(user_id, session_id) if not meta: return None meta.update(updates) - r.set(f"chatSession:{user_id}:{session_id}", json.dumps(meta)) + r.set(f"chatSession:{user_id}:{session_id}", json.dumps(meta), ex=DEFAULT_TTL_SECONDS) + _refresh_ttl(user_id, session_id) return meta def delete_session(user_id: str, session_id: str): - """ - Delete metadata, chat history, and remove from index. - """ + """Delete metadata, chat history, and remove from index.""" r.delete(f"chatSession:{user_id}:{session_id}") r.delete(f"chatHistory:{user_id}:{session_id}") r.zrem(f"chatSessionsIndex:{user_id}", session_id) -def extend_session_ttl(user_id: str, session_id: str, extra_seconds: int): - meta_key = f"chatSession:{user_id}:{session_id}" - history_key = f"chatHistory:{user_id}:{session_id}" - index_key = f"chatSessionsIndex:{user_id}" - for key in [meta_key, history_key, index_key]: +# ------------------------- +# TTL management +# ------------------------- + +def extend_session_ttl(user_id: str, session_id: str, extra_seconds: int): + """Extend TTL for session metadata, history, and index.""" + keys = [ + f"chatSession:{user_id}:{session_id}", + f"chatHistory:{user_id}:{session_id}", + f"chatSessionsIndex:{user_id}" + ] + for key in keys: if r.exists(key): current_ttl = r.ttl(key) if current_ttl > 0: @@ -138,29 +131,35 @@ def extend_session_ttl(user_id: str, session_id: str, extra_seconds: int): else: r.expire(key, extra_seconds) +def refresh_session_ttl(user_id: str, session_id: str): + """Reset TTL for session metadata, history, and index to default.""" + keys = [ + f"chatSession:{user_id}:{session_id}", + f"chatHistory:{user_id}:{session_id}", + f"chatSessionsIndex:{user_id}" + ] + for key in keys: + if r.exists(key): + r.expire(key, DEFAULT_TTL_SECONDS) + # ------------------------- # Internal helpers # ------------------------- def _update_session_stats(user_id: str, session_id: str, reset: bool = False): - """ - Refresh message_count and history_size_bytes in metadata. - """ + """Refresh message_count and history_size_bytes in metadata.""" meta = get_session_meta(user_id, session_id) if not meta: return - if reset: meta["message_count"] = 0 meta["history_size_bytes"] = 0 else: - # Count messages and total size key = f"chatHistory:{user_id}:{session_id}" messages = r.lrange(key, 0, -1) meta["message_count"] = len(messages) meta["history_size_bytes"] = sum(len(m.encode("utf-8")) for m in messages) - - r.set(f"chatSession:{user_id}:{session_id}", json.dumps(meta)) + r.set(f"chatSession:{user_id}:{session_id}", json.dumps(meta), ex=DEFAULT_TTL_SECONDS)