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 ====================
|
# ==================== 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):
|
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:
|
Returns:
|
||||||
- Response tuple (jsonify(...), status_code) on error or peer response
|
- Response tuple (jsonify(...), status_code) on error or peer response
|
||||||
- None if scanner_id matches local scanner (caller should use local handler)
|
- 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")
|
local_id = app.config.get("SCANNER_IDENTITY", {}).get("id")
|
||||||
if scanner_id == local_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"):
|
if not peer or not peer.get("url"):
|
||||||
return jsonify({"error": "Peer not found"}), 404
|
return jsonify({"error": "Peer not found"}), 404
|
||||||
|
|
||||||
|
cache_key = f"{scanner_id}:{path}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
url = f"{peer['url']}{path}"
|
url = f"{peer['url']}{path}"
|
||||||
resp = requests.request(method, url, timeout=15, **kwargs)
|
resp = requests.request(method, url, timeout=10, **kwargs)
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
|
|
||||||
# Enrich response with source scanner info
|
# Enrich response with source scanner info
|
||||||
data["_source_scanner"] = scanner_id
|
data["_source_scanner"] = scanner_id
|
||||||
data["_source_position"] = {
|
data["_source_position"] = {
|
||||||
@@ -1706,9 +1718,39 @@ def create_app(config: Config | None = None) -> Flask:
|
|||||||
"lon": peer.get("longitude"),
|
"lon": peer.get("longitude"),
|
||||||
"floor": peer.get("floor")
|
"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)
|
return jsonify(data)
|
||||||
|
|
||||||
except requests.RequestException as e:
|
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")
|
@app.route("/api/node/<scanner_id>/latest")
|
||||||
def api_node_latest(scanner_id: str):
|
def api_node_latest(scanner_id: str):
|
||||||
@@ -1742,6 +1784,14 @@ def create_app(config: Config | None = None) -> Flask:
|
|||||||
return api_trilaterated_positions()
|
return api_trilaterated_positions()
|
||||||
return result
|
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
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user