1use 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#[derive(Clone, Copy, Debug)]
27pub struct PeerServerConfig {
28 pub max_fuel: u64,
30 pub chunk_size: usize,
32 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
46pub struct PeerServerSvc {
48 connection: Arc<Connection>,
49 cache: QueryCache,
50 config: PeerServerConfig,
51 metrics: Arc<Metrics>,
52}
53
54impl PeerServerSvc {
55 #[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 #[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 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 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 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
480pub 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
502pub 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
527pub 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}