1pub mod metrics;
11pub mod segment;
12pub mod server;
13
14use std::collections::BTreeMap;
15use std::sync::{Arc, Mutex, RwLock};
16use std::time::Duration;
17
18use corium_core::{Datom, EntityId, KeywordInterner, Schema};
19use corium_db::{Db, Idents};
20use corium_log::TxRecord;
21use corium_protocol::auth::TokenInterceptor;
22use corium_protocol::codec::{self, CodecError};
23use corium_protocol::pb;
24use corium_protocol::pb::catalog_client::CatalogClient;
25use corium_protocol::pb::transactor_client::TransactorClient;
26use corium_query::edn::Edn;
27use thiserror::Error;
28use tokio::sync::{broadcast, watch};
29use tonic::Status;
30use tonic::service::interceptor::InterceptedService;
31use tonic::transport::{Channel, ClientTlsConfig, Endpoint};
32
33type Client = TransactorClient<InterceptedService<Channel, TokenInterceptor>>;
34
35#[derive(Debug, Error)]
37pub enum PeerError {
38 #[error(transparent)]
40 Transport(#[from] tonic::transport::Error),
41 #[error(transparent)]
43 Rpc(#[from] Status),
44 #[error(transparent)]
46 Codec(#[from] CodecError),
47 #[error("protocol error: {0}")]
49 Protocol(String),
50 #[error("connection closed")]
52 Closed,
53}
54
55#[derive(Clone, Debug)]
57pub struct ConnectConfig {
58 pub endpoint: String,
60 pub db: String,
62 pub token: Option<String>,
64 pub tls: Option<ClientTlsConfig>,
66 pub reconnect_min: Duration,
68 pub reconnect_max: Duration,
70}
71
72impl ConnectConfig {
73 #[must_use]
75 pub fn new(endpoint: impl Into<String>, db: impl Into<String>) -> Self {
76 Self {
77 endpoint: endpoint.into(),
78 db: db.into(),
79 token: None,
80 tls: None,
81 reconnect_min: Duration::from_millis(100),
82 reconnect_max: Duration::from_secs(5),
83 }
84 }
85}
86
87#[derive(Clone, Debug)]
89pub struct PeerReport {
90 pub t: u64,
92 pub tx_instant: i64,
94 pub datoms: Vec<Datom>,
96 pub db_after: Db,
98}
99
100#[derive(Clone, Debug)]
102pub struct TxResult {
103 pub basis_before: u64,
105 pub basis_t: u64,
107 pub tx_instant: i64,
109 pub tempids: BTreeMap<String, EntityId>,
111 pub db_after: Db,
113}
114
115struct PeerState {
116 schema: Schema,
117 idents: Idents,
118 interner: KeywordInterner,
119 db: Db,
120 instants: BTreeMap<u64, i64>,
121}
122
123struct Inner {
124 config: ConnectConfig,
125 state: RwLock<Option<PeerState>>,
126 basis: watch::Sender<u64>,
127 index_basis: watch::Sender<u64>,
128 reports: broadcast::Sender<PeerReport>,
129 client: Mutex<Client>,
130}
131
132pub struct Connection {
134 inner: Arc<Inner>,
135 task: tokio::task::JoinHandle<()>,
136}
137
138impl Drop for Connection {
139 fn drop(&mut self) {
140 self.task.abort();
141 }
142}
143
144async fn open_channel(config: &ConnectConfig) -> Result<Channel, PeerError> {
145 let mut endpoint = Endpoint::from_shared(config.endpoint.clone())
146 .map_err(|error| PeerError::Protocol(format!("bad endpoint: {error}")))?
147 .connect_timeout(Duration::from_secs(10));
148 if let Some(tls) = &config.tls {
149 endpoint = endpoint.tls_config(tls.clone())?;
150 }
151 Ok(endpoint.connect().await?)
152}
153
154fn make_client(channel: Channel, token: Option<String>) -> Client {
155 TransactorClient::with_interceptor(channel, TokenInterceptor::new(token))
156}
157
158impl Connection {
159 pub async fn connect(config: ConnectConfig) -> Result<Self, PeerError> {
166 let channel = open_channel(&config).await?;
167 let client = make_client(channel, config.token.clone());
168 let inner = Arc::new(Inner {
169 config,
170 state: RwLock::new(None),
171 basis: watch::channel(0).0,
172 index_basis: watch::channel(0).0,
173 reports: broadcast::channel(1024).0,
174 client: Mutex::new(client.clone()),
175 });
176 let mut stream = subscribe(&inner, 0).await?;
179 let handshake_basis = pump_handshake(&inner, &mut stream).await?;
180 drain_until(&inner, &mut stream, handshake_basis).await?;
181 let task_inner = Arc::clone(&inner);
182 let task = tokio::spawn(async move {
183 run_loop(task_inner, Some(stream)).await;
184 });
185 Ok(Self { inner, task })
186 }
187
188 #[must_use]
190 pub fn db_name(&self) -> &str {
191 &self.inner.config.db
192 }
193
194 #[must_use]
201 pub fn db(&self) -> Db {
202 self.inner
203 .state
204 .read()
205 .unwrap_or_else(std::sync::PoisonError::into_inner)
206 .as_ref()
207 .expect("connection is initialized")
208 .db
209 .clone()
210 }
211
212 #[must_use]
214 pub fn basis_t(&self) -> u64 {
215 *self.inner.basis.subscribe().borrow()
216 }
217
218 #[must_use]
221 pub fn index_basis_t(&self) -> u64 {
222 *self.inner.index_basis.subscribe().borrow()
223 }
224
225 #[must_use]
227 pub fn tx_reports(&self) -> broadcast::Receiver<PeerReport> {
228 self.inner.reports.subscribe()
229 }
230
231 #[must_use]
234 pub fn tx_range(&self, start: u64, end: Option<u64>) -> Vec<TxRecord> {
235 let guard = self
236 .inner
237 .state
238 .read()
239 .unwrap_or_else(std::sync::PoisonError::into_inner);
240 let Some(state) = guard.as_ref() else {
241 return Vec::new();
242 };
243 state
244 .db
245 .tx_range(start, end)
246 .into_iter()
247 .map(|(t, datoms)| TxRecord {
248 t,
249 tx_instant: state.instants.get(&t).copied().unwrap_or_default(),
250 datoms,
251 })
252 .collect()
253 }
254
255 pub async fn transact(&self, forms: Vec<Edn>) -> Result<TxResult, PeerError> {
261 let response = self
262 .transact_raw(codec::encode_edn(&Edn::Vector(forms)))
263 .await?;
264 let tempids = decode_tempids(&response.tempids)?;
265 let db_after = self.sync_to(response.basis_t).await?;
266 Ok(TxResult {
267 basis_before: response.basis_before,
268 basis_t: response.basis_t,
269 tx_instant: response.tx_instant,
270 tempids,
271 db_after,
272 })
273 }
274
275 pub async fn transact_raw(&self, tx_data: Vec<u8>) -> Result<pb::TransactResponse, PeerError> {
281 let request = pb::TransactRequest {
282 db: self.inner.config.db.clone(),
283 protocol_version: corium_protocol::PROTOCOL_VERSION,
284 tx_data,
285 };
286 let mut client = self
287 .inner
288 .client
289 .lock()
290 .unwrap_or_else(std::sync::PoisonError::into_inner)
291 .clone();
292 Ok(client.transact(request).await?.into_inner())
293 }
294
295 pub async fn sync(&self) -> Result<Db, PeerError> {
300 let mut client = self
301 .inner
302 .client
303 .lock()
304 .unwrap_or_else(std::sync::PoisonError::into_inner)
305 .clone();
306 let response = client
307 .sync(pb::SyncRequest {
308 db: self.inner.config.db.clone(),
309 t: 0,
310 })
311 .await?
312 .into_inner();
313 self.sync_to(response.basis_t).await
314 }
315
316 pub async fn sync_to(&self, t: u64) -> Result<Db, PeerError> {
321 let mut basis = self.inner.basis.subscribe();
322 loop {
323 if *basis.borrow() >= t {
324 return Ok(self.db());
325 }
326 basis.changed().await.map_err(|_| PeerError::Closed)?;
327 }
328 }
329
330 pub async fn status(&self) -> Result<pb::StatusResponse, PeerError> {
335 let mut client = self
336 .inner
337 .client
338 .lock()
339 .unwrap_or_else(std::sync::PoisonError::into_inner)
340 .clone();
341 Ok(client
342 .status(pb::StatusRequest {
343 db: self.inner.config.db.clone(),
344 })
345 .await?
346 .into_inner())
347 }
348
349 pub async fn subscribe_raw(
355 &self,
356 from_basis_t: u64,
357 ) -> Result<tonic::Streaming<pb::SubscribeItem>, PeerError> {
358 let mut client = self
359 .inner
360 .client
361 .lock()
362 .unwrap_or_else(std::sync::PoisonError::into_inner)
363 .clone();
364 Ok(client
365 .subscribe(pb::SubscribeRequest {
366 db: self.inner.config.db.clone(),
367 protocol_version: corium_protocol::PROTOCOL_VERSION,
368 from_basis_t,
369 })
370 .await?
371 .into_inner())
372 }
373}
374
375fn decode_tempids(bytes: &[u8]) -> Result<BTreeMap<String, EntityId>, PeerError> {
376 let Edn::Map(pairs) = codec::decode_edn(bytes)? else {
377 return Err(PeerError::Protocol("tempids must be a map".into()));
378 };
379 let mut tempids = BTreeMap::new();
380 for (key, value) in pairs {
381 let (Edn::Str(name), Edn::Long(raw)) = (key, value) else {
382 return Err(PeerError::Protocol("bad tempid entry".into()));
383 };
384 let raw = u64::try_from(raw).map_err(|_| PeerError::Protocol("bad entity id".into()))?;
385 tempids.insert(name, EntityId::from_raw(raw));
386 }
387 Ok(tempids)
388}
389
390async fn subscribe(
391 inner: &Arc<Inner>,
392 from_basis_t: u64,
393) -> Result<tonic::Streaming<pb::SubscribeItem>, PeerError> {
394 let mut client = inner
395 .client
396 .lock()
397 .unwrap_or_else(std::sync::PoisonError::into_inner)
398 .clone();
399 Ok(client
400 .subscribe(pb::SubscribeRequest {
401 db: inner.config.db.clone(),
402 protocol_version: corium_protocol::PROTOCOL_VERSION,
403 from_basis_t,
404 })
405 .await?
406 .into_inner())
407}
408
409async fn pump_handshake(
412 inner: &Arc<Inner>,
413 stream: &mut tonic::Streaming<pb::SubscribeItem>,
414) -> Result<u64, PeerError> {
415 let first = stream
416 .message()
417 .await?
418 .and_then(|item| item.item)
419 .ok_or_else(|| PeerError::Protocol("subscription ended before handshake".into()))?;
420 let pb::subscribe_item::Item::Handshake(handshake) = first else {
421 return Err(PeerError::Protocol(
422 "subscription must begin with a handshake".into(),
423 ));
424 };
425 let (schema, idents) = codec::decode_schema(&handshake.schema)?;
426 {
427 let mut guard = inner
428 .state
429 .write()
430 .unwrap_or_else(std::sync::PoisonError::into_inner);
431 if let Some(state) = guard.as_mut() {
432 state.schema = schema;
435 state.idents = idents;
436 } else {
437 let interner = KeywordInterner::default();
438 let db = Db::new(schema.clone()).with_naming(idents.clone(), interner.clone());
439 *guard = Some(PeerState {
440 schema,
441 idents,
442 interner,
443 db,
444 instants: BTreeMap::new(),
445 });
446 }
447 }
448 let _ = inner.index_basis.send_replace(handshake.index_basis_t);
449 Ok(handshake.basis_t)
450}
451
452async fn drain_until(
454 inner: &Arc<Inner>,
455 stream: &mut tonic::Streaming<pb::SubscribeItem>,
456 target: u64,
457) -> Result<(), PeerError> {
458 while *inner.basis.subscribe().borrow() < target {
459 let Some(item) = stream.message().await? else {
460 return Err(PeerError::Protocol(
461 "subscription ended during backfill".into(),
462 ));
463 };
464 if let Some(item) = item.item {
465 apply_item(inner, item)?;
466 }
467 }
468 Ok(())
469}
470
471fn apply_item(inner: &Arc<Inner>, item: pb::subscribe_item::Item) -> Result<(), PeerError> {
472 match item {
473 pb::subscribe_item::Item::Report(report) => {
474 let peer_report = {
475 let mut guard = inner
476 .state
477 .write()
478 .unwrap_or_else(std::sync::PoisonError::into_inner);
479 let state = guard
480 .as_mut()
481 .ok_or_else(|| PeerError::Protocol("report before handshake".into()))?;
482 if report.t <= state.db.basis_t() {
483 return Ok(());
484 }
485 let before = state.interner.len();
486 let datoms = codec::decode_datoms(&report.datoms, &mut state.interner)?;
487 if state.interner.len() > before {
488 state.db = state
489 .db
490 .clone()
491 .with_naming(state.idents.clone(), state.interner.clone());
492 }
493 state.db = state.db.with_transaction(report.t, &datoms);
494 state.instants.insert(report.t, report.tx_instant);
495 PeerReport {
496 t: report.t,
497 tx_instant: report.tx_instant,
498 datoms,
499 db_after: state.db.clone(),
500 }
501 };
502 let _ = inner.basis.send_replace(peer_report.t);
503 let _ = inner.reports.send(peer_report);
504 }
505 pb::subscribe_item::Item::IndexBasis(index) => {
506 let _ = inner.index_basis.send_replace(index.index_basis_t);
507 }
508 pb::subscribe_item::Item::Heartbeat(_) | pb::subscribe_item::Item::Handshake(_) => {}
509 }
510 Ok(())
511}
512
513async fn run_loop(inner: Arc<Inner>, initial: Option<tonic::Streaming<pb::SubscribeItem>>) {
517 let mut stream = initial;
518 let mut backoff = inner.config.reconnect_min;
519 loop {
520 if let Some(active) = stream.as_mut() {
521 match active.message().await {
522 Ok(Some(item)) => {
523 backoff = inner.config.reconnect_min;
524 if let Some(item) = item.item {
525 if apply_item(&inner, item).is_err() {
526 stream = None;
527 }
528 }
529 continue;
530 }
531 Ok(None) | Err(_) => {
532 stream = None;
533 }
534 }
535 }
536 tokio::time::sleep(backoff).await;
537 backoff = (backoff * 2).min(inner.config.reconnect_max);
538 match open_channel(&inner.config).await {
539 Ok(channel) => {
540 let client = make_client(channel, inner.config.token.clone());
541 *inner
542 .client
543 .lock()
544 .unwrap_or_else(std::sync::PoisonError::into_inner) = client;
545 }
546 Err(_) => continue,
547 }
548 let from = *inner.basis.subscribe().borrow();
549 if let Ok(mut fresh) = subscribe(&inner, from).await {
550 if pump_handshake(&inner, &mut fresh).await.is_ok() {
551 stream = Some(fresh);
552 }
553 }
554 }
555}
556
557pub struct Admin {
559 client: CatalogClient<InterceptedService<Channel, TokenInterceptor>>,
560}
561
562impl Admin {
563 pub async fn connect(
568 endpoint: &str,
569 token: Option<String>,
570 tls: Option<ClientTlsConfig>,
571 ) -> Result<Self, PeerError> {
572 let config = ConnectConfig {
573 tls,
574 token: token.clone(),
575 ..ConnectConfig::new(endpoint, "")
576 };
577 let channel = open_channel(&config).await?;
578 Ok(Self {
579 client: CatalogClient::with_interceptor(channel, TokenInterceptor::new(token)),
580 })
581 }
582
583 pub async fn create_database(&mut self, db: &str, schema: &[Edn]) -> Result<bool, PeerError> {
588 let response = self
589 .client
590 .create_database(pb::CreateDatabaseRequest {
591 db: db.to_owned(),
592 schema: codec::encode_edn(&Edn::Vector(schema.to_vec())),
593 })
594 .await?;
595 Ok(response.into_inner().created)
596 }
597
598 pub async fn delete_database(&mut self, db: &str) -> Result<bool, PeerError> {
603 let response = self
604 .client
605 .delete_database(pb::DeleteDatabaseRequest { db: db.to_owned() })
606 .await?;
607 Ok(response.into_inner().deleted)
608 }
609
610 pub async fn list_databases(&mut self) -> Result<Vec<String>, PeerError> {
615 let response = self
616 .client
617 .list_databases(pb::ListDatabasesRequest {})
618 .await?;
619 Ok(response.into_inner().dbs)
620 }
621
622 pub async fn gc_deleted_databases(&mut self) -> Result<u64, PeerError> {
627 self.gc_deleted_databases_with_retention(None).await
628 }
629
630 pub async fn gc_deleted_databases_with_retention(
635 &mut self,
636 retention: Option<Duration>,
637 ) -> Result<u64, PeerError> {
638 let response = self
639 .client
640 .gc_deleted_databases(pb::GcDeletedDatabasesRequest {
641 retention_millis: retention.map(duration_millis),
642 })
643 .await?;
644 Ok(response.into_inner().swept_blobs)
645 }
646}
647
648fn duration_millis(duration: Duration) -> u64 {
649 u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655
656 #[test]
657 fn gc_retention_wire_value_preserves_zero_and_subseconds() {
658 assert_eq!(duration_millis(Duration::ZERO), 0);
659 assert_eq!(duration_millis(Duration::from_millis(500)), 500);
660 }
661}