Skip to main content

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}