Compare commits

...
1 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.5 4cfad2e8bc fix: double volatile TTLs to survive missed scheduler runs
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 2m13s
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 <noreply@anthropic.com>
2026-01-08 16:54:18 +01:00
4 changed files with 123 additions and 14 deletions
+6
View File
@@ -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
+102
View File
@@ -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())
+1 -1
View File
@@ -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"
+14 -13
View File
@@ -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
}