Skip to main content

subetha_cxc/
virtual_endpoint.rs

1//! `VirtualEndpoint`: substrate-level endpoint identity that resolves
2//! to either a local `LocaleAdaptiveRing` or a remote-via-QUIC
3//! target at runtime.
4//!
5//! The substrate's local data path is mmap-backed rings; the
6//! cross-host extension is the QUIC bridge. Today callers know
7//! which they're talking to at construction time: they hold an
8//! `Arc<LocaleAdaptiveRing>` for local peers and configure a
9//! `QuicBridgeClient` for remote peers. `VirtualEndpoint` unifies
10//! the two behind one identifier so application code calls
11//! `endpoint.send(payload)` without grepping config for "is this
12//! peer local or remote".
13//!
14//! # Registry model
15//!
16//! Each substrate process has one in-process
17//! `VirtualEndpointRegistry` that maps `EndpointId -> EndpointTarget`.
18//! The default registry is a process-global `OnceLock`. Callers
19//! that want a custom lifecycle (per-test isolation, per-tenant
20//! routing) construct their own registry and pass `&registry` to
21//! the endpoint constructors.
22//!
23//! `EndpointTarget` is an enum: `Local(Arc<LocaleAdaptiveRing>)` or
24//! `Remote(RemoteEndpoint)`. The remote variant holds the address +
25//! optional bridge handle and is wired up when QUIC support is
26//! enabled.
27//!
28//! # Pin protocol
29//!
30//! `VirtualEndpoint::pin_current_target()` returns a
31//! `PinnedEndpoint<'_>` that captures the active target at pin time.
32//! A subsequent `registry.rebind(endpoint_id, new_target)` bumps the
33//! registry's generation counter; the pin sees
34//! `is_still_valid() == false` and the holder re-acquires.
35//!
36//! For local targets, `PinnedEndpoint::as_local()` returns
37//! `&LocaleAdaptiveRing`; the caller chains directly into the
38//! existing locale-axis pin (`pin_current_locale()`) and from there
39//! into the shape-axis pin. The full chain reaches the native
40//! primitive through three Acquire loads, one per axis level.
41
42use std::cell::Cell;
43use std::collections::HashMap;
44use std::marker::PhantomData;
45use std::net::SocketAddr;
46use std::sync::atomic::{AtomicU64, Ordering};
47use std::sync::{Arc, RwLock};
48
49use crate::locale_adaptive_ring::LocaleAdaptiveRing;
50
51/// Application-supplied identifier for a virtual endpoint. Opaque
52/// to the substrate; the registry maps it to a target.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
54pub struct EndpointId(pub u64);
55
56/// What a virtual endpoint resolves to at runtime.
57#[derive(Clone)]
58pub enum EndpointTarget {
59    /// Local target: bytes flow through a `LocaleAdaptiveRing` on
60    /// this host. No network involved.
61    Local(Arc<LocaleAdaptiveRing>),
62    /// Remote target: bytes flow over the wire to another host's
63    /// substrate instance. The address tells the QUIC bridge where
64    /// to connect; the substrate does not require QUIC to be
65    /// enabled for the local path to work.
66    Remote(RemoteEndpoint),
67}
68
69/// Describes a remote substrate endpoint reachable over the network.
70#[derive(Clone, Debug)]
71pub struct RemoteEndpoint {
72    /// Server address of the remote substrate's QUIC bridge.
73    pub server_addr: SocketAddr,
74    /// SNI / server name for TLS validation.
75    pub server_name: String,
76}
77
78/// In-process registry mapping `EndpointId` to `EndpointTarget`.
79/// Each rebind bumps the registry-wide generation counter so
80/// pinned-endpoint holders see invalidation.
81pub struct VirtualEndpointRegistry {
82    table: RwLock<HashMap<EndpointId, EndpointTarget>>,
83    /// Bumped on every successful bind / rebind / unbind.
84    generation: AtomicU64,
85}
86
87impl VirtualEndpointRegistry {
88    /// Construct an empty registry.
89    pub fn new() -> Self {
90        Self {
91            table: RwLock::new(HashMap::new()),
92            generation: AtomicU64::new(0),
93        }
94    }
95
96    /// Insert or replace the target for `id`. Bumps the generation.
97    pub fn bind(&self, id: EndpointId, target: EndpointTarget) {
98        let mut table = self.table.write().expect("registry table poisoned");
99        table.insert(id, target);
100        self.generation.fetch_add(1, Ordering::AcqRel);
101    }
102
103    /// Remove the binding for `id`, returning the prior target if
104    /// any. Bumps the generation.
105    pub fn unbind(&self, id: EndpointId) -> Option<EndpointTarget> {
106        let mut table = self.table.write().expect("registry table poisoned");
107        let prior = table.remove(&id);
108        if prior.is_some() {
109            self.generation.fetch_add(1, Ordering::AcqRel);
110        }
111        prior
112    }
113
114    /// Look up the target for `id`. Returns a clone so the caller
115    /// does not hold the registry lock across awaits.
116    pub fn lookup(&self, id: EndpointId) -> Option<EndpointTarget> {
117        let table = self.table.read().expect("registry table poisoned");
118        table.get(&id).cloned()
119    }
120
121    /// Current generation. Pinned endpoints compare captured
122    /// generation against this on `is_still_valid()`.
123    pub fn generation(&self) -> u64 {
124        self.generation.load(Ordering::Acquire)
125    }
126
127    /// Number of bound endpoints.
128    pub fn len(&self) -> usize {
129        self.table.read().expect("registry table poisoned").len()
130    }
131
132    /// True when no endpoints are bound.
133    pub fn is_empty(&self) -> bool { self.len() == 0 }
134}
135
136impl Default for VirtualEndpointRegistry {
137    fn default() -> Self { Self::new() }
138}
139
140/// A virtual endpoint handle. Holds a reference to the registry +
141/// the endpoint id. Application code calls `pin_current_target()`
142/// to get a target snapshot, then dispatches on
143/// `as_local()` / `as_remote()` to do work.
144pub struct VirtualEndpoint {
145    registry: Arc<VirtualEndpointRegistry>,
146    id: EndpointId,
147}
148
149impl VirtualEndpoint {
150    /// Bind a fresh endpoint and return a handle to it.
151    pub fn bind(
152        registry: Arc<VirtualEndpointRegistry>,
153        id: EndpointId,
154        target: EndpointTarget,
155    ) -> Self {
156        registry.bind(id, target);
157        Self { registry, id }
158    }
159
160    /// Construct a handle pointing at an already-bound endpoint.
161    /// Returns `None` if no binding exists.
162    pub fn attach(
163        registry: Arc<VirtualEndpointRegistry>,
164        id: EndpointId,
165    ) -> Option<Self> {
166        registry.lookup(id)?;
167        Some(Self { registry, id })
168    }
169
170    /// The endpoint's id.
171    pub fn id(&self) -> EndpointId { self.id }
172
173    /// Capture the current target and return a pinned handle.
174    /// Returns `None` if the endpoint has been unbound.
175    pub fn pin_current_target(&self) -> Option<PinnedEndpoint<'_>> {
176        let captured_gen = self.registry.generation();
177        let target = self.registry.lookup(self.id)?;
178        Some(PinnedEndpoint {
179            registry: &self.registry,
180            id: self.id,
181            pinned_generation: captured_gen,
182            target,
183            _not_sync: PhantomData,
184        })
185    }
186}
187
188/// Pinned snapshot of a virtual endpoint's target. Captures the
189/// registry generation at pin time; one Acquire load on
190/// `is_still_valid()` detects rebinds.
191pub struct PinnedEndpoint<'a> {
192    registry: &'a VirtualEndpointRegistry,
193    id: EndpointId,
194    pinned_generation: u64,
195    target: EndpointTarget,
196    _not_sync: PhantomData<Cell<()>>,
197}
198
199impl<'a> PinnedEndpoint<'a> {
200    /// Endpoint id this pin was captured for.
201    pub fn id(&self) -> EndpointId { self.id }
202
203    /// Generation captured at pin time.
204    pub fn pinned_generation(&self) -> u64 { self.pinned_generation }
205
206    /// One Acquire load on the registry's generation counter.
207    /// Returns `true` while the pin is current; `false` if any
208    /// `bind` / `unbind` has happened on ANY endpoint in the
209    /// registry since pin time.
210    ///
211    /// The coarse-grained check is intentional: a registry-wide
212    /// generation is one atomic load per check, vs per-endpoint
213    /// generations which require lookup + dereference. Callers
214    /// that pin many endpoints in a tight loop trade some
215    /// false-positive re-acquires for a much cheaper validity check.
216    pub fn is_still_valid(&self) -> bool {
217        self.registry.generation() == self.pinned_generation
218    }
219
220    /// Returns `Some(&Arc<LocaleAdaptiveRing>)` when the captured
221    /// target is local, `None` otherwise. The Arc is borrowed from
222    /// the pinned target snapshot and lives for the pin's lifetime.
223    pub fn as_local(&self) -> Option<&Arc<LocaleAdaptiveRing>> {
224        match &self.target {
225            EndpointTarget::Local(ring) => Some(ring),
226            _ => None,
227        }
228    }
229
230    /// Returns `Some(&RemoteEndpoint)` when the captured target is
231    /// remote, `None` otherwise.
232    pub fn as_remote(&self) -> Option<&RemoteEndpoint> {
233        match &self.target {
234            EndpointTarget::Remote(remote) => Some(remote),
235            _ => None,
236        }
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    fn tmp(name: &str) -> std::path::PathBuf {
245        let mut p = std::env::temp_dir();
246        let pid = std::process::id();
247        let nonce = std::time::SystemTime::now()
248            .duration_since(std::time::UNIX_EPOCH)
249            .map(|d| d.as_nanos())
250            .unwrap_or(0);
251        p.push(format!("ve_{pid}_{nonce}_{name}"));
252        p
253    }
254
255    fn local_target(name: &str) -> EndpointTarget {
256        let ring = Arc::new(
257            LocaleAdaptiveRing::create(tmp(name), 1, 1, 64)
258                .expect("locale ring create"),
259        );
260        ring.register_producer().expect("p");
261        ring.register_consumer().expect("c");
262        EndpointTarget::Local(ring)
263    }
264
265    #[test]
266    fn bind_then_lookup() {
267        let registry = Arc::new(VirtualEndpointRegistry::new());
268        let id = EndpointId(42);
269        let target = local_target("bind");
270        registry.bind(id, target);
271        assert!(matches!(
272            registry.lookup(id),
273            Some(EndpointTarget::Local(_)),
274        ));
275        assert_eq!(registry.len(), 1);
276    }
277
278    #[test]
279    fn rebind_bumps_generation() {
280        let registry = Arc::new(VirtualEndpointRegistry::new());
281        let id = EndpointId(7);
282        let gen_before = registry.generation();
283        registry.bind(id, local_target("rebind_a"));
284        let gen_after_first = registry.generation();
285        assert!(gen_after_first > gen_before);
286        registry.bind(id, local_target("rebind_b"));
287        assert!(registry.generation() > gen_after_first);
288    }
289
290    #[test]
291    fn pin_invalidates_on_rebind() {
292        let registry = Arc::new(VirtualEndpointRegistry::new());
293        let id = EndpointId(99);
294        let endpoint = VirtualEndpoint::bind(
295            registry.clone(), id, local_target("pin_inv_a"),
296        );
297        let pin = endpoint.pin_current_target().expect("pinned");
298        assert!(pin.is_still_valid());
299        assert!(pin.as_local().is_some());
300        assert!(pin.as_remote().is_none());
301
302        // Rebind to a different target; pin must invalidate.
303        registry.bind(id, local_target("pin_inv_b"));
304        assert!(!pin.is_still_valid(),
305                "pin must invalidate after rebind");
306
307        // Re-acquire reaches the new target.
308        let pin2 = endpoint.pin_current_target().expect("re-pinned");
309        assert!(pin2.is_still_valid());
310    }
311
312    #[test]
313    fn pin_invalidates_on_unbind() {
314        let registry = Arc::new(VirtualEndpointRegistry::new());
315        let id = EndpointId(123);
316        let endpoint = VirtualEndpoint::bind(
317            registry.clone(), id, local_target("unbind_test"),
318        );
319        let pin = endpoint.pin_current_target().expect("pinned");
320        registry.unbind(id);
321        assert!(!pin.is_still_valid());
322        assert!(endpoint.pin_current_target().is_none(),
323                "after unbind, re-pin returns None");
324    }
325
326    #[test]
327    fn attach_returns_none_for_unbound() {
328        let registry = Arc::new(VirtualEndpointRegistry::new());
329        let id = EndpointId(404);
330        assert!(VirtualEndpoint::attach(registry, id).is_none());
331    }
332
333    #[test]
334    fn remote_target_pins_correctly() {
335        let registry = Arc::new(VirtualEndpointRegistry::new());
336        let id = EndpointId(8080);
337        registry.bind(id, EndpointTarget::Remote(RemoteEndpoint {
338            server_addr: "127.0.0.1:8080".parse().unwrap(),
339            server_name: "remote.example".to_string(),
340        }));
341        let endpoint = VirtualEndpoint::attach(registry, id)
342            .expect("attached");
343        let pin = endpoint.pin_current_target().expect("pinned");
344        assert!(pin.as_local().is_none());
345        let remote = pin.as_remote().expect("remote target");
346        assert_eq!(remote.server_name, "remote.example");
347    }
348
349    #[test]
350    fn pin_chains_into_locale_axis() {
351        let registry = Arc::new(VirtualEndpointRegistry::new());
352        let id = EndpointId(1);
353        let target = local_target("chain");
354        registry.bind(id, target);
355        let endpoint = VirtualEndpoint::attach(registry, id).expect("attached");
356
357        let pin_endpoint = endpoint.pin_current_target().expect("pinned");
358        let ring = pin_endpoint.as_local().expect("local target");
359        let pin_locale = ring.pin_current_locale();
360        let adaptive = pin_locale.as_anon().expect("anon locale");
361        let pin_shape = adaptive.pin_current_shape();
362        assert_eq!(pin_shape.shape(), crate::RingShape::Spsc);
363        assert!(pin_endpoint.is_still_valid()
364            && pin_locale.is_still_valid()
365            && pin_shape.is_still_valid());
366    }
367}