heliosdb_proxy/edge/mod.rs
1//! Edge / geo proxy mode (T3.2).
2//!
3//! A HeliosProxy can run in *edge* mode: it terminates client SQL
4//! against a local in-memory cache and only forwards to the *home*
5//! proxy on cache miss. Writes always pass through to home, and
6//! home broadcasts invalidations back to every registered edge so
7//! cached results don't go stale on subsequent reads.
8//!
9//! ## Coherence model — last-write-wins with TTL
10//!
11//! - **Reads**: edge looks up `(query_fingerprint, params)` in the
12//! local cache. Hit → serve from cache. Miss → forward to home,
13//! cache the result with the configured TTL, serve.
14//! - **Writes**: edge forwards verbatim. On success, home computes
15//! the set of touched-tables (via the analytics fingerprint) and
16//! pushes an `Invalidate { tables, version }` event to every
17//! registered edge.
18//! - **Conflict resolution**: each cache entry carries a monotonic
19//! `version` (logical wall-clock). An invalidation drops every
20//! entry whose `version <= invalidation.version`. Late writes
21//! (clock skew across regions) cannot resurrect stale data.
22//!
23//! ## Cross-region link — pull-on-miss + invalidation push
24//!
25//! Edge → Home: HTTP/1.1 with bearer-token auth. Each edge starts
26//! up by registering with home (`POST /api/edge/register`) and
27//! holding the response stream open for invalidation events
28//! (chunked-transfer Server-Sent Events).
29//!
30//! Home → Edge: same connection — home pushes
31//! `event: invalidate\ndata: {...}\n\n` whenever a write commits.
32//!
33//! No per-region central registrar, no distributed consensus, no
34//! vector clocks. Picks "eventual consistency with bounded
35//! staleness" as the explicit contract — readers may see TTL-window
36//! stale data on any region after a write to another region.
37
38pub mod cache;
39pub mod registry;
40
41pub use cache::{CacheEntry, CacheKey, EdgeCache, EdgeCacheStats};
42pub use registry::{EdgeNode, EdgeRegistry, InvalidationEvent};
43
44use serde::{Deserialize, Serialize};
45use std::time::Duration;
46
47/// Edge-mode runtime config.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct EdgeConfig {
50 /// "edge" or "home" mode. Edges forward writes + cache reads;
51 /// home is authoritative.
52 pub role: EdgeRole,
53
54 /// For edge: home proxy admin URL (e.g. "https://proxy.home.svc:9090").
55 /// Empty when role = home.
56 #[serde(default)]
57 pub home_url: String,
58
59 /// For edge: bearer token presented to home for register +
60 /// pull-on-miss. Empty when role = home.
61 #[serde(default)]
62 pub auth_token: String,
63
64 /// Default TTL applied to cache entries when the home doesn't
65 /// supply one explicitly. Reads expire after this elapses.
66 pub default_ttl: Duration,
67
68 /// Maximum cache entries before LRU eviction kicks in.
69 pub max_entries: usize,
70
71 /// For home: maximum simultaneous registered edge nodes.
72 pub max_edges: usize,
73}
74
75impl Default for EdgeConfig {
76 fn default() -> Self {
77 Self {
78 role: EdgeRole::Home,
79 home_url: String::new(),
80 auth_token: String::new(),
81 default_ttl: Duration::from_secs(60),
82 max_entries: 10_000,
83 max_edges: 32,
84 }
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "lowercase")]
90pub enum EdgeRole {
91 /// Authoritative proxy. Routes writes to backends, broadcasts
92 /// invalidations to every registered edge.
93 Home,
94 /// Cache-first proxy. Forwards writes to home, cache reads,
95 /// listens for invalidations.
96 Edge,
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn default_role_is_home() {
105 let cfg = EdgeConfig::default();
106 assert_eq!(cfg.role, EdgeRole::Home);
107 }
108
109 #[test]
110 fn config_round_trips_through_serde() {
111 let cfg = EdgeConfig {
112 role: EdgeRole::Edge,
113 home_url: "https://home.svc:9090".into(),
114 auth_token: "tkn".into(),
115 default_ttl: Duration::from_secs(30),
116 max_entries: 5_000,
117 max_edges: 0,
118 };
119 let json = serde_json::to_string(&cfg).unwrap();
120 let back: EdgeConfig = serde_json::from_str(&json).unwrap();
121 assert_eq!(back.role, EdgeRole::Edge);
122 assert_eq!(back.home_url, "https://home.svc:9090");
123 assert_eq!(back.default_ttl, Duration::from_secs(30));
124 }
125}