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 let tx_data = codec::encode_edn(&Edn::Vector(forms));
428 let deadline = tokio::time::Instant::now() + self.inner.config.failover_timeout;
429 let response = loop {
430 match self.transact_raw(tx_data.clone()).await {
431 Ok(response) => break response,
432 Err(error) if retry_is_safe(&error) && tokio::time::Instant::now() < deadline => {
433 tokio::time::sleep(self.inner.config.reconnect_min).await;
434 }
435 Err(error) => return Err(error),
436 }
437 };
438 let tempids = decode_tempids(&response.tempids)?;
439 let db_after = self.sync_to(response.basis_t).await?;
440 Ok(TxResult {
441 basis_before: response.basis_before,
442 basis_t: response.basis_t,
443 tx_instant: response.tx_instant,
444 tempids,
445 db_after,
446 })
447 }
448
449 pub async fn transact_raw(&self, tx_data: Vec<u8>) -> Result<pb::TransactResponse, PeerError> {
455 let request = pb::TransactRequest {
456 db: self.inner.config.db.clone(),
457 protocol_version: corium_protocol::PROTOCOL_VERSION,
458 tx_data,
459 };
460 let mut client = self
461 .inner
462 .client
463 .lock()
464 .unwrap_or_else(std::sync::PoisonError::into_inner)
465 .clone();
466 Ok(client.transact(request).await?.into_inner())
467 }
468
469 pub async fn sync(&self) -> Result<Db, PeerError> {
474 let mut client = self
475 .inner
476 .client
477 .lock()
478 .unwrap_or_else(std::sync::PoisonError::into_inner)
479 .clone();
480 let response = client
481 .sync(pb::SyncRequest {
482 db: self.inner.config.db.clone(),
483 t: 0,
484 })
485 .await?
486 .into_inner();
487 self.sync_to(response.basis_t).await
488 }
489
490 pub async fn sync_to(&self, t: u64) -> Result<Db, PeerError> {
495 let mut basis = self.inner.basis.subscribe();
496 loop {
497 if *basis.borrow() >= t {
498 return Ok(self.db());
499 }
500 basis.changed().await.map_err(|_| PeerError::Closed)?;
501 }
502 }
503
504 pub async fn status(&self) -> Result<pb::StatusResponse, PeerError> {
509 let mut client = self
510 .inner
511 .client
512 .lock()
513 .unwrap_or_else(std::sync::PoisonError::into_inner)
514 .clone();
515 Ok(client
516 .status(pb::StatusRequest {
517 db: self.inner.config.db.clone(),
518 })
519 .await?
520 .into_inner())
521 }
522
523 pub async fn subscribe_raw(
529 &self,
530 from_basis_t: u64,
531 ) -> Result<tonic::Streaming<pb::SubscribeItem>, PeerError> {
532 let mut client = self
533 .inner
534 .client
535 .lock()
536 .unwrap_or_else(std::sync::PoisonError::into_inner)
537 .clone();
538 Ok(client
539 .subscribe(pb::SubscribeRequest {
540 db: self.inner.config.db.clone(),
541 protocol_version: corium_protocol::PROTOCOL_VERSION,
542 from_basis_t,
543 })
544 .await?
545 .into_inner())
546 }
547}
548
549fn retry_is_safe(error: &PeerError) -> bool {
552 match error {
553 PeerError::Transport(_) => true,
555 PeerError::Rpc(status) => match status.code() {
556 tonic::Code::FailedPrecondition => {
559 let message = status.message();
560 message.contains("standby") || message.contains("deposed")
561 }
562 tonic::Code::Unavailable => status.message().contains("connect"),
566 _ => false,
567 },
568 _ => false,
569 }
570}
571
572fn decode_tempids(bytes: &[u8]) -> Result<BTreeMap<String, EntityId>, PeerError> {
573 let Edn::Map(pairs) = codec::decode_edn(bytes)? else {
574 return Err(PeerError::Protocol("tempids must be a map".into()));
575 };
576 let mut tempids = BTreeMap::new();
577 for (key, value) in pairs {
578 let (Edn::Str(name), Edn::Long(raw)) = (key, value) else {
579 return Err(PeerError::Protocol("bad tempid entry".into()));
580 };
581 let raw = u64::try_from(raw).map_err(|_| PeerError::Protocol("bad entity id".into()))?;
582 tempids.insert(name, EntityId::from_raw(raw));
583 }
584 Ok(tempids)
585}
586
587async fn subscribe_with(
588 client: &mut Client,
589 db: &str,
590 from_basis_t: u64,
591) -> Result<tonic::Streaming<pb::SubscribeItem>, PeerError> {
592 Ok(client
593 .subscribe(pb::SubscribeRequest {
594 db: db.to_owned(),
595 protocol_version: corium_protocol::PROTOCOL_VERSION,
596 from_basis_t,
597 })
598 .await?
599 .into_inner())
600}
601
602async fn pump_handshake(
605 inner: &Arc<Inner>,
606 stream: &mut tonic::Streaming<pb::SubscribeItem>,
607) -> Result<u64, PeerError> {
608 let first = stream
609 .message()
610 .await?
611 .and_then(|item| item.item)
612 .ok_or_else(|| PeerError::Protocol("subscription ended before handshake".into()))?;
613 let pb::subscribe_item::Item::Handshake(handshake) = first else {
614 return Err(PeerError::Protocol(
615 "subscription must begin with a handshake".into(),
616 ));
617 };
618 let local_basis = *inner.basis.subscribe().borrow();
619 if handshake.basis_t < local_basis {
620 return Err(PeerError::Protocol(format!(
621 "published snapshot basis {local_basis} is newer than transactor basis {}",
622 handshake.basis_t
623 )));
624 }
625 let (schema, idents) = codec::decode_schema(&handshake.schema)?;
626 inner.heartbeat_ms.store(
627 handshake.heartbeat_interval_ms,
628 std::sync::atomic::Ordering::Relaxed,
629 );
630 {
631 let mut guard = inner
632 .state
633 .write()
634 .unwrap_or_else(std::sync::PoisonError::into_inner);
635 if let Some(state) = guard.as_mut() {
636 state.schema = schema;
639 state.idents = idents;
640 } else {
641 let interner = KeywordInterner::default();
642 let db = Db::new(schema.clone()).with_naming(idents.clone(), interner.clone());
643 *guard = Some(PeerState {
644 schema,
645 idents,
646 interner,
647 db,
648 instants: BTreeMap::new(),
649 });
650 }
651 }
652 let _ = inner.index_basis.send_replace(handshake.index_basis_t);
653 Ok(handshake.basis_t)
654}
655
656async fn drain_until(
658 inner: &Arc<Inner>,
659 stream: &mut tonic::Streaming<pb::SubscribeItem>,
660 target: u64,
661) -> Result<(), PeerError> {
662 while *inner.basis.subscribe().borrow() < target {
663 let Some(item) = stream.message().await? else {
664 return Err(PeerError::Protocol(
665 "subscription ended during backfill".into(),
666 ));
667 };
668 if let Some(item) = item.item {
669 apply_item(inner, item)?;
670 }
671 }
672 Ok(())
673}
674
675fn apply_item(inner: &Arc<Inner>, item: pb::subscribe_item::Item) -> Result<(), PeerError> {
676 match item {
677 pb::subscribe_item::Item::Report(report) => {
678 let peer_report = {
679 let mut guard = inner
680 .state
681 .write()
682 .unwrap_or_else(std::sync::PoisonError::into_inner);
683 let state = guard
684 .as_mut()
685 .ok_or_else(|| PeerError::Protocol("report before handshake".into()))?;
686 if report.t <= state.db.basis_t() {
687 return Ok(());
688 }
689 let before = state.interner.len();
690 let datoms = codec::decode_datoms(&report.datoms, &mut state.interner)?;
691 if state.interner.len() > before {
692 state.db = state
693 .db
694 .clone()
695 .with_naming(state.idents.clone(), state.interner.clone());
696 }
697 state.db = state.db.with_transaction(report.t, &datoms);
698 state.instants.insert(report.t, report.tx_instant);
699 PeerReport {
700 t: report.t,
701 tx_instant: report.tx_instant,
702 datoms,
703 db_after: state.db.clone(),
704 }
705 };
706 let _ = inner.basis.send_replace(peer_report.t);
707 let _ = inner.reports.send(peer_report);
708 }
709 pb::subscribe_item::Item::IndexBasis(index) => {
710 let _ = inner.index_basis.send_replace(index.index_basis_t);
711 }
712 pb::subscribe_item::Item::Heartbeat(_) | pb::subscribe_item::Item::Handshake(_) => {}
713 }
714 Ok(())
715}
716
717async fn run_loop(inner: Arc<Inner>, initial: Option<tonic::Streaming<pb::SubscribeItem>>) {
724 let mut stream = initial;
725 let mut backoff = inner.config.reconnect_min;
726 let mut candidate = inner
727 .endpoint_index
728 .load(std::sync::atomic::Ordering::Relaxed);
729 loop {
730 if let Some(active) = stream.as_mut() {
731 let next = match inner.heartbeat_deadline() {
732 Some(deadline) => match tokio::time::timeout(deadline, active.message()).await {
733 Ok(next) => next,
734 Err(_elapsed) => Ok(None),
738 },
739 None => active.message().await,
740 };
741 match next {
742 Ok(Some(item)) => {
743 backoff = inner.config.reconnect_min;
744 if let Some(item) = item.item
745 && apply_item(&inner, item).is_err()
746 {
747 stream = None;
748 }
749 continue;
750 }
751 Ok(None) | Err(_) => {
752 stream = None;
753 }
754 }
755 }
756 tokio::time::sleep(backoff).await;
757 backoff = (backoff * 2).min(inner.config.reconnect_max);
758 let endpoints = &inner.config.endpoints;
759 let endpoint = &endpoints[candidate % endpoints.len()];
760 let Ok(channel) = open_channel(&inner.config, endpoint).await else {
761 candidate = (candidate + 1) % endpoints.len();
762 continue;
763 };
764 let mut client = make_client(channel, inner.config.token.clone());
765 let from = *inner.basis.subscribe().borrow();
766 match subscribe_with(&mut client, &inner.config.db, from).await {
767 Ok(mut fresh) => {
768 if pump_handshake(&inner, &mut fresh).await.is_ok() {
769 *inner
771 .client
772 .lock()
773 .unwrap_or_else(std::sync::PoisonError::into_inner) = client;
774 inner.endpoint_index.store(
775 candidate % endpoints.len(),
776 std::sync::atomic::Ordering::Relaxed,
777 );
778 stream = Some(fresh);
779 } else {
780 candidate = (candidate + 1) % endpoints.len();
781 }
782 }
783 Err(_) => {
784 candidate = (candidate + 1) % endpoints.len();
785 }
786 }
787 }
788}
789
790pub struct Admin {
792 client: CatalogClient<InterceptedService<Channel, TokenInterceptor>>,
793}
794
795impl Admin {
796 pub async fn connect(
801 endpoint: &str,
802 token: Option<String>,
803 tls: Option<ClientTlsConfig>,
804 ) -> Result<Self, PeerError> {
805 let config = ConnectConfig {
806 tls,
807 token: token.clone(),
808 ..ConnectConfig::new(endpoint, "")
809 };
810 let channel = open_channel(&config, endpoint).await?;
811 Ok(Self {
812 client: CatalogClient::with_interceptor(channel, TokenInterceptor::new(token)),
813 })
814 }
815
816 pub async fn create_database(&mut self, db: &str, schema: &[Edn]) -> Result<bool, PeerError> {
821 let response = self
822 .client
823 .create_database(pb::CreateDatabaseRequest {
824 db: db.to_owned(),
825 schema: codec::encode_edn(&Edn::Vector(schema.to_vec())),
826 })
827 .await?;
828 Ok(response.into_inner().created)
829 }
830
831 pub async fn fork_database(
839 &mut self,
840 db: &str,
841 target: &str,
842 as_of_t: Option<u64>,
843 ) -> Result<Option<u64>, PeerError> {
844 let response = self
845 .client
846 .fork_database(pb::ForkDatabaseRequest {
847 db: db.to_owned(),
848 target: target.to_owned(),
849 as_of_t: as_of_t.unwrap_or(0),
850 })
851 .await?
852 .into_inner();
853 Ok(response.created.then_some(response.basis_t))
854 }
855
856 pub async fn delete_database(&mut self, db: &str) -> Result<bool, PeerError> {
861 let response = self
862 .client
863 .delete_database(pb::DeleteDatabaseRequest { db: db.to_owned() })
864 .await?;
865 Ok(response.into_inner().deleted)
866 }
867
868 pub async fn list_databases(&mut self) -> Result<Vec<String>, PeerError> {
873 let response = self
874 .client
875 .list_databases(pb::ListDatabasesRequest {})
876 .await?;
877 Ok(response.into_inner().dbs)
878 }
879
880 pub async fn gc_deleted_databases(&mut self) -> Result<u64, PeerError> {
885 self.gc_deleted_databases_with_retention(None).await
886 }
887
888 pub async fn gc_deleted_databases_with_retention(
893 &mut self,
894 retention: Option<Duration>,
895 ) -> Result<u64, PeerError> {
896 let response = self
897 .client
898 .gc_deleted_databases(pb::GcDeletedDatabasesRequest {
899 retention_millis: retention.map(duration_millis),
900 })
901 .await?;
902 Ok(response.into_inner().swept_blobs)
903 }
904
905 pub async fn request_index(&mut self, db: &str) -> Result<u64, PeerError> {
911 let response = self
912 .client
913 .request_index(pb::RequestIndexRequest { db: db.to_owned() })
914 .await?;
915 Ok(response.into_inner().index_basis_t)
916 }
917
918 pub async fn set_index_policy(
925 &mut self,
926 db: &str,
927 update: IndexPolicySettings,
928 ) -> Result<IndexPolicySettings, PeerError> {
929 let response = self
930 .client
931 .set_index_policy(pb::SetIndexPolicyRequest {
932 db: db.to_owned(),
933 interval_ms: update.interval_ms,
934 backoff: update.backoff,
935 tail_threshold: update.tail_threshold,
936 tail_deadline_ms: update.tail_deadline_ms,
937 })
938 .await?
939 .into_inner();
940 Ok(IndexPolicySettings {
941 interval_ms: Some(response.interval_ms),
942 backoff: Some(response.backoff),
943 tail_threshold: Some(response.tail_threshold),
944 tail_deadline_ms: Some(response.tail_deadline_ms),
945 })
946 }
947
948 pub async fn get_storage_info(
955 &mut self,
956 db: &str,
957 ) -> Result<pb::GetStorageInfoResponse, PeerError> {
958 Ok(self
959 .client
960 .get_storage_info(pb::GetStorageInfoRequest { db: db.to_owned() })
961 .await?
962 .into_inner())
963 }
964}
965
966#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
970pub struct IndexPolicySettings {
971 pub interval_ms: Option<u64>,
973 pub backoff: Option<u32>,
976 pub tail_threshold: Option<u64>,
979 pub tail_deadline_ms: Option<u64>,
981}
982
983fn duration_millis(duration: Duration) -> u64 {
984 u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
985}
986
987#[cfg(test)]
988mod tests {
989 use super::*;
990
991 #[test]
992 fn gc_retention_wire_value_preserves_zero_and_subseconds() {
993 assert_eq!(duration_millis(Duration::ZERO), 0);
994 assert_eq!(duration_millis(Duration::from_millis(500)), 500);
995 }
996}