Skip to main content

exoware_server/
engine.rs

1//! Storage callbacks for the store services.
2//!
3//! Implement the capability traits your component serves. String errors are surfaced to clients as
4//! internal RPC failures (message only; keep messages safe to expose if you rely on that). `Ingest`
5//! additionally lets a backend mark a write failure transient via [`IngestError`].
6
7use std::collections::HashMap;
8use std::future::Future;
9
10use buffa::Message;
11use bytes::Bytes;
12use exoware_sdk::common::kv::v1::Entry;
13use exoware_sdk::log::stream::v1::GetResponse as StreamGetResponse;
14use exoware_sdk::prune_policy::PrunePolicyDocument;
15
16/// Backend-defined query metadata.
17///
18/// Keep this lightweight: streaming query RPCs may emit detail on every frame.
19pub type QueryExtra = HashMap<String, buffa_types::google::protobuf::Value>;
20
21#[derive(Clone, Debug, Default)]
22pub struct RangeScanBatch {
23    /// Rows read by this cursor pull.
24    pub rows: Vec<(Bytes, Bytes)>,
25    /// Backend-specific query metadata after reading these rows.
26    pub extra: QueryExtra,
27}
28
29/// Owned pull-based range cursor for query RPCs.
30///
31/// Implementations own any state needed to produce batches, allowing query
32/// handlers to pull rows lazily without borrowing the engine.
33pub trait RangeScan: Send {
34    /// Pull up to `max_items` rows. Returning an empty `rows` batch marks EOF.
35    /// EOF may carry non-empty `extra` with final query metadata.
36    /// `extra` is emitted with the response frame built from the same batch.
37    fn next_batch(
38        &mut self,
39        max_items: usize,
40    ) -> impl Future<Output = Result<RangeScanBatch, String>> + Send;
41}
42
43/// Local sequence frontier visible to this process.
44pub trait Sequence: Send + Sync + 'static {
45    /// Highest sequence number this process can currently serve.
46    fn current_sequence(&self) -> u64;
47}
48
49/// Why an ingest write was not accepted.
50///
51/// Backends choose the variant; the Connect layer maps it to the wire code and retry details.
52#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
53pub enum IngestError {
54    /// The write cannot currently be accepted; clients may retry with backoff.
55    #[error("unavailable: {message}")]
56    Unavailable { message: String },
57
58    /// The write failed in a way retries will not fix.
59    #[error("internal: {message}")]
60    Internal { message: String },
61}
62
63/// Ingest write capability.
64pub trait Ingest: Send + Sync + 'static {
65    /// Persist key-value pairs atomically and return the global sequence number that includes this
66    /// write. Backends may coalesce concurrent writes and return the same sequence number to each
67    /// coalesced caller.
68    fn put_batch(
69        &self,
70        kvs: Vec<(Bytes, Bytes)>,
71    ) -> impl Future<Output = Result<u64, IngestError>> + Send;
72}
73
74/// Query read capability.
75pub trait Query: Sequence {
76    type RangeScan: RangeScan + 'static;
77
78    /// Fetch the value for a single key plus backend-specific query metadata.
79    /// Returns `None` when the key does not exist.
80    fn get(
81        &self,
82        key: Bytes,
83    ) -> impl Future<Output = Result<(Option<Bytes>, QueryExtra), String>> + Send;
84
85    /// Cursor over keys in `[start, end]` (inclusive) when `end` is non-empty;
86    /// empty `end` means unbounded above. Matches `store.query.v1.RangeRequest`
87    /// / `ReduceRequest` on the wire. `limit` caps rows yielded.
88    fn range_scan(
89        &self,
90        start: Bytes,
91        end: Bytes,
92        limit: usize,
93        forward: bool,
94    ) -> impl Future<Output = Result<Self::RangeScan, String>> + Send;
95
96    /// Batch-get plus backend-specific query metadata. Returns `(key, Option<value>)`
97    /// for each input key, preserving order.
98    fn get_many(
99        &self,
100        keys: Vec<Bytes>,
101    ) -> impl Future<Output = Result<(Vec<(Bytes, Option<Bytes>)>, QueryExtra), String>> + Send;
102}
103
104/// Prune mutation capability.
105pub trait Prune: Send + Sync + 'static {
106    /// Apply a validated prune policy document sequentially.
107    fn apply_prune_policies(
108        &self,
109        document: PrunePolicyDocument,
110    ) -> impl Future<Output = Result<(), String>> + Send;
111}
112
113/// Pre-encoded `log.stream.v1.GetResponse` stored for a retained sequence batch.
114#[derive(Clone, Debug)]
115pub struct LogBatch {
116    sequence_number: u64,
117    response_bytes: Bytes,
118}
119
120impl LogBatch {
121    /// Wrap protobuf bytes already encoded as `log.stream.v1.GetResponse`.
122    pub fn from_response_bytes(sequence_number: u64, response_bytes: impl Into<Bytes>) -> Self {
123        Self {
124            sequence_number,
125            response_bytes: response_bytes.into(),
126        }
127    }
128
129    /// Build a log batch from key-value pairs.
130    pub fn from_entries(sequence_number: u64, kvs: Vec<(Bytes, Bytes)>) -> Self {
131        Self::from_response(StreamGetResponse {
132            sequence_number,
133            entries: kvs
134                .into_iter()
135                .map(|(key, value)| Entry {
136                    key: key.to_vec(),
137                    value,
138                    ..Default::default()
139                })
140                .collect(),
141            ..Default::default()
142        })
143    }
144
145    /// Build a log batch from an owned response.
146    pub fn from_response(response: StreamGetResponse) -> Self {
147        Self {
148            sequence_number: response.sequence_number,
149            response_bytes: Bytes::from(response.encode_to_vec()),
150        }
151    }
152
153    /// Sequence number under which this batch was loaded.
154    pub fn sequence_number(&self) -> u64 {
155        self.sequence_number
156    }
157
158    /// Consume the batch into its pre-encoded response bytes.
159    pub fn into_response_bytes(self) -> Bytes {
160        self.response_bytes
161    }
162
163    /// Decode the stored response for paths that need to inspect entries.
164    pub fn decode_response(&self) -> Result<StreamGetResponse, String> {
165        StreamGetResponse::decode_from_slice(&self.response_bytes)
166            .map_err(|err| format!("failed to decode sequence log value: {err}"))
167    }
168}
169
170/// Retained per-sequence batch-log access for stream replay and lookups.
171pub trait Log: Sequence {
172    /// Return the pre-encoded `GetResponse` for the `put_batch` call that was
173    /// assigned `sequence_number`. Return `Ok(None)` when the batch is not
174    /// available from this log.
175    ///
176    /// Engines that don't retain a log return `Ok(None)` unconditionally,
177    /// which disables `GetBatch` and since-cursored `Subscribe` for that
178    /// deployment.
179    ///
180    /// The stream service maps unavailable batches to `BATCH_NOT_FOUND` when
181    /// they are beyond the visible sequence frontier and `BATCH_EVICTED`
182    /// otherwise.
183    fn get_batch(
184        &self,
185        sequence_number: u64,
186    ) -> impl Future<Output = Result<Option<LogBatch>, String>> + Send;
187
188    /// Lowest retained batch sequence number, or `None` when the log is
189    /// empty. Surfaced in `BATCH_EVICTED` error details so clients know where
190    /// to resume from.
191    fn oldest_retained_batch(&self) -> impl Future<Output = Result<Option<u64>, String>> + Send;
192}
193
194/// Compatibility facade for backends that serve every store capability.
195pub trait StoreEngine: Ingest + Query + Prune + Log {}
196
197impl<T: Ingest + Query + Prune + Log> StoreEngine for T {}