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