1pub mod authz;
11pub mod metrics;
12pub mod segment;
13pub mod server;
14
15use std::collections::BTreeMap;
16use std::sync::{Arc, Mutex, RwLock};
17use std::time::Duration;
18
19use corium_core::{Datom, EntityId, KeywordInterner, Schema};
20use corium_db::{Db, Idents};
21use corium_log::TxRecord;
22use corium_protocol::auth::TokenInterceptor;
23use corium_protocol::codec::{self, CodecError};
24use corium_protocol::pb;
25use corium_protocol::pb::catalog_client::CatalogClient;
26use corium_protocol::pb::transactor_client::TransactorClient;
27use corium_query::edn::Edn;
28use thiserror::Error;
29use tokio::sync::{broadcast, watch};
30use tonic::Status;
31use tonic::service::interceptor::InterceptedService;
32use tonic::transport::{Channel, ClientTlsConfig, Endpoint};
33
34pub use crate::segment::CachedPeerStorage;
35use crate::segment::{PeerStorage, SnapshotError, load_current_snapshot};
36pub use corium_store::{SegmentCacheConfig, SegmentCacheMetrics};
37
38type Client = TransactorClient<InterceptedService<Channel, TokenInterceptor>>;
39
40#[derive(Debug, Error)]
42pub enum PeerError {
43 #[error(transparent)]
45 Transport(#[from] tonic::transport::Error),
46 #[error(transparent)]
48 Rpc(#[from] Status),
49 #[error(transparent)]
51 Codec(#[from] CodecError),
52 #[error(transparent)]
54 Snapshot(#[from] SnapshotError),
55 #[error("protocol error: {0}")]
57 Protocol(String),
58 #[error("connection closed")]
60 Closed,
61}
62
63#[derive(Clone)]
65pub struct ConnectConfig {
66 pub endpoints: Vec<String>,
72 pub db: String,
74 pub token: Option<String>,
76 pub tls: Option<ClientTlsConfig>,
78 pub reconnect_min: Duration,
80 pub reconnect_max: Duration,
82 pub failover_timeout: Duration,
88 pub heartbeat_timeout: Option<Duration>,
92 storage: Option<Arc<dyn PeerStorage>>,
95 segment_cache_metrics: Option<SegmentCacheMetricsHandle>,
96}
97
98#[derive(Clone)]
99pub(crate) struct SegmentCacheMetricsHandle {
100 pub(crate) metrics: Arc<SegmentCacheMetrics>,
101 pub(crate) disk_capacity: u64,
102 pub(crate) memory_capacity: u64,
103}
104
105impl std::fmt::Debug for ConnectConfig {
106 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 formatter
108 .debug_struct("ConnectConfig")
109 .field("endpoints", &self.endpoints)
110 .field("db", &self.db)
111 .field("token", &self.token.as_ref().map(|_| "[REDACTED]"))
112 .field("tls", &self.tls.is_some())
113 .field("reconnect_min", &self.reconnect_min)
114 .field("reconnect_max", &self.reconnect_max)
115 .field("failover_timeout", &self.failover_timeout)
116 .field("heartbeat_timeout", &self.heartbeat_timeout)
117 .field("storage", &self.storage.is_some())
118 .field("segment_cache", &self.segment_cache_metrics.is_some())
119 .finish()
120 }
121}
122
123impl ConnectConfig {
124 #[must_use]
126 pub fn new(endpoint: impl Into<String>, db: impl Into<String>) -> Self {
127 Self::with_failover(vec![endpoint.into()], db)
128 }
129
130 #[must_use]
132 pub fn with_failover(endpoints: Vec<String>, db: impl Into<String>) -> Self {
133 Self {
134 endpoints,
135 db: db.into(),
136 token: None,
137 tls: None,
138 reconnect_min: Duration::from_millis(100),
139 reconnect_max: Duration::from_secs(5),
140 failover_timeout: Duration::from_secs(30),
141 heartbeat_timeout: None,
142 storage: None,
143 segment_cache_metrics: None,
144 }
145 }
146
147 #[must_use]
153 pub fn with_storage(mut self, storage: Arc<dyn PeerStorage>) -> Self {
154 self.storage = Some(storage);
155 self.segment_cache_metrics = None;
156 self
157 }
158
159 pub fn with_storage_cache(
164 mut self,
165 storage: Arc<dyn PeerStorage>,
166 cache: &SegmentCacheConfig,
167 ) -> std::io::Result<Self> {
168 let cached = CachedPeerStorage::open(storage, cache)?;
169 self.segment_cache_metrics = Some(SegmentCacheMetricsHandle {
170 metrics: cached.metrics(),
171 disk_capacity: cache.capacity_bytes,
172 memory_capacity: cache.memory_capacity_bytes,
173 });
174 self.storage = Some(Arc::new(cached));
175 Ok(self)
176 }
177
178 #[must_use]
180 pub fn has_storage(&self) -> bool {
181 self.storage.is_some()
182 }
183}
184
185#[derive(Clone, Debug)]
187pub struct PeerReport {
188 pub t: u64,
190 pub tx_instant: i64,
192 pub datoms: Vec<Datom>,
194 pub db_after: Db,
196}
197
198#[derive(Clone, Debug)]
200pub struct TxResult {
201 pub basis_before: u64,
203 pub basis_t: u64,
205 pub tx_instant: i64,
207 pub tempids: BTreeMap<String, EntityId>,
209 pub db_after: Db,
211}
212
213struct PeerState {
214 schema: Schema,
215 idents: Idents,
216 interner: KeywordInterner,
217 db: Db,
218 instants: BTreeMap<u64, i64>,
219}
220
221struct Inner {
222 config: ConnectConfig,
223 state: RwLock<Option<PeerState>>,
224 basis: watch::Sender<u64>,
225 index_basis: watch::Sender<u64>,
226 reports: broadcast::Sender<PeerReport>,
227 client: Mutex<Client>,
228 endpoint_index: std::sync::atomic::AtomicUsize,
231 heartbeat_ms: std::sync::atomic::AtomicU64,
234}
235
236impl Inner {
237 fn heartbeat_deadline(&self) -> Option<Duration> {
240 if let Some(timeout) = self.config.heartbeat_timeout {
241 return Some(timeout);
242 }
243 let advertised = self.heartbeat_ms.load(std::sync::atomic::Ordering::Relaxed);
244 (advertised > 0).then(|| Duration::from_millis(advertised.saturating_mul(3)))
245 }
246}
247
248pub struct Connection {
250 inner: Arc<Inner>,
251 task: tokio::task::JoinHandle<()>,
252}
253
254impl Drop for Connection {
255 fn drop(&mut self) {
256 self.task.abort();
257 }
258}
259
260async fn open_channel(config: &ConnectConfig, endpoint: &str) -> Result<Channel, PeerError> {
261 let mut endpoint = Endpoint::from_shared(endpoint.to_owned())
262 .map_err(|error| PeerError::Protocol(format!("bad endpoint: {error}")))?
263 .connect_timeout(Duration::from_secs(10));
264 if let Some(tls) = &config.tls {
265 endpoint = endpoint.tls_config(tls.clone())?;
266 }
267 Ok(endpoint.connect().await?)
268}
269
270fn make_client(channel: Channel, token: Option<String>) -> Client {
271 TransactorClient::with_interceptor(channel, TokenInterceptor::new(token))
272}
273
274impl Connection {
275 pub(crate) fn segment_cache_metrics(&self) -> Option<SegmentCacheMetricsHandle> {
276 self.inner.config.segment_cache_metrics.clone()
277 }
278
279 pub async fn connect(config: ConnectConfig) -> Result<Self, PeerError> {
289 if config.endpoints.is_empty() {
290 return Err(PeerError::Protocol("no endpoints configured".into()));
291 }
292 let snapshot = match &config.storage {
293 Some(storage) => load_current_snapshot(storage.as_ref(), &config.db).await?,
294 None => None,
295 };
296 let start_basis = snapshot.as_ref().map_or(0, Db::basis_t);
297 let mut last_error = PeerError::Closed;
300 let mut first = None;
301 for (index, endpoint) in config.endpoints.iter().enumerate() {
302 match open_channel(&config, endpoint).await {
303 Ok(channel) => {
304 let mut client = make_client(channel, config.token.clone());
305 match subscribe_with(&mut client, &config.db, start_basis).await {
306 Ok(stream) => {
307 first = Some((index, client, stream));
308 break;
309 }
310 Err(error) => last_error = error,
311 }
312 }
313 Err(error) => last_error = error,
314 }
315 }
316 let Some((index, client, mut stream)) = first else {
317 return Err(last_error);
318 };
319 let initial_state = snapshot.map(|db| PeerState {
320 schema: db.schema().clone(),
321 idents: db.idents().clone(),
322 interner: db.interner().clone(),
323 db,
324 instants: BTreeMap::new(),
325 });
326 let inner = Arc::new(Inner {
327 state: RwLock::new(initial_state),
328 basis: watch::channel(start_basis).0,
329 index_basis: watch::channel(0).0,
330 reports: broadcast::channel(1024).0,
331 client: Mutex::new(client),
332 endpoint_index: std::sync::atomic::AtomicUsize::new(index),
333 heartbeat_ms: std::sync::atomic::AtomicU64::new(0),
334 config,
335 });
336 let handshake_basis = pump_handshake(&inner, &mut stream).await?;
337 drain_until(&inner, &mut stream, handshake_basis).await?;
338 let task_inner = Arc::clone(&inner);
339 let task = tokio::spawn(async move {
340 run_loop(task_inner, Some(stream)).await;
341 });
342 Ok(Self { inner, task })
343 }
344
345 #[must_use]
347 pub fn db_name(&self) -> &str {
348 &self.inner.config.db
349 }
350
351 #[must_use]
358 pub fn db(&self) -> Db {
359 self.inner
360 .state
361 .read()
362 .unwrap_or_else(std::sync::PoisonError::into_inner)
363 .as_ref()
364 .expect("connection is initialized")
365 .db
366 .clone()
367 }
368
369 #[must_use]
371 pub fn basis_t(&self) -> u64 {
372 *self.inner.basis.subscribe().borrow()
373 }
374
375 #[must_use]
378 pub fn index_basis_t(&self) -> u64 {
379 *self.inner.index_basis.subscribe().borrow()
380 }
381
382 #[must_use]
384 pub fn tx_reports(&self) -> broadcast::Receiver<PeerReport> {
385 self.inner.reports.subscribe()
386 }
387
388 #[must_use]
391 pub fn tx_range(&self, start: u64, end: Option<u64>) -> Vec<TxRecord> {
392 let guard = self
393 .inner
394 .state
395 .read()
396 .unwrap_or_else(std::sync::PoisonError::into_inner);
397 let Some(state) = guard.as_ref() else {
398 return Vec::new();
399 };
400 state
401 .db
402 .tx_range(start, end)
403 .into_iter()
404 .map(|(t, datoms)| TxRecord {
405 t,
406 tx_instant: state.instants.get(&t).copied().unwrap_or_default(),
407 datoms,
408 })
409 .collect()
410 }
411
412 pub async fn transact(&self, forms: Vec<Edn>) -> Result<TxResult, PeerError> {
427 self.transact_at(forms, None).await
428 }
429
430 pub async fn transact_at(
436 &self,
437 forms: Vec<Edn>,
438 expected_basis_t: Option<u64>,
439 ) -> Result<TxResult, PeerError> {
440 let tx_data = codec::encode_edn(&Edn::Vector(forms));
441 let deadline = tokio::time::Instant::now() + self.inner.config.failover_timeout;
442 let response = loop {
443 match self
444 .transact_raw_at(tx_data.clone(), expected_basis_t)
445 .await
446 {
447 Ok(response) => break response,
448 Err(error) if retry_is_safe(&error) && tokio::time::Instant::now() < deadline => {
449 tokio::time::sleep(self.inner.config.reconnect_min).await;
450 }
451 Err(error) => return Err(error),
452 }
453 };
454 let tempids = decode_tempids(&response.tempids)?;
455 let db_after = self.sync_to(response.basis_t).await?;
456 Ok(TxResult {
457 basis_before: response.basis_before,
458 basis_t: response.basis_t,
459 tx_instant: response.tx_instant,
460 tempids,
461 db_after,
462 })
463 }
464
465 pub async fn transact_raw(&self, tx_data: Vec<u8>) -> Result<pb::TransactResponse, PeerError> {
471 self.transact_raw_at(tx_data, None).await
472 }
473
474 pub async fn transact_raw_at(
480 &self,
481 tx_data: Vec<u8>,
482 expected_basis_t: Option<u64>,
483 ) -> Result<pb::TransactResponse, PeerError> {
484 let request = pb::TransactRequest {
485 db: self.inner.config.db.clone(),
486 protocol_version: corium_protocol::PROTOCOL_VERSION,
487 tx_data,
488 expected_basis_t,
489 };
490 let mut client = self
491 .inner
492 .client
493 .lock()
494 .unwrap_or_else(std::sync::PoisonError::into_inner)
495 .clone();
496 Ok(client.transact(request).await?.into_inner())
497 }
498
499 pub async fn sync(&self) -> Result<Db, PeerError> {
504 let mut client = self
505 .inner
506 .client
507 .lock()
508 .unwrap_or_else(std::sync::PoisonError::into_inner)
509 .clone();
510 let response = client
511 .sync(pb::SyncRequest {
512 db: self.inner.config.db.clone(),
513 t: 0,
514 })
515 .await?
516 .into_inner();
517 self.sync_to(response.basis_t).await
518 }
519
520 pub async fn sync_to(&self, t: u64) -> Result<Db, PeerError> {
525 let mut basis = self.inner.basis.subscribe();
526 loop {
527 if *basis.borrow() >= t {
528 return Ok(self.db());
529 }
530 basis.changed().await.map_err(|_| PeerError::Closed)?;
531 }
532 }
533
534 pub async fn status(&self) -> Result<pb::StatusResponse, PeerError> {
539 let mut client = self
540 .inner
541 .client
542 .lock()
543 .unwrap_or_else(std::sync::PoisonError::into_inner)
544 .clone();
545 Ok(client
546 .status(pb::StatusRequest {
547 db: self.inner.config.db.clone(),
548 })
549 .await?
550 .into_inner())
551 }
552
553 pub async fn subscribe_raw(
559 &self,
560 from_basis_t: u64,
561 ) -> Result<tonic::Streaming<pb::SubscribeItem>, PeerError> {
562 let mut client = self
563 .inner
564 .client
565 .lock()
566 .unwrap_or_else(std::sync::PoisonError::into_inner)
567 .clone();
568 Ok(client
569 .subscribe(pb::SubscribeRequest {
570 db: self.inner.config.db.clone(),
571 protocol_version: corium_protocol::PROTOCOL_VERSION,
572 from_basis_t,
573 })
574 .await?
575 .into_inner())
576 }
577}
578
579fn retry_is_safe(error: &PeerError) -> bool {
582 match error {
583 PeerError::Transport(_) => true,
585 PeerError::Rpc(status) => match status.code() {
586 tonic::Code::FailedPrecondition => {
589 let message = status.message();
590 message.contains("standby") || message.contains("deposed")
591 }
592 tonic::Code::Unavailable => status.message().contains("connect"),
596 _ => false,
597 },
598 _ => false,
599 }
600}
601
602fn decode_tempids(bytes: &[u8]) -> Result<BTreeMap<String, EntityId>, PeerError> {
603 let Edn::Map(pairs) = codec::decode_edn(bytes)? else {
604 return Err(PeerError::Protocol("tempids must be a map".into()));
605 };
606 let mut tempids = BTreeMap::new();
607 for (key, value) in pairs {
608 let (Edn::Str(name), Edn::Long(raw)) = (key, value) else {
609 return Err(PeerError::Protocol("bad tempid entry".into()));
610 };
611 let raw = u64::try_from(raw).map_err(|_| PeerError::Protocol("bad entity id".into()))?;
612 tempids.insert(name, EntityId::from_raw(raw));
613 }
614 Ok(tempids)
615}
616
617async fn subscribe_with(
618 client: &mut Client,
619 db: &str,
620 from_basis_t: u64,
621) -> Result<tonic::Streaming<pb::SubscribeItem>, PeerError> {
622 Ok(client
623 .subscribe(pb::SubscribeRequest {
624 db: db.to_owned(),
625 protocol_version: corium_protocol::PROTOCOL_VERSION,
626 from_basis_t,
627 })
628 .await?
629 .into_inner())
630}
631
632async fn pump_handshake(
635 inner: &Arc<Inner>,
636 stream: &mut tonic::Streaming<pb::SubscribeItem>,
637) -> Result<u64, PeerError> {
638 let first = stream
639 .message()
640 .await?
641 .and_then(|item| item.item)
642 .ok_or_else(|| PeerError::Protocol("subscription ended before handshake".into()))?;
643 let pb::subscribe_item::Item::Handshake(handshake) = first else {
644 return Err(PeerError::Protocol(
645 "subscription must begin with a handshake".into(),
646 ));
647 };
648 let local_basis = *inner.basis.subscribe().borrow();
649 if handshake.basis_t < local_basis {
650 return Err(PeerError::Protocol(format!(
651 "published snapshot basis {local_basis} is newer than transactor basis {}",
652 handshake.basis_t
653 )));
654 }
655 let (schema, idents) = codec::decode_schema(&handshake.schema)?;
656 inner.heartbeat_ms.store(
657 handshake.heartbeat_interval_ms,
658 std::sync::atomic::Ordering::Relaxed,
659 );
660 {
661 let mut guard = inner
662 .state
663 .write()
664 .unwrap_or_else(std::sync::PoisonError::into_inner);
665 if let Some(state) = guard.as_mut() {
666 state.schema = schema;
669 state.idents = idents;
670 } else {
671 let interner = KeywordInterner::default();
672 let db = Db::new(schema.clone()).with_naming(idents.clone(), interner.clone());
673 *guard = Some(PeerState {
674 schema,
675 idents,
676 interner,
677 db,
678 instants: BTreeMap::new(),
679 });
680 }
681 }
682 let _ = inner.index_basis.send_replace(handshake.index_basis_t);
683 Ok(handshake.basis_t)
684}
685
686async fn drain_until(
688 inner: &Arc<Inner>,
689 stream: &mut tonic::Streaming<pb::SubscribeItem>,
690 target: u64,
691) -> Result<(), PeerError> {
692 while *inner.basis.subscribe().borrow() < target {
693 let Some(item) = stream.message().await? else {
694 return Err(PeerError::Protocol(
695 "subscription ended during backfill".into(),
696 ));
697 };
698 if let Some(item) = item.item {
699 apply_item(inner, item)?;
700 }
701 }
702 Ok(())
703}
704
705fn apply_item(inner: &Arc<Inner>, item: pb::subscribe_item::Item) -> Result<(), PeerError> {
706 match item {
707 pb::subscribe_item::Item::Report(report) => {
708 let peer_report = {
709 let mut guard = inner
710 .state
711 .write()
712 .unwrap_or_else(std::sync::PoisonError::into_inner);
713 let state = guard
714 .as_mut()
715 .ok_or_else(|| PeerError::Protocol("report before handshake".into()))?;
716 if report.t <= state.db.basis_t() {
717 return Ok(());
718 }
719 let before = state.interner.len();
720 let datoms = codec::decode_datoms(&report.datoms, &mut state.interner)?;
721 if state.interner.len() > before {
722 state.db = state
723 .db
724 .clone()
725 .with_naming(state.idents.clone(), state.interner.clone());
726 }
727 state.db = state.db.with_transaction(report.t, &datoms);
728 state.instants.insert(report.t, report.tx_instant);
729 PeerReport {
730 t: report.t,
731 tx_instant: report.tx_instant,
732 datoms,
733 db_after: state.db.clone(),
734 }
735 };
736 let _ = inner.basis.send_replace(peer_report.t);
737 let _ = inner.reports.send(peer_report);
738 }
739 pb::subscribe_item::Item::IndexBasis(index) => {
740 let _ = inner.index_basis.send_replace(index.index_basis_t);
741 }
742 pb::subscribe_item::Item::Heartbeat(_) | pb::subscribe_item::Item::Handshake(_) => {}
743 }
744 Ok(())
745}
746
747async fn run_loop(inner: Arc<Inner>, initial: Option<tonic::Streaming<pb::SubscribeItem>>) {
754 let mut stream = initial;
755 let mut backoff = inner.config.reconnect_min;
756 let mut candidate = inner
757 .endpoint_index
758 .load(std::sync::atomic::Ordering::Relaxed);
759 loop {
760 if let Some(active) = stream.as_mut() {
761 let next = match inner.heartbeat_deadline() {
762 Some(deadline) => match tokio::time::timeout(deadline, active.message()).await {
763 Ok(next) => next,
764 Err(_elapsed) => Ok(None),
768 },
769 None => active.message().await,
770 };
771 match next {
772 Ok(Some(item)) => {
773 backoff = inner.config.reconnect_min;
774 if let Some(item) = item.item
775 && apply_item(&inner, item).is_err()
776 {
777 stream = None;
778 }
779 continue;
780 }
781 Ok(None) | Err(_) => {
782 stream = None;
783 }
784 }
785 }
786 tokio::time::sleep(backoff).await;
787 backoff = (backoff * 2).min(inner.config.reconnect_max);
788 let endpoints = &inner.config.endpoints;
789 let endpoint = &endpoints[candidate % endpoints.len()];
790 let Ok(channel) = open_channel(&inner.config, endpoint).await else {
791 candidate = (candidate + 1) % endpoints.len();
792 continue;
793 };
794 let mut client = make_client(channel, inner.config.token.clone());
795 let from = *inner.basis.subscribe().borrow();
796 match subscribe_with(&mut client, &inner.config.db, from).await {
797 Ok(mut fresh) => {
798 if pump_handshake(&inner, &mut fresh).await.is_ok() {
799 *inner
801 .client
802 .lock()
803 .unwrap_or_else(std::sync::PoisonError::into_inner) = client;
804 inner.endpoint_index.store(
805 candidate % endpoints.len(),
806 std::sync::atomic::Ordering::Relaxed,
807 );
808 stream = Some(fresh);
809 } else {
810 candidate = (candidate + 1) % endpoints.len();
811 }
812 }
813 Err(_) => {
814 candidate = (candidate + 1) % endpoints.len();
815 }
816 }
817 }
818}
819
820pub struct Admin {
822 client: CatalogClient<InterceptedService<Channel, TokenInterceptor>>,
823}
824
825impl Admin {
826 pub async fn connect(
831 endpoint: &str,
832 token: Option<String>,
833 tls: Option<ClientTlsConfig>,
834 ) -> Result<Self, PeerError> {
835 let config = ConnectConfig {
836 tls,
837 token: token.clone(),
838 ..ConnectConfig::new(endpoint, "")
839 };
840 let channel = open_channel(&config, endpoint).await?;
841 Ok(Self {
842 client: CatalogClient::with_interceptor(channel, TokenInterceptor::new(token)),
843 })
844 }
845
846 pub async fn create_database(&mut self, db: &str, schema: &[Edn]) -> Result<bool, PeerError> {
851 let response = self
852 .client
853 .create_database(pb::CreateDatabaseRequest {
854 db: db.to_owned(),
855 schema: codec::encode_edn(&Edn::Vector(schema.to_vec())),
856 })
857 .await?;
858 Ok(response.into_inner().created)
859 }
860
861 pub async fn fork_database(
869 &mut self,
870 db: &str,
871 target: &str,
872 as_of_t: Option<u64>,
873 ) -> Result<Option<u64>, PeerError> {
874 let response = self
875 .client
876 .fork_database(pb::ForkDatabaseRequest {
877 db: db.to_owned(),
878 target: target.to_owned(),
879 as_of_t: as_of_t.unwrap_or(0),
880 })
881 .await?
882 .into_inner();
883 Ok(response.created.then_some(response.basis_t))
884 }
885
886 pub async fn delete_database(&mut self, db: &str) -> Result<bool, PeerError> {
891 let response = self
892 .client
893 .delete_database(pb::DeleteDatabaseRequest { db: db.to_owned() })
894 .await?;
895 Ok(response.into_inner().deleted)
896 }
897
898 pub async fn list_databases(&mut self) -> Result<Vec<String>, PeerError> {
903 let response = self
904 .client
905 .list_databases(pb::ListDatabasesRequest {})
906 .await?;
907 Ok(response.into_inner().dbs)
908 }
909
910 pub async fn gc_deleted_databases(&mut self) -> Result<u64, PeerError> {
915 self.gc_deleted_databases_with_retention(None).await
916 }
917
918 pub async fn gc_deleted_databases_with_retention(
923 &mut self,
924 retention: Option<Duration>,
925 ) -> Result<u64, PeerError> {
926 let response = self
927 .client
928 .gc_deleted_databases(pb::GcDeletedDatabasesRequest {
929 retention_millis: retention.map(duration_millis),
930 })
931 .await?;
932 Ok(response.into_inner().swept_blobs)
933 }
934
935 pub async fn request_index(&mut self, db: &str) -> Result<u64, PeerError> {
941 let response = self
942 .client
943 .request_index(pb::RequestIndexRequest { db: db.to_owned() })
944 .await?;
945 Ok(response.into_inner().index_basis_t)
946 }
947
948 pub async fn set_index_policy(
955 &mut self,
956 db: &str,
957 update: IndexPolicySettings,
958 ) -> Result<IndexPolicySettings, PeerError> {
959 let response = self
960 .client
961 .set_index_policy(pb::SetIndexPolicyRequest {
962 db: db.to_owned(),
963 interval_ms: update.interval_ms,
964 backoff: update.backoff,
965 tail_threshold: update.tail_threshold,
966 tail_deadline_ms: update.tail_deadline_ms,
967 })
968 .await?
969 .into_inner();
970 Ok(IndexPolicySettings {
971 interval_ms: Some(response.interval_ms),
972 backoff: Some(response.backoff),
973 tail_threshold: Some(response.tail_threshold),
974 tail_deadline_ms: Some(response.tail_deadline_ms),
975 })
976 }
977
978 pub async fn get_storage_info(
985 &mut self,
986 db: &str,
987 ) -> Result<pb::GetStorageInfoResponse, PeerError> {
988 Ok(self
989 .client
990 .get_storage_info(pb::GetStorageInfoRequest { db: db.to_owned() })
991 .await?
992 .into_inner())
993 }
994}
995
996#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1000pub struct IndexPolicySettings {
1001 pub interval_ms: Option<u64>,
1003 pub backoff: Option<u32>,
1006 pub tail_threshold: Option<u64>,
1009 pub tail_deadline_ms: Option<u64>,
1011}
1012
1013fn duration_millis(duration: Duration) -> u64 {
1014 u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
1015}
1016
1017#[cfg(test)]
1018mod tests {
1019 use super::*;
1020
1021 #[test]
1022 fn gc_retention_wire_value_preserves_zero_and_subseconds() {
1023 assert_eq!(duration_millis(Duration::ZERO), 0);
1024 assert_eq!(duration_millis(Duration::from_millis(500)), 500);
1025 }
1026}