Skip to main content

corium_peer/
server.rs

1//! Peer server: hosts a peer for thin clients over the public
2//! `PeerServerService` gRPC surface (see `docs/design/protocol.md`).
3//!
4//! Queries execute server-side against the hosted peer's local database
5//! values with per-request fuel and chunked result streams.
6
7use std::pin::Pin;
8use std::sync::Arc;
9use std::time::Instant;
10
11use corium_core::{EntityId, IndexOrder};
12use corium_db::{Db, key_prefix};
13use corium_protocol::auth::{AuthInterceptor, Authenticator};
14use corium_protocol::codec;
15use corium_protocol::pb;
16use corium_protocol::pb::peer_server_server::{PeerServer, PeerServerServer};
17use corium_query::edn::Edn;
18use corium_query::{ExecOptions, QInput, QueryCache, ast, boundary, exec};
19use tokio_stream::Stream;
20use tonic::{Request, Response, Status};
21
22use crate::Connection;
23use crate::metrics::Metrics;
24
25/// Peer-server execution limits.
26#[derive(Clone, Copy, Debug)]
27pub struct PeerServerConfig {
28    /// Fuel ceiling per query (datoms touched).
29    pub max_fuel: u64,
30    /// Rows or datoms per streamed chunk.
31    pub chunk_size: usize,
32    /// Maximum datoms returned by one `Datoms` request.
33    pub max_datoms: usize,
34}
35
36impl Default for PeerServerConfig {
37    fn default() -> Self {
38        Self {
39            max_fuel: 10_000_000,
40            chunk_size: 1024,
41            max_datoms: 1_000_000,
42        }
43    }
44}
45
46/// The hosted-peer gRPC service.
47pub struct PeerServerSvc {
48    connection: Arc<Connection>,
49    cache: QueryCache,
50    config: PeerServerConfig,
51    metrics: Arc<Metrics>,
52}
53
54impl PeerServerSvc {
55    /// Hosts `connection` with the given limits.
56    #[must_use]
57    pub fn new(connection: Arc<Connection>, config: PeerServerConfig) -> Self {
58        Self {
59            connection,
60            cache: QueryCache::new(),
61            config,
62            metrics: Arc::new(Metrics::default()),
63        }
64    }
65
66    /// Process query metrics suitable for a Prometheus endpoint.
67    #[must_use]
68    pub fn metrics(&self) -> Arc<Metrics> {
69        Arc::clone(&self.metrics)
70    }
71
72    fn view_db(&self, spec: Option<&pb::DbViewSpec>) -> Result<Db, Status> {
73        let spec = spec.ok_or_else(|| Status::invalid_argument("request names no database"))?;
74        if spec.db != self.connection.db_name() {
75            return Err(Status::not_found(format!(
76                "this peer server hosts {:?}, not {:?}",
77                self.connection.db_name(),
78                spec.db
79            )));
80        }
81        let base = self.connection.db();
82        Ok(match &spec.view {
83            Some(pb::db_view_spec::View::AsOf(t)) => base.as_of(*t),
84            Some(pb::db_view_spec::View::Since(t)) => base.since(*t),
85            Some(pb::db_view_spec::View::History(true)) => base.history(),
86            None | Some(pb::db_view_spec::View::History(false)) => base,
87        })
88    }
89}
90
91fn decode_edn(bytes: &[u8], what: &str) -> Result<Edn, Status> {
92    codec::decode_edn(bytes)
93        .map_err(|error| Status::invalid_argument(format!("bad {what}: {error}")))
94}
95
96fn resolve_eid(db: &Db, form: &Edn) -> Result<EntityId, Status> {
97    match form {
98        Edn::Long(n) => u64::try_from(*n)
99            .map(EntityId::from_raw)
100            .map_err(|_| Status::invalid_argument("negative entity id")),
101        Edn::Tagged(tag, value) if tag == "eid" => match value.as_ref() {
102            Edn::Long(n) => u64::try_from(*n)
103                .map(EntityId::from_raw)
104                .map_err(|_| Status::invalid_argument("negative entity id")),
105            _ => Err(Status::invalid_argument("#eid requires a long")),
106        },
107        Edn::Keyword(keyword) => db
108            .idents()
109            .entid(keyword)
110            .ok_or_else(|| Status::invalid_argument(format!("unknown ident {keyword}"))),
111        Edn::Vector(items) => {
112            let [attr_form, value_form] = items.as_slice() else {
113                return Err(Status::invalid_argument("lookup ref requires [attr value]"));
114            };
115            let attr = attr_form
116                .as_keyword()
117                .and_then(|keyword| db.idents().entid(keyword))
118                .ok_or_else(|| Status::invalid_argument("unknown lookup attribute"))?;
119            let value = boundary::edn_to_value(Some(db), value_form)
120                .ok_or_else(|| Status::invalid_argument("bad lookup value"))?;
121            let value = db.schema().get(attr).map_or(value.clone(), |meta| {
122                exec::coerce_for_type(value, meta.value_type)
123            });
124            db.lookup(attr, &value)
125                .ok_or_else(|| Status::not_found(format!("lookup ref {form} did not resolve")))
126        }
127        other => Err(Status::invalid_argument(format!(
128            "bad entity position {other}"
129        ))),
130    }
131}
132
133type ChunkStream<T> = Pin<Box<dyn Stream<Item = Result<T, Status>> + Send>>;
134
135enum Slot {
136    E,
137    A,
138    V,
139}
140
141fn chunk_stream<T: Send + 'static>(chunks: Vec<T>) -> ChunkStream<T> {
142    Box::pin(tokio_stream::iter(chunks.into_iter().map(Ok)))
143}
144
145#[tonic::async_trait]
146impl PeerServer for PeerServerSvc {
147    type QueryStream = ChunkStream<pb::QueryResultChunk>;
148
149    async fn query(
150        &self,
151        request: Request<pb::QueryRequest>,
152    ) -> Result<Response<Self::QueryStream>, Status> {
153        let request = request.into_inner();
154        let query_form = decode_edn(&request.query, "query")?;
155        let parsed = self
156            .cache
157            .parse(&query_form)
158            .map_err(|error| Status::invalid_argument(error.to_string()))?;
159        let args = match decode_edn(&request.args, "args")? {
160            Edn::Nil => Vec::new(),
161            Edn::Vector(items) | Edn::List(items) => items,
162            other => {
163                return Err(Status::invalid_argument(format!(
164                    "args must be a vector, got {other}"
165                )));
166            }
167        };
168        // Bind database views and arguments positionally per the :in spec.
169        let mut dbs = Vec::new();
170        for spec in &request.dbs {
171            dbs.push(self.view_db(Some(spec))?);
172        }
173        let mut next_db = 0;
174        let mut next_arg = 0;
175        let mut inputs: Vec<QInput<'_>> = Vec::with_capacity(parsed.inputs.len());
176        for spec in &parsed.inputs {
177            if matches!(spec, ast::InSpec::Db(_)) {
178                let db = dbs
179                    .get(next_db)
180                    .ok_or_else(|| Status::invalid_argument("query needs more database views"))?;
181                inputs.push(QInput::Db(db));
182                next_db += 1;
183            } else {
184                let arg = args
185                    .get(next_arg)
186                    .cloned()
187                    .ok_or_else(|| Status::invalid_argument("query needs more arguments"))?;
188                inputs.push(QInput::Edn(arg));
189                next_arg += 1;
190            }
191        }
192        let fuel = if request.fuel == 0 {
193            self.config.max_fuel
194        } else {
195            request.fuel.min(self.config.max_fuel)
196        };
197        let started = Instant::now();
198        let (result, report) = corium_query::run(
199            &parsed,
200            &inputs,
201            ExecOptions {
202                fuel: Some(fuel),
203                ..ExecOptions::default()
204            },
205        )
206        .map_err(|error| Status::invalid_argument(error.to_string()))?;
207        self.metrics
208            .record_query(started.elapsed(), report.datoms_scanned);
209        tracing::debug!(
210            db = %self.connection.db_name(),
211            datoms_scanned = report.datoms_scanned,
212            elapsed_micros = started.elapsed().as_micros(),
213            "query completed"
214        );
215        let (shape, rows) = match (&parsed.find, result) {
216            (ast::FindSpec::Rel(_), Edn::Vector(rows)) => (pb::ResultShape::Relation, rows),
217            (ast::FindSpec::Coll(_), Edn::Vector(rows)) => (pb::ResultShape::Collection, rows),
218            (ast::FindSpec::Tuple(_), value) => (pb::ResultShape::Tuple, vec![value]),
219            (ast::FindSpec::Scalar(_), value) => (pb::ResultShape::Scalar, vec![value]),
220            (_, value) => (pb::ResultShape::Relation, vec![value]),
221        };
222        let mut chunks = Vec::new();
223        match shape {
224            pb::ResultShape::Tuple | pb::ResultShape::Scalar => {
225                chunks.push(pb::QueryResultChunk {
226                    shape: shape.into(),
227                    rows: codec::encode_edn(&rows.into_iter().next().unwrap_or(Edn::Nil)),
228                    last: true,
229                });
230            }
231            _ => {
232                let groups: Vec<&[Edn]> = if rows.is_empty() {
233                    vec![&[]]
234                } else {
235                    rows.chunks(self.config.chunk_size.max(1)).collect()
236                };
237                let total = groups.len();
238                for (index, group) in groups.into_iter().enumerate() {
239                    chunks.push(pb::QueryResultChunk {
240                        shape: shape.into(),
241                        rows: codec::encode_edn(&Edn::Vector(group.to_vec())),
242                        last: index + 1 == total,
243                    });
244                }
245            }
246        }
247        Ok(Response::new(chunk_stream(chunks)))
248    }
249
250    async fn pull(
251        &self,
252        request: Request<pb::PullRequest>,
253    ) -> Result<Response<pb::PullResponse>, Status> {
254        let request = request.into_inner();
255        let db = self.view_db(request.db.as_ref())?;
256        let pattern = decode_edn(&request.pattern, "pull pattern")?;
257        let eid = resolve_eid(&db, &decode_edn(&request.eid, "entity")?)?;
258        let result = corium_query::pull(&db, &pattern, eid)
259            .map_err(|error| Status::invalid_argument(error.to_string()))?;
260        Ok(Response::new(pb::PullResponse {
261            result: codec::encode_edn(&result),
262        }))
263    }
264
265    async fn transact(
266        &self,
267        request: Request<pb::TransactRequest>,
268    ) -> Result<Response<pb::TransactResponse>, Status> {
269        let request = request.into_inner();
270        if request.db != self.connection.db_name() {
271            return Err(Status::not_found(format!(
272                "this peer server hosts {:?}, not {:?}",
273                self.connection.db_name(),
274                request.db
275            )));
276        }
277        let response = self
278            .connection
279            .transact_raw(request.tx_data)
280            .await
281            .map_err(|error| match error {
282                crate::PeerError::Rpc(status) => status,
283                other => Status::internal(other.to_string()),
284            })?;
285        // Read-your-writes for thin clients: later queries on this server
286        // observe the transaction.
287        self.connection
288            .sync_to(response.basis_t)
289            .await
290            .map_err(|error| Status::internal(error.to_string()))?;
291        Ok(Response::new(response))
292    }
293
294    type DatomsStream = ChunkStream<pb::DatomChunk>;
295
296    #[allow(clippy::too_many_lines)]
297    async fn datoms(
298        &self,
299        request: Request<pb::DatomsRequest>,
300    ) -> Result<Response<Self::DatomsStream>, Status> {
301        let request = request.into_inner();
302        let db = self.view_db(request.db.as_ref())?;
303        let order = match request.index.as_str() {
304            "eavt" => IndexOrder::Eavt,
305            "aevt" => IndexOrder::Aevt,
306            "avet" => IndexOrder::Avet,
307            "vaet" => IndexOrder::Vaet,
308            other => {
309                return Err(Status::invalid_argument(format!("unknown index {other:?}")));
310            }
311        };
312        let components = match decode_edn(&request.components, "components")? {
313            Edn::Nil => Vec::new(),
314            Edn::Vector(items) | Edn::List(items) => items,
315            other => {
316                return Err(Status::invalid_argument(format!(
317                    "components must be a vector, got {other}"
318                )));
319            }
320        };
321        // Components bind positionally in index order and must be a prefix.
322        let positions = match order {
323            IndexOrder::Eavt => [Slot::E, Slot::A, Slot::V],
324            IndexOrder::Aevt => [Slot::A, Slot::E, Slot::V],
325            IndexOrder::Avet => [Slot::A, Slot::V, Slot::E],
326            IndexOrder::Vaet => [Slot::V, Slot::A, Slot::E],
327        };
328        if components.len() > 3 {
329            return Err(Status::invalid_argument("at most three components"));
330        }
331        let mut e = None;
332        let mut a = None;
333        let mut v = None;
334        for (slot, form) in positions.iter().zip(&components) {
335            match slot {
336                Slot::E => e = Some(resolve_eid(&db, form)?),
337                Slot::A => {
338                    a = Some(match form {
339                        Edn::Keyword(keyword) => db.idents().entid(keyword).ok_or_else(|| {
340                            Status::invalid_argument(format!("unknown attribute {keyword}"))
341                        })?,
342                        other => resolve_eid(&db, other)?,
343                    });
344                }
345                Slot::V => {
346                    let value = boundary::edn_to_value(Some(&db), form)
347                        .ok_or_else(|| Status::invalid_argument(format!("bad value {form}")))?;
348                    let value = a
349                        .and_then(|a| db.schema().get(a))
350                        .map_or(value.clone(), |meta| {
351                            exec::coerce_for_type(value, meta.value_type)
352                        });
353                    v = Some(value);
354                }
355            }
356        }
357        let prefix = key_prefix(order, e, a, v.as_ref());
358        let limit = if request.limit == 0 {
359            self.config.max_datoms
360        } else {
361            usize::try_from(request.limit)
362                .unwrap_or(usize::MAX)
363                .min(self.config.max_datoms)
364        };
365        let datoms: Vec<corium_core::Datom> = db
366            .datoms_prefix(order, &prefix)
367            .take(limit)
368            .cloned()
369            .collect();
370        let interner = db.interner();
371        let mut chunks = Vec::new();
372        let groups: Vec<&[corium_core::Datom]> = if datoms.is_empty() {
373            vec![&[]]
374        } else {
375            datoms.chunks(self.config.chunk_size.max(1)).collect()
376        };
377        let total = groups.len();
378        for (index, group) in groups.into_iter().enumerate() {
379            let bytes = codec::encode_datoms(group, interner)
380                .map_err(|error| Status::internal(error.to_string()))?;
381            chunks.push(pb::DatomChunk {
382                datoms: bytes,
383                last: index + 1 == total,
384            });
385        }
386        Ok(Response::new(chunk_stream(chunks)))
387    }
388
389    type TxRangeStream = ChunkStream<pb::TxChunk>;
390
391    async fn tx_range(
392        &self,
393        request: Request<pb::TxRangeRequest>,
394    ) -> Result<Response<Self::TxRangeStream>, Status> {
395        let request = request.into_inner();
396        if request.db != self.connection.db_name() {
397            return Err(Status::not_found(format!(
398                "this peer server hosts {:?}, not {:?}",
399                self.connection.db_name(),
400                request.db
401            )));
402        }
403        let end = if request.end == 0 {
404            None
405        } else {
406            Some(request.end)
407        };
408        let records = self.connection.tx_range(request.start, end);
409        let db = self.connection.db();
410        let interner = db.interner();
411        let mut txes = Vec::with_capacity(records.len());
412        for record in records {
413            txes.push(pb::TxReport {
414                t: record.t,
415                tx_instant: record.tx_instant,
416                datoms: codec::encode_datoms(&record.datoms, interner)
417                    .map_err(|error| Status::internal(error.to_string()))?,
418            });
419        }
420        let groups: Vec<Vec<pb::TxReport>> = if txes.is_empty() {
421            vec![Vec::new()]
422        } else {
423            txes.chunks(self.config.chunk_size.max(1))
424                .map(<[pb::TxReport]>::to_vec)
425                .collect()
426        };
427        let total = groups.len();
428        let chunks: Vec<pb::TxChunk> = groups
429            .into_iter()
430            .enumerate()
431            .map(|(index, group)| pb::TxChunk {
432                txes: group,
433                last: index + 1 == total,
434            })
435            .collect();
436        Ok(Response::new(chunk_stream(chunks)))
437    }
438
439    async fn db_stats(
440        &self,
441        request: Request<pb::DbStatsRequest>,
442    ) -> Result<Response<pb::DbStatsResponse>, Status> {
443        let request = request.into_inner();
444        let db = self.view_db(request.db.as_ref())?;
445        let stats = db.stats();
446        Ok(Response::new(pb::DbStatsResponse {
447            basis_t: db.basis_t(),
448            datom_count: stats.datoms as u64,
449            entity_count: stats.entities as u64,
450            attribute_count: stats.attributes as u64,
451        }))
452    }
453
454    type SubscribeStream = tonic::Streaming<pb::SubscribeItem>;
455
456    async fn subscribe(
457        &self,
458        request: Request<pb::SubscribeRequest>,
459    ) -> Result<Response<Self::SubscribeStream>, Status> {
460        let request = request.into_inner();
461        if request.db != self.connection.db_name() {
462            return Err(Status::not_found(format!(
463                "this peer server hosts {:?}, not {:?}",
464                self.connection.db_name(),
465                request.db
466            )));
467        }
468        let upstream = self
469            .connection
470            .subscribe_raw(request.from_basis_t)
471            .await
472            .map_err(|error| match error {
473                crate::PeerError::Rpc(status) => status,
474                other => Status::unavailable(other.to_string()),
475            })?;
476        Ok(Response::new(upstream))
477    }
478}
479
480/// Serves the peer-server service until `shutdown` resolves.
481///
482/// # Errors
483/// Returns an error when the listener cannot be bound or TLS is invalid.
484pub async fn serve(
485    connection: Arc<Connection>,
486    addr: std::net::SocketAddr,
487    authenticator: Arc<dyn Authenticator>,
488    tls: Option<tonic::transport::ServerTlsConfig>,
489    config: PeerServerConfig,
490    shutdown: impl std::future::Future<Output = ()> + Send,
491) -> Result<(), tonic::transport::Error> {
492    serve_service(
493        PeerServerSvc::new(connection, config),
494        addr,
495        authenticator,
496        tls,
497        shutdown,
498    )
499    .await
500}
501
502/// Serves a pre-built peer service, allowing process wiring to expose its
503/// metrics handle before the server future is awaited.
504///
505/// # Errors
506/// Returns an error when the listener cannot be bound or TLS is invalid.
507pub async fn serve_service(
508    service: PeerServerSvc,
509    addr: std::net::SocketAddr,
510    authenticator: Arc<dyn Authenticator>,
511    tls: Option<tonic::transport::ServerTlsConfig>,
512    shutdown: impl std::future::Future<Output = ()> + Send,
513) -> Result<(), tonic::transport::Error> {
514    let mut builder = tonic::transport::Server::builder();
515    if let Some(tls) = tls {
516        builder = builder.tls_config(tls)?;
517    }
518    builder
519        .add_service(PeerServerServer::with_interceptor(
520            service,
521            AuthInterceptor::new(authenticator),
522        ))
523        .serve_with_shutdown(addr, shutdown)
524        .await
525}
526
527/// Reassembles a chunked query result on the client side.
528///
529/// # Errors
530/// Returns [`Status`]-style codec failures as strings.
531pub fn assemble_query_result(chunks: &[pb::QueryResultChunk]) -> Result<Edn, String> {
532    let shape = chunks
533        .first()
534        .map_or(pb::ResultShape::Relation, pb::QueryResultChunk::shape);
535    match shape {
536        pb::ResultShape::Tuple | pb::ResultShape::Scalar => {
537            let bytes = &chunks
538                .first()
539                .ok_or_else(|| "empty result stream".to_owned())?
540                .rows;
541            codec::decode_edn(bytes).map_err(|error| error.to_string())
542        }
543        _ => {
544            let mut rows = Vec::new();
545            for chunk in chunks {
546                match codec::decode_edn(&chunk.rows).map_err(|error| error.to_string())? {
547                    Edn::Vector(items) => rows.extend(items),
548                    other => return Err(format!("bad result chunk {other}")),
549                }
550            }
551            Ok(Edn::Vector(rows))
552        }
553    }
554}