From 4cfad2e8bc3d485b336e32dae6895eb7903b8a94 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 8 Jan 2026 16:54:18 +0100 Subject: [PATCH] fix: double volatile TTLs to survive missed scheduler runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TTL = 2x refresh interval ensures data remains valid even if a scheduled refresh is delayed or fails. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 6 +++ check_environment.py | 102 +++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/models/volatile.py | 27 +++++------ 4 files changed, 123 insertions(+), 14 deletions(-) create mode 100644 check_environment.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 55a8f9f..a1c9bd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Library Desk will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.7.2] - 2026-01-07 + +### Fixed + +- **Volatile TTL doubled** - TTL now 2x refresh interval to survive missed/delayed scheduler runs + ## [1.7.1] - 2026-01-07 ### Fixed diff --git a/check_environment.py b/check_environment.py new file mode 100644 index 0000000..1443a7f --- /dev/null +++ b/check_environment.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Quick script to check environmental data in Qdrant volatile cache.""" + +import asyncio +import os +import time +from dotenv import load_dotenv + +load_dotenv() + +async def main(): + from qdrant_client import QdrantClient + from qdrant_client.models import Filter, FieldCondition, MatchValue + from src.config import get_settings + + settings = get_settings() + qdrant = QdrantClient(url=settings.qdrant_url) + + user = "jpmschweitzer" + collection = f"volatile_{user}" + + print(f"\n{'='*60}") + print(f"Volatile Data in Qdrant ({collection})") + print(f"{'='*60}\n") + + namespaces = ["weather", "air_quality", "forecast", "sun", "news"] + + for ns in namespaces: + try: + results = qdrant.scroll( + collection_name=collection, + scroll_filter=Filter( + must=[FieldCondition(key="namespace", match=MatchValue(value=ns))] + ), + limit=10, + with_payload=True, + with_vectors=False, + ) + + points = results[0] + print(f"=== {ns.upper()} ({len(points)} records) ===") + + if not points: + print(" (no data)") + print() + continue + + for point in points: + payload = point.payload + raw_data = payload.get("raw_data", {}) + + if ns == "weather": + print(f" Temperature: {raw_data.get('temperature')}°C (feels like {raw_data.get('feels_like')}°C)") + print(f" Conditions: {raw_data.get('conditions')}") + print(f" Humidity: {raw_data.get('humidity')}%") + print(f" Wind: {raw_data.get('wind_speed')} km/h") + print(f" UV Index: {raw_data.get('uv_index')}") + + elif ns == "air_quality": + print(f" European AQI: {raw_data.get('aqi_european')}") + print(f" US AQI: {raw_data.get('aqi_us')}") + print(f" PM2.5: {raw_data.get('pm2_5')} µg/m³") + print(f" PM10: {raw_data.get('pm10')} µg/m³") + print(f" Ozone: {raw_data.get('ozone')} µg/m³") + print(f" NO₂: {raw_data.get('nitrogen_dioxide')} µg/m³") + + elif ns == "forecast": + daily = raw_data.get("daily", []) + for day in daily[:5]: + print(f" {day.get('day_name', 'N/A')[:3]}: {day.get('temp_low'):.0f}-{day.get('temp_high'):.0f}°C, {day.get('conditions')}") + + elif ns == "sun": + print(f" Sunrise: {raw_data.get('sunrise')}") + print(f" Sunset: {raw_data.get('sunset')}") + print(f" Daylight: {raw_data.get('daylight_hours', 0):.1f} hours") + + elif ns == "news": + headlines = raw_data.get("headlines", []) + print(f" Category: {raw_data.get('category', 'general')}") + print(f" Headlines ({len(headlines)}):") + for item in headlines[:5]: + title = item.get("title", "")[:60] + source = item.get("source", "") + print(f" - [{source}] {title}...") + + # Show TTL info + ttl_expiry = payload.get("ttl_expiry") + if ttl_expiry: + remaining = (ttl_expiry / 1000) - time.time() + if remaining > 0: + print(f" TTL remaining: {int(remaining)}s ({int(remaining/60)} min)") + else: + print(f" TTL: EXPIRED") + + print() + + except Exception as e: + print(f" Error fetching {ns}: {e}") + print() + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/pyproject.toml b/pyproject.toml index 2b4760c..bfe24cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "library-desk" -version = "1.7.1" +version = "1.7.2" description = "Coordination service for The Library system - HybridRAG queries, document ingestion, entity extraction, and knowledge consolidation" readme = "README.md" requires-python = ">=3.12" diff --git a/src/models/volatile.py b/src/models/volatile.py index cbdb78c..99166b8 100644 --- a/src/models/volatile.py +++ b/src/models/volatile.py @@ -38,20 +38,21 @@ class VolatileNamespace(str, Enum): # Default TTLs per namespace (in seconds) +# TTL = 2x refresh interval to ensure data survives missed/delayed refreshes NAMESPACE_DEFAULT_TTL: Dict[str, int] = { - VolatileNamespace.WEATHER: 3600, # 1 hour - current conditions - VolatileNamespace.FORECAST: 43200, # 12 hours - forecast stable longer - VolatileNamespace.SUN: 86400, # 24 hours - sun times change daily - VolatileNamespace.NEWS: 3600, # 1 hour - news cycles - VolatileNamespace.FINANCIAL: 300, # 5 min - markets move fast - VolatileNamespace.TRANSIT: 300, # 5 min - schedules update frequently - VolatileNamespace.TRAFFIC: 600, # 10 min - traffic patterns - VolatileNamespace.AIR_QUALITY: 3600, # 1 hour - air quality stable - VolatileNamespace.SPORTS: 60, # 1 min - live scores - VolatileNamespace.SOCIAL: 600, # 10 min - social notifications - VolatileNamespace.SYSTEM: 60, # 1 min - system health - VolatileNamespace.CONTEXT: 3600, # 1 hour - session context - VolatileNamespace.CUSTOM: 3600, # 1 hour - default for custom + VolatileNamespace.WEATHER: 7200, # 2 hours (hourly refresh) + VolatileNamespace.FORECAST: 86400, # 24 hours (12hr refresh) + VolatileNamespace.SUN: 172800, # 48 hours (daily refresh) + VolatileNamespace.NEWS: 7200, # 2 hours (hourly refresh) + VolatileNamespace.FINANCIAL: 600, # 10 min (5 min refresh) + VolatileNamespace.TRANSIT: 600, # 10 min (5 min refresh) + VolatileNamespace.TRAFFIC: 1200, # 20 min (10 min refresh) + VolatileNamespace.AIR_QUALITY: 7200, # 2 hours (hourly refresh) + VolatileNamespace.SPORTS: 120, # 2 min (1 min refresh) + VolatileNamespace.SOCIAL: 1200, # 20 min (10 min refresh) + VolatileNamespace.SYSTEM: 120, # 2 min (1 min refresh) + VolatileNamespace.CONTEXT: 7200, # 2 hours - session context + VolatileNamespace.CUSTOM: 7200, # 2 hours - default for custom }