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