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