feat: add caching for peer proxy requests
- Cache successful GET responses from peers (5 min TTL) - Return cached data when peer is unreachable - Add _cached, _cached_at, _cache_age_seconds flags - Add /api/node/<id>/health proxy endpoint Enables master dashboard to show peer data even when peers are temporarily unreachable (e.g., mobile devices, VPN disconnections). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1679,12 +1679,21 @@ def create_app(config: Config | None = None) -> Flask:
|
||||
|
||||
# ==================== Multi-Node Master Dashboard Proxy API ====================
|
||||
|
||||
# Cache for peer responses (scanner_id -> path -> {data, timestamp})
|
||||
_peer_cache: dict[str, dict[str, dict]] = {}
|
||||
_peer_cache_ttl = 300 # 5 minutes cache TTL
|
||||
|
||||
def proxy_peer_request(scanner_id: str, path: str, method: str = "GET", **kwargs):
|
||||
"""Proxy a request to a peer node.
|
||||
"""Proxy a request to a peer node with caching.
|
||||
|
||||
Returns:
|
||||
- Response tuple (jsonify(...), status_code) on error or peer response
|
||||
- None if scanner_id matches local scanner (caller should use local handler)
|
||||
|
||||
Caching behavior:
|
||||
- GET requests are cached on success
|
||||
- On peer unreachable, returns cached data if available (with _cached flag)
|
||||
- POST requests are not cached but will return cached GET data on failure
|
||||
"""
|
||||
local_id = app.config.get("SCANNER_IDENTITY", {}).get("id")
|
||||
if scanner_id == local_id:
|
||||
@@ -1695,10 +1704,13 @@ def create_app(config: Config | None = None) -> Flask:
|
||||
if not peer or not peer.get("url"):
|
||||
return jsonify({"error": "Peer not found"}), 404
|
||||
|
||||
cache_key = f"{scanner_id}:{path}"
|
||||
|
||||
try:
|
||||
url = f"{peer['url']}{path}"
|
||||
resp = requests.request(method, url, timeout=15, **kwargs)
|
||||
resp = requests.request(method, url, timeout=10, **kwargs)
|
||||
data = resp.json()
|
||||
|
||||
# Enrich response with source scanner info
|
||||
data["_source_scanner"] = scanner_id
|
||||
data["_source_position"] = {
|
||||
@@ -1706,9 +1718,39 @@ def create_app(config: Config | None = None) -> Flask:
|
||||
"lon": peer.get("longitude"),
|
||||
"floor": peer.get("floor")
|
||||
}
|
||||
data["_cached"] = False
|
||||
|
||||
# Cache successful GET responses
|
||||
if method == "GET":
|
||||
if scanner_id not in _peer_cache:
|
||||
_peer_cache[scanner_id] = {}
|
||||
_peer_cache[scanner_id][path] = {
|
||||
"data": data,
|
||||
"timestamp": datetime.now()
|
||||
}
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
except requests.RequestException as e:
|
||||
return jsonify({"error": f"Peer unreachable: {e}"}), 503
|
||||
# Try to return cached data
|
||||
if scanner_id in _peer_cache and path in _peer_cache[scanner_id]:
|
||||
cached = _peer_cache[scanner_id][path]
|
||||
cache_age = (datetime.now() - cached["timestamp"]).total_seconds()
|
||||
|
||||
# Return cached data if within TTL (or always for display purposes)
|
||||
cached_data = cached["data"].copy()
|
||||
cached_data["_cached"] = True
|
||||
cached_data["_cached_at"] = cached["timestamp"].isoformat()
|
||||
cached_data["_cache_age_seconds"] = int(cache_age)
|
||||
cached_data["_peer_error"] = str(e)
|
||||
|
||||
return jsonify(cached_data)
|
||||
|
||||
return jsonify({
|
||||
"error": f"Peer unreachable: {e}",
|
||||
"_source_scanner": scanner_id,
|
||||
"_cached": False
|
||||
}), 503
|
||||
|
||||
@app.route("/api/node/<scanner_id>/latest")
|
||||
def api_node_latest(scanner_id: str):
|
||||
@@ -1742,6 +1784,14 @@ def create_app(config: Config | None = None) -> Flask:
|
||||
return api_trilaterated_positions()
|
||||
return result
|
||||
|
||||
@app.route("/api/node/<scanner_id>/health")
|
||||
def api_node_health(scanner_id: str):
|
||||
"""Proxy: Get health status from a peer node."""
|
||||
result = proxy_peer_request(scanner_id, "/api/health")
|
||||
if result is None:
|
||||
return api_health()
|
||||
return result
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user