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::authz::{self, Access, Action, Guard, IdentityInterceptor, Principal};
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 guard: Guard,
53}
54
55impl PeerServerSvc {
56 #[must_use]
59 pub fn new(connection: Arc<Connection>, config: PeerServerConfig) -> Self {
60 Self {
61 connection,
62 cache: QueryCache::new(),
63 config,
64 metrics: Arc::new(Metrics::default()),
65 guard: Guard::disabled(),
66 }
67 }
68
69 #[must_use]
71 pub fn with_guard(mut self, guard: Guard) -> Self {
72 self.guard = guard;
73 self
74 }
75
76 #[must_use]
78 pub fn metrics(&self) -> Arc<Metrics> {
79 Arc::clone(&self.metrics)
80 }
81
82 async fn authorize<T>(&self, request: &Request<T>, access: Access) -> Result<(), Status> {
88 let principal: Principal = authz::principal(request);
89 match self.guard.authorize(&principal, &access).await {
90 Ok(None) => Ok(()),
91 Ok(Some(_filter)) => Err(Status::unimplemented(
92 "view filtering is not enforced on this surface yet",
93 )),
94 Err(error) => Err(error.to_status()),
95 }
96 }
97
98 fn db_target(&self) -> String {
100 self.connection.db_name().to_owned()
101 }
102
103 fn view_db(&self, spec: Option<&pb::DbViewSpec>) -> Result<Db, Status> {
104 let spec = spec.ok_or_else(|| Status::invalid_argument("request names no database"))?;
105 if spec.db != self.connection.db_name() {
106 return Err(Status::not_found(format!(
107 "this peer server hosts {:?}, not {:?}",
108 self.connection.db_name(),
109 spec.db
110 )));
111 }
112 let base = self.connection.db();
113 Ok(match &spec.view {
114 Some(pb::db_view_spec::View::AsOf(t)) => base.as_of(*t),
115 Some(pb::db_view_spec::View::Since(t)) => base.since(*t),
116 Some(pb::db_view_spec::View::History(true)) => base.history(),
117 None | Some(pb::db_view_spec::View::History(false)) => base,
118 })
119 }
120}
121
122fn decode_edn(bytes: &[u8], what: &str) -> Result<Edn, Status> {
123 codec::decode_edn(bytes)
124 .map_err(|error| Status::invalid_argument(format!("bad {what}: {error}")))
125}
126
127fn resolve_eid(db: &Db, form: &Edn) -> Result<EntityId, Status> {
128 match form {
129 Edn::Long(n) => u64::try_from(*n)
130 .map(EntityId::from_raw)
131 .map_err(|_| Status::invalid_argument("negative entity id")),
132 Edn::Tagged(tag, value) if tag == "eid" => match value.as_ref() {
133 Edn::Long(n) => u64::try_from(*n)
134 .map(EntityId::from_raw)
135 .map_err(|_| Status::invalid_argument("negative entity id")),
136 _ => Err(Status::invalid_argument("#eid requires a long")),
137 },
138 Edn::Keyword(keyword) => db
139 .idents()
140 .entid(keyword)
141 .ok_or_else(|| Status::invalid_argument(format!("unknown ident {keyword}"))),
142 Edn::Vector(items) => {
143 let [attr_form, value_form] = items.as_slice() else {
144 return Err(Status::invalid_argument("lookup ref requires [attr value]"));
145 };
146 let attr = attr_form
147 .as_keyword()
148 .and_then(|keyword| db.idents().entid(keyword))
149 .ok_or_else(|| Status::invalid_argument("unknown lookup attribute"))?;
150 let value = boundary::edn_to_value(Some(db), value_form)
151 .ok_or_else(|| Status::invalid_argument("bad lookup value"))?;
152 let value = db.schema().get(attr).map_or(value.clone(), |meta| {
153 exec::coerce_for_type(value, meta.value_type)
154 });
155 db.lookup(attr, &value)
156 .ok_or_else(|| Status::not_found(format!("lookup ref {form} did not resolve")))
157 }
158 other => Err(Status::invalid_argument(format!(
159 "bad entity position {other}"
160 ))),
161 }
162}
163
164type ChunkStream<T> = Pin<Box<dyn Stream<Item = Result<T, Status>> + Send>>;
165
166enum Slot {
167 E,
168 A,
169 V,
170}
171
172fn chunk_stream<T: Send + 'static>(chunks: Vec<T>) -> ChunkStream<T> {
173 Box::pin(tokio_stream::iter(chunks.into_iter().map(Ok)))
174}
175
176#[tonic::async_trait]
177impl PeerServer for PeerServerSvc {
178 type QueryStream = ChunkStream<pb::QueryResultChunk>;
179
180 async fn query(
181 &self,
182 request: Request<pb::QueryRequest>,
183 ) -> Result<Response<Self::QueryStream>, Status> {
184 self.authorize(&request, Access::on(Action::Query, self.db_target()))
185 .await?;
186 let request = request.into_inner();
187 let query_form = decode_edn(&request.query, "query")?;
188 let parsed = self
189 .cache
190 .parse(&query_form)
191 .map_err(|error| Status::invalid_argument(error.to_string()))?;
192 let args = match decode_edn(&request.args, "args")? {
193 Edn::Nil => Vec::new(),
194 Edn::Vector(items) | Edn::List(items) => items,
195 other => {
196 return Err(Status::invalid_argument(format!(
197 "args must be a vector, got {other}"
198 )));
199 }
200 };
201 let mut dbs = Vec::new();
203 for spec in &request.dbs {
204 dbs.push(self.view_db(Some(spec))?);
205 }
206 let mut next_db = 0;
207 let mut next_arg = 0;
208 let mut inputs: Vec<QInput<'_>> = Vec::with_capacity(parsed.inputs.len());
209 for spec in &parsed.inputs {
210 if matches!(spec, ast::InSpec::Db(_)) {
211 let db = dbs
212 .get(next_db)
213 .ok_or_else(|| Status::invalid_argument("query needs more database views"))?;
214 inputs.push(QInput::Db(db));
215 next_db += 1;
216 } else {
217 let arg = args
218 .get(next_arg)
219 .cloned()
220 .ok_or_else(|| Status::invalid_argument("query needs more arguments"))?;
221 inputs.push(QInput::Edn(arg));
222 next_arg += 1;
223 }
224 }
225 let fuel = if request.fuel == 0 {
226 self.config.max_fuel
227 } else {
228 request.fuel.min(self.config.max_fuel)
229 };
230 let started = Instant::now();
231 let (result, report) = corium_query::run(
232 &parsed,
233 &inputs,
234 ExecOptions {
235 fuel: Some(fuel),
236 ..ExecOptions::default()
237 },
238 )
239 .map_err(|error| Status::invalid_argument(error.to_string()))?;
240 self.metrics
241 .record_query(started.elapsed(), report.datoms_scanned);
242 tracing::debug!(
243 db = %self.connection.db_name(),
244 datoms_scanned = report.datoms_scanned,
245 elapsed_micros = started.elapsed().as_micros(),
246 "query completed"
247 );
248 let (shape, rows) = match (&parsed.find, result) {
249 (ast::FindSpec::Rel(_), Edn::Vector(rows)) => (pb::ResultShape::Relation, rows),
250 (ast::FindSpec::Coll(_), Edn::Vector(rows)) => (pb::ResultShape::Collection, rows),
251 (ast::FindSpec::Tuple(_), value) => (pb::ResultShape::Tuple, vec![value]),
252 (ast::FindSpec::Scalar(_), value) => (pb::ResultShape::Scalar, vec![value]),
253 (_, value) => (pb::ResultShape::Relation, vec![value]),
254 };
255 let mut chunks = Vec::new();
256 match shape {
257 pb::ResultShape::Tuple | pb::ResultShape::Scalar => {
258 chunks.push(pb::QueryResultChunk {
259 shape: shape.into(),
260 rows: codec::encode_edn(&rows.into_iter().next().unwrap_or(Edn::Nil)),
261 last: true,
262 });
263 }
264 _ => {
265 let groups: Vec<&[Edn]> = if rows.is_empty() {
266 vec![&[]]
267 } else {
268 rows.chunks(self.config.chunk_size.max(1)).collect()
269 };
270 let total = groups.len();
271 for (index, group) in groups.into_iter().enumerate() {
272 chunks.push(pb::QueryResultChunk {
273 shape: shape.into(),
274 rows: codec::encode_edn(&Edn::Vector(group.to_vec())),
275 last: index + 1 == total,
276 });
277 }
278 }
279 }
280 Ok(Response::new(chunk_stream(chunks)))
281 }
282
283 async fn pull(
284 &self,
285 request: Request<pb::PullRequest>,
286 ) -> Result<Response<pb::PullResponse>, Status> {
287 self.authorize(&request, Access::on(Action::Pull, self.db_target()))
288 .await?;
289 let request = request.into_inner();
290 let db = self.view_db(request.db.as_ref())?;
291 let pattern = decode_edn(&request.pattern, "pull pattern")?;
292 let eid = resolve_eid(&db, &decode_edn(&request.eid, "entity")?)?;
293 let result = corium_query::pull(&db, &pattern, eid)
294 .map_err(|error| Status::invalid_argument(error.to_string()))?;
295 Ok(Response::new(pb::PullResponse {
296 result: codec::encode_edn(&result),
297 }))
298 }
299
300 async fn transact(
301 &self,
302 request: Request<pb::TransactRequest>,
303 ) -> Result<Response<pb::TransactResponse>, Status> {
304 self.authorize(&request, Access::on(Action::Transact, self.db_target()))
305 .await?;
306 let request = request.into_inner();
307 if request.db != self.connection.db_name() {
308 return Err(Status::not_found(format!(
309 "this peer server hosts {:?}, not {:?}",
310 self.connection.db_name(),
311 request.db
312 )));
313 }
314 let response = self
315 .connection
316 .transact_raw(request.tx_data)
317 .await
318 .map_err(|error| match error {
319 crate::PeerError::Rpc(status) => status,
320 other => Status::internal(other.to_string()),
321 })?;
322 self.connection
325 .sync_to(response.basis_t)
326 .await
327 .map_err(|error| Status::internal(error.to_string()))?;
328 Ok(Response::new(response))
329 }
330
331 type DatomsStream = ChunkStream<pb::DatomChunk>;
332
333 #[allow(clippy::too_many_lines)]
334 async fn datoms(
335 &self,
336 request: Request<pb::DatomsRequest>,
337 ) -> Result<Response<Self::DatomsStream>, Status> {
338 self.authorize(&request, Access::on(Action::Datoms, self.db_target()))
339 .await?;
340 let request = request.into_inner();
341 let db = self.view_db(request.db.as_ref())?;
342 let order = match request.index.as_str() {
343 "eavt" => IndexOrder::Eavt,
344 "aevt" => IndexOrder::Aevt,
345 "avet" => IndexOrder::Avet,
346 "vaet" => IndexOrder::Vaet,
347 other => {
348 return Err(Status::invalid_argument(format!("unknown index {other:?}")));
349 }
350 };
351 let components = match decode_edn(&request.components, "components")? {
352 Edn::Nil => Vec::new(),
353 Edn::Vector(items) | Edn::List(items) => items,
354 other => {
355 return Err(Status::invalid_argument(format!(
356 "components must be a vector, got {other}"
357 )));
358 }
359 };
360 let positions = match order {
362 IndexOrder::Eavt => [Slot::E, Slot::A, Slot::V],
363 IndexOrder::Aevt => [Slot::A, Slot::E, Slot::V],
364 IndexOrder::Avet => [Slot::A, Slot::V, Slot::E],
365 IndexOrder::Vaet => [Slot::V, Slot::A, Slot::E],
366 };
367 if components.len() > 3 {
368 return Err(Status::invalid_argument("at most three components"));
369 }
370 let mut e = None;
371 let mut a = None;
372 let mut v = None;
373 for (slot, form) in positions.iter().zip(&components) {
374 match slot {
375 Slot::E => e = Some(resolve_eid(&db, form)?),
376 Slot::A => {
377 a = Some(match form {
378 Edn::Keyword(keyword) => db.idents().entid(keyword).ok_or_else(|| {
379 Status::invalid_argument(format!("unknown attribute {keyword}"))
380 })?,
381 other => resolve_eid(&db, other)?,
382 });
383 }
384 Slot::V => {
385 let value = boundary::edn_to_value(Some(&db), form)
386 .ok_or_else(|| Status::invalid_argument(format!("bad value {form}")))?;
387 let value = a
388 .and_then(|a| db.schema().get(a))
389 .map_or(value.clone(), |meta| {
390 exec::coerce_for_type(value, meta.value_type)
391 });
392 v = Some(value);
393 }
394 }
395 }
396 let prefix = key_prefix(order, e, a, v.as_ref());
397 let limit = if request.limit == 0 {
398 self.config.max_datoms
399 } else {
400 usize::try_from(request.limit)
401 .unwrap_or(usize::MAX)
402 .min(self.config.max_datoms)
403 };
404 let datoms: Vec<corium_core::Datom> = db
405 .datoms_prefix(order, &prefix)
406 .take(limit)
407 .cloned()
408 .collect();
409 let interner = db.interner();
410 let mut chunks = Vec::new();
411 let groups: Vec<&[corium_core::Datom]> = if datoms.is_empty() {
412 vec![&[]]
413 } else {
414 datoms.chunks(self.config.chunk_size.max(1)).collect()
415 };
416 let total = groups.len();
417 for (index, group) in groups.into_iter().enumerate() {
418 let bytes = codec::encode_datoms(group, interner)
419 .map_err(|error| Status::internal(error.to_string()))?;
420 chunks.push(pb::DatomChunk {
421 datoms: bytes,
422 last: index + 1 == total,
423 });
424 }
425 Ok(Response::new(chunk_stream(chunks)))
426 }
427
428 type TxRangeStream = ChunkStream<pb::TxChunk>;
429
430 async fn tx_range(
431 &self,
432 request: Request<pb::TxRangeRequest>,
433 ) -> Result<Response<Self::TxRangeStream>, Status> {
434 self.authorize(&request, Access::on(Action::TxRange, self.db_target()))
435 .await?;
436 let request = request.into_inner();
437 if request.db != self.connection.db_name() {
438 return Err(Status::not_found(format!(
439 "this peer server hosts {:?}, not {:?}",
440 self.connection.db_name(),
441 request.db
442 )));
443 }
444 let end = if request.end == 0 {
445 None
446 } else {
447 Some(request.end)
448 };
449 let records = self.connection.tx_range(request.start, end);
450 let db = self.connection.db();
451 let interner = db.interner();
452 let mut txes = Vec::with_capacity(records.len());
453 for record in records {
454 txes.push(pb::TxReport {
455 t: record.t,
456 tx_instant: record.tx_instant,
457 datoms: codec::encode_datoms(&record.datoms, interner)
458 .map_err(|error| Status::internal(error.to_string()))?,
459 });
460 }
461 let groups: Vec<Vec<pb::TxReport>> = if txes.is_empty() {
462 vec![Vec::new()]
463 } else {
464 txes.chunks(self.config.chunk_size.max(1))
465 .map(<[pb::TxReport]>::to_vec)
466 .collect()
467 };
468 let total = groups.len();
469 let chunks: Vec<pb::TxChunk> = groups
470 .into_iter()
471 .enumerate()
472 .map(|(index, group)| pb::TxChunk {
473 txes: group,
474 last: index + 1 == total,
475 })
476 .collect();
477 Ok(Response::new(chunk_stream(chunks)))
478 }
479
480 async fn db_stats(
481 &self,
482 request: Request<pb::DbStatsRequest>,
483 ) -> Result<Response<pb::DbStatsResponse>, Status> {
484 self.authorize(&request, Access::on(Action::Inspect, self.db_target()))
485 .await?;
486 let request = request.into_inner();
487 let db = self.view_db(request.db.as_ref())?;
488 let stats = db.stats();
489 Ok(Response::new(pb::DbStatsResponse {
490 basis_t: db.basis_t(),
491 datom_count: stats.datoms as u64,
492 entity_count: stats.entities as u64,
493 attribute_count: stats.attributes as u64,
494 }))
495 }
496
497 type SubscribeStream = tonic::Streaming<pb::SubscribeItem>;
498
499 async fn subscribe(
500 &self,
501 request: Request<pb::SubscribeRequest>,
502 ) -> Result<Response<Self::SubscribeStream>, Status> {
503 self.authorize(&request, Access::on(Action::Subscribe, self.db_target()))
504 .await?;
505 let request = request.into_inner();
506 if request.db != self.connection.db_name() {
507 return Err(Status::not_found(format!(
508 "this peer server hosts {:?}, not {:?}",
509 self.connection.db_name(),
510 request.db
511 )));
512 }
513 let upstream = self
514 .connection
515 .subscribe_raw(request.from_basis_t)
516 .await
517 .map_err(|error| match error {
518 crate::PeerError::Rpc(status) => status,
519 other => Status::unavailable(other.to_string()),
520 })?;
521 Ok(Response::new(upstream))
522 }
523}
524
525pub async fn serve(
530 connection: Arc<Connection>,
531 addr: std::net::SocketAddr,
532 guard: Guard,
533 tls: Option<tonic::transport::ServerTlsConfig>,
534 config: PeerServerConfig,
535 shutdown: impl std::future::Future<Output = ()> + Send,
536) -> Result<(), tonic::transport::Error> {
537 serve_service(
538 PeerServerSvc::new(connection, config).with_guard(guard),
539 addr,
540 tls,
541 shutdown,
542 )
543 .await
544}
545
546pub async fn serve_service(
552 service: PeerServerSvc,
553 addr: std::net::SocketAddr,
554 tls: Option<tonic::transport::ServerTlsConfig>,
555 shutdown: impl std::future::Future<Output = ()> + Send,
556) -> Result<(), tonic::transport::Error> {
557 let interceptor = IdentityInterceptor::new(service.guard.clone());
560 let mut builder = tonic::transport::Server::builder();
561 if let Some(tls) = tls {
562 builder = builder.tls_config(tls)?;
563 }
564 builder
565 .add_service(PeerServerServer::with_interceptor(service, interceptor))
566 .serve_with_shutdown(addr, shutdown)
567 .await
568}
569
570pub fn assemble_query_result(chunks: &[pb::QueryResultChunk]) -> Result<Edn, String> {
575 let shape = chunks
576 .first()
577 .map_or(pb::ResultShape::Relation, pb::QueryResultChunk::shape);
578 match shape {
579 pb::ResultShape::Tuple | pb::ResultShape::Scalar => {
580 let bytes = &chunks
581 .first()
582 .ok_or_else(|| "empty result stream".to_owned())?
583 .rows;
584 codec::decode_edn(bytes).map_err(|error| error.to_string())
585 }
586 _ => {
587 let mut rows = Vec::new();
588 for chunk in chunks {
589 match codec::decode_edn(&chunk.rows).map_err(|error| error.to_string())? {
590 Edn::Vector(items) => rows.extend(items),
591 other => return Err(format!("bad result chunk {other}")),
592 }
593 }
594 Ok(Edn::Vector(rows))
595 }
596 }
597}