Skip to main content

osproxy_spi/
routing.rs

1//! The low-level routing contract.
2
3use crate::decision::RouteDecision;
4use crate::error::SpiError;
5use crate::request::RequestCtx;
6
7/// Decides where and how a single request is routed.
8///
9/// This is the low-level contract for a single routing *decision*: full control
10/// over the destination and the transforms. Most implementers instead provide a
11/// [`crate::TenancySpi`], which `osproxy-tenancy` adapts into a `RoutingSpi`.
12///
13/// Note this yields only a [`RouteDecision`]. The engine pipeline needs more than
14/// a decision (the resolved partition, epoch, and migration phase, to construct
15/// ids, demux bulk, and gate writes), so it is generic over the richer
16/// `osproxy_tenancy::Router` seam rather than this trait. Implement `Router` to
17/// drive the engine with custom routing; implement `RoutingSpi` where only a
18/// `RouteDecision` is required.
19///
20/// # Invariants
21///
22/// - MUST resolve to exactly one [`Target`](osproxy_core::Target), no
23///   synchronous fan-out in v1 (ADR-002).
24/// - MUST NOT panic; return [`SpiError`] for every failure (NFR-R1).
25/// - The returned [`RouteDecision::epoch`] MUST come from the placement state
26///   the decision was derived from, so the sink can detect a stale-epoch write
27///   during a migration (`docs/06` §2).
28///
29/// The engine drives implementations through generics (monomorphized, no dyn
30/// dispatch on the hot path), so the future's `Send`-ness is checked at the
31/// spawn site.
32///
33/// # Examples
34///
35/// ```
36/// use osproxy_core::{ClusterId, Epoch, IndexName, Target};
37/// use osproxy_spi::{Protocol, RequestCtx, RouteDecision, RoutingSpi, SpiError};
38///
39/// struct PinToOne;
40///
41/// impl RoutingSpi for PinToOne {
42///     async fn route(&self, _ctx: &RequestCtx<'_>) -> Result<RouteDecision, SpiError> {
43///         let target = Target::new(ClusterId::from("only"), IndexName::from("logs"));
44///         Ok(RouteDecision::passthrough(target, Protocol::Http1, Epoch::ZERO))
45///     }
46/// }
47/// ```
48#[allow(
49    async_fn_in_trait,
50    reason = "implementations are consumed through generics in the engine, where \
51              Send is verified at the spawn site; no public dyn boundary needs an \
52              explicit Send bound in M1 (docs/02 §1)"
53)]
54pub trait RoutingSpi: Send + Sync + 'static {
55    /// Resolves the routing decision for an authenticated request.
56    ///
57    /// # Errors
58    ///
59    /// Returns [`SpiError`] when the partition cannot be resolved, no placement
60    /// exists, the placement backend is unavailable, or the endpoint is
61    /// unsupported.
62    async fn route(&self, ctx: &RequestCtx<'_>) -> Result<RouteDecision, SpiError>;
63}