Skip to main content

reddb_server/application/
operation_context.rs

1//! Per-request `OperationContext` — the single bag of state every
2//! port receives.
3//!
4//! Today, ports take `&self` only and reach into hidden runtime
5//! state (transaction map keyed by connection id, audit principal
6//! resolved via thread-local hacks, request id missing entirely).
7//! That spreading makes multi-port invariants — "this request's
8//! port_a call and port_b call must share an xid" — invisible to
9//! the type system and untestable.
10//!
11//! `OperationContext` flips that around: handlers build it once at
12//! request entry and pass it through every port call. Forgetting to
13//! propagate it is a compile error; sharing it across two ports is
14//! a single move.
15//!
16//! `WriteConsent` is a sealed token: it can only be constructed by
17//! `WriteGate::check`, so a port's mutating method that demands
18//! `ctx.write_consent.is_some()` is statically guaranteed to have
19//! passed the gate. Forgetting the gate is impossible at the type
20//! level.
21//!
22//! Migration is gated behind the `ctx-ports` feature flag while the
23//! 9 ports are converted one PR at a time. `OperationContext::implicit()`
24//! returns a no-op context that lets unmigrated callers keep
25//! compiling.
26
27use std::sync::atomic::{AtomicU64, Ordering};
28
29/// Snapshot identifier used for MVCC reads. `None` means autocommit
30/// — the port allocates a fresh snapshot per call (current default
31/// for unwrapped paths).
32pub type Xid = u64;
33
34/// Sealed write-permission token. Construct via
35/// `WriteGate::check`; cannot be assembled by application code,
36/// even within this crate, because the inner field is private and
37/// marked `PhantomData<*const ()>` — and the `_seal` field can only
38/// be created inside `runtime::write_gate`.
39#[derive(Debug, Clone)]
40pub struct WriteConsent {
41    pub(crate) kind: crate::runtime::write_gate::WriteKind,
42    pub(crate) _seal: WriteConsentSeal,
43}
44
45/// Module-private marker. Public type, but the only constructor lives
46/// in `runtime::write_gate::WriteConsentSeal::new()`, which is only
47/// callable from the write-gate module. Code outside that module can
48/// pattern-match on this struct but cannot construct it, so building
49/// a `WriteConsent` requires going through the gate.
50#[derive(Debug, Clone)]
51pub struct WriteConsentSeal {
52    _private: std::marker::PhantomData<*const ()>,
53}
54
55// SAFETY: `WriteConsentSeal` carries no owned state; the
56// `PhantomData<*const ()>` marker is unsendable by default to
57// discourage cross-thread token sharing without explicit auditing.
58// We re-add Send+Sync because `WriteConsent` itself is Clone and
59// gets stored on `OperationContext`, which crosses async/Tokio
60// task boundaries on every request. The marker only exists to
61// gate construction; thread-safety of the runtime gate it
62// represents is not affected by sending the token.
63unsafe impl Send for WriteConsentSeal {}
64unsafe impl Sync for WriteConsentSeal {}
65
66impl WriteConsentSeal {
67    /// Create the sealed marker. Pub only within the crate so the
68    /// write-gate module can mint tokens; everything else must
69    /// call `WriteGate::check`.
70    pub(crate) fn new() -> Self {
71        Self {
72            _private: std::marker::PhantomData,
73        }
74    }
75}
76
77/// Per-request context plumbed through every port method.
78#[derive(Debug, Clone)]
79pub struct OperationContext {
80    /// MVCC snapshot id when the request opened a transaction;
81    /// `None` for autocommit reads/writes.
82    pub xid: Option<Xid>,
83    /// Connection identifier the request arrived on; ties this
84    /// context back to per-connection state (current transaction,
85    /// session variables) when needed.
86    pub connection_id: Option<u64>,
87    /// Identity recorded in audit logs. `"anonymous"` when the
88    /// caller did not authenticate.
89    pub audit_principal: String,
90    /// Stable per-request id used for log correlation. Either
91    /// supplied by the caller via `X-Request-Id` or minted as a
92    /// monotonic ULID-like string at request entry.
93    pub request_id: String,
94    /// Sealed gate token, present only when `WriteGate::check`
95    /// granted permission. Mutating port methods demand this is
96    /// `Some(...)`; missing it is a runtime error (and during the
97    /// migration window, a structural reminder that the call site
98    /// hasn't been threaded yet).
99    pub write_consent: Option<WriteConsent>,
100    /// Optional tenant override. `None` falls back to the
101    /// connection's default tenant.
102    pub tenant: Option<String>,
103}
104
105impl OperationContext {
106    /// Anonymous, no-write-consent context. The default for any
107    /// caller that hasn't been migrated to construct an explicit
108    /// context yet — keeps the migration window compilable.
109    pub fn implicit() -> Self {
110        Self {
111            xid: None,
112            connection_id: None,
113            audit_principal: "anonymous".to_string(),
114            request_id: mint_request_id(),
115            write_consent: None,
116            tenant: None,
117        }
118    }
119
120    /// Read-only context bound to a stable request id. Use when the
121    /// handler knows it is dispatching a query that never mutates.
122    pub fn read_only(request_id: impl Into<String>) -> Self {
123        Self {
124            xid: None,
125            connection_id: None,
126            audit_principal: "anonymous".to_string(),
127            request_id: request_id.into(),
128            write_consent: None,
129            tenant: None,
130        }
131    }
132
133    /// Writing context with an attached gate token. Construct
134    /// `WriteConsent` via `WriteGate::check` first; passing the
135    /// token here is the single point that proves the request
136    /// passed the policy.
137    pub fn writing(consent: WriteConsent, request_id: impl Into<String>) -> Self {
138        Self {
139            xid: None,
140            connection_id: None,
141            audit_principal: "anonymous".to_string(),
142            request_id: request_id.into(),
143            write_consent: Some(consent),
144            tenant: None,
145        }
146    }
147
148    pub fn with_principal(mut self, principal: impl Into<String>) -> Self {
149        self.audit_principal = principal.into();
150        self
151    }
152
153    pub fn with_connection(mut self, connection_id: u64) -> Self {
154        self.connection_id = Some(connection_id);
155        self
156    }
157
158    pub fn with_xid(mut self, xid: Xid) -> Self {
159        self.xid = Some(xid);
160        self
161    }
162
163    pub fn with_tenant(mut self, tenant: impl Into<String>) -> Self {
164        self.tenant = Some(tenant.into());
165        self
166    }
167
168    pub fn require_write_consent(&self) -> Result<&WriteConsent, crate::api::RedDBError> {
169        self.write_consent.as_ref().ok_or_else(|| {
170            crate::api::RedDBError::ReadOnly(
171                "operation context is missing WriteConsent — handler must call WriteGate::check"
172                    .to_string(),
173            )
174        })
175    }
176}
177
178/// Monotonic request-id minter used when the caller doesn't supply
179/// one. Format: `req-<unix_micros>-<seq>` — sortable, unique,
180/// human-readable. Not a true ULID (no need to pull in another
181/// crate just for this).
182fn mint_request_id() -> String {
183    static SEQ: AtomicU64 = AtomicU64::new(0);
184    let seq = SEQ.fetch_add(1, Ordering::Relaxed);
185    let now_us = std::time::SystemTime::now()
186        .duration_since(std::time::UNIX_EPOCH)
187        .map(|d| d.as_micros() as u64)
188        .unwrap_or(0);
189    format!("req-{now_us}-{seq}")
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn implicit_has_no_write_consent() {
198        let ctx = OperationContext::implicit();
199        assert!(ctx.write_consent.is_none());
200        assert_eq!(ctx.audit_principal, "anonymous");
201        assert!(!ctx.request_id.is_empty());
202    }
203
204    #[test]
205    fn read_only_constructor_carries_supplied_request_id() {
206        let ctx = OperationContext::read_only("req-abc");
207        assert_eq!(ctx.request_id, "req-abc");
208        assert!(ctx.write_consent.is_none());
209    }
210
211    #[test]
212    fn require_write_consent_errors_when_missing() {
213        let ctx = OperationContext::implicit();
214        let err = ctx.require_write_consent().unwrap_err();
215        assert!(matches!(err, crate::api::RedDBError::ReadOnly(_)));
216    }
217
218    #[test]
219    fn request_ids_are_monotonic_within_process() {
220        let a = mint_request_id();
221        let b = mint_request_id();
222        assert_ne!(a, b);
223    }
224
225    #[test]
226    fn builder_setters_compose() {
227        let ctx = OperationContext::read_only("req-1")
228            .with_principal("operator")
229            .with_connection(42)
230            .with_xid(7)
231            .with_tenant("acme");
232        assert_eq!(ctx.audit_principal, "operator");
233        assert_eq!(ctx.connection_id, Some(42));
234        assert_eq!(ctx.xid, Some(7));
235        assert_eq!(ctx.tenant.as_deref(), Some("acme"));
236    }
237}