1mod credential;
14pub mod keys;
15pub mod kv_codec;
16pub mod proto;
17pub mod prune_policy;
18pub mod retention;
19pub mod selector;
20pub mod stream_filter;
21pub use credential::API_KEY_ENV;
22pub use keys::{Key, KeyMut, KeyValidationError, Prefix, PrefixError, Value, MAX_KEY_LEN};
23pub use proto::*;
24extern crate self as exoware_proto;
25
26use bytes::Bytes;
27use connectrpc::client::{ClientConfig, ServerStream as ConnectServerStream};
28pub use connectrpc::{ConnectError, ErrorCode};
29use credential::{client_error_from_connect, ApiKey, Credential, UnusableEnvKey};
30use exoware_proto::ingest::ServiceClient as IngestServiceClient;
31use exoware_proto::log::ingest::v1::PutRequest as ProtoPutRequest;
32use exoware_proto::prune::ServiceClient as PruneServiceClient;
33use exoware_proto::query as proto_query;
34use exoware_proto::query::ServiceClient as QueryServiceClient;
35use exoware_proto::store::prune::v1::PruneRequest as ProtoPruneRequest;
36use exoware_proto::store::query::v1::{
37 GetManyRequest as ProtoGetManyRequest, GetRequest as ProtoGetRequest,
38 RangeRequest as ProtoRangeRequest, ReduceRequest as ProtoWireReduceRequest,
39};
40use exoware_proto::RangeReduceRequest as DomainRangeReduceRequest;
41use exoware_proto::{
42 connect_compression_registry as proto_connect_compression_registry,
43 decode_connect_error as proto_decode_connect_error,
44 to_domain_reduce_response as proto_to_domain_reduce_response,
45 to_proto_reduce_params as proto_to_proto_reduce_params,
46 PreferZstdHttpClient as ProtoPreferZstdHttpClient,
47};
48use futures::future::BoxFuture;
49use futures::{stream::BoxStream, StreamExt};
50use keys::is_valid_key_size;
51use kv_codec::{KvExpr, KvFieldRef, KvReducedValue};
52use rustls_platform_verifier::ConfigVerifierExt;
53use std::collections::HashMap;
54use std::sync::{
55 atomic::{AtomicU64, Ordering},
56 Arc,
57};
58use std::time::Duration;
59
60use crate::proto::transport::{
61 BalancedHttp2Config as ProtoBalancedHttp2Config,
62 BalancedHttp2Transport as ProtoBalancedHttp2Transport,
63 ErasedClientTransport as ProtoErasedClientTransport,
64 ErasedResponseBody as ProtoErasedResponseBody,
65};
66
67const DEFAULT_RETRY_MAX_ATTEMPTS: usize = 3;
68const DEFAULT_RETRY_INITIAL_BACKOFF_MS: u64 = 100;
69const DEFAULT_RETRY_MAX_BACKOFF_MS: u64 = 2_000;
70
71pub trait IntoStoreWriteValue {
74 fn into_store_write_value(self) -> Bytes;
75}
76
77impl IntoStoreWriteValue for Bytes {
78 fn into_store_write_value(self) -> Bytes {
79 self
80 }
81}
82
83impl IntoStoreWriteValue for &Bytes {
84 fn into_store_write_value(self) -> Bytes {
85 self.clone()
86 }
87}
88
89impl IntoStoreWriteValue for Vec<u8> {
90 fn into_store_write_value(self) -> Bytes {
91 self.into()
92 }
93}
94
95impl IntoStoreWriteValue for &Vec<u8> {
96 fn into_store_write_value(self) -> Bytes {
97 Bytes::copy_from_slice(self)
98 }
99}
100
101impl IntoStoreWriteValue for &[u8] {
102 fn into_store_write_value(self) -> Bytes {
103 Bytes::copy_from_slice(self)
104 }
105}
106
107impl<const N: usize> IntoStoreWriteValue for &[u8; N] {
108 fn into_store_write_value(self) -> Bytes {
109 Bytes::copy_from_slice(self)
110 }
111}
112
113#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
125pub enum ConnectRequestCompression {
126 #[default]
128 None,
129 Zstd,
131 Gzip,
133}
134
135impl ConnectRequestCompression {
136 fn wire_name(self) -> Option<&'static str> {
137 match self {
138 Self::None => None,
139 Self::Zstd => Some("zstd"),
140 Self::Gzip => Some("gzip"),
141 }
142 }
143}
144
145const STORE_CLIENT_MAX_MESSAGE_BYTES: usize = 256 * 1024 * 1024;
150
151fn store_connect_client_config(
158 base_uri: http::Uri,
159 request_compression: ConnectRequestCompression,
160 timeout: Option<Duration>,
161) -> ClientConfig {
162 let config = ClientConfig::new(base_uri)
163 .with_compression(proto_connect_compression_registry())
164 .with_default_max_message_size(STORE_CLIENT_MAX_MESSAGE_BYTES);
165 let config = match timeout {
166 Some(timeout) => config.with_default_timeout(timeout),
167 None => config,
168 };
169 match request_compression.wire_name() {
170 Some(name) => config.compress_requests(name),
171 None => config,
172 }
173}
174
175#[derive(Debug, thiserror::Error)]
177pub enum ClientError {
178 #[error("HTTP error: {0}")]
179 Http(#[from] reqwest::Error),
180 #[error("RPC error ({0})")]
181 Rpc(Box<ConnectError>),
182 #[error("store key prefix error: {0}")]
183 Prefix(#[from] StoreKeyPrefixError),
184 #[error("invalid key length: expected {expected}, got {got}")]
185 InvalidKeyLength { expected: usize, got: usize },
186 #[error("wire format error: {0}")]
187 WireFormat(String),
188}
189
190impl ClientError {
191 pub fn rpc_error(&self) -> Option<&ConnectError> {
192 match self {
193 Self::Rpc(err) => Some(err.as_ref()),
194 _ => None,
195 }
196 }
197
198 pub fn rpc_code(&self) -> Option<ErrorCode> {
199 self.rpc_error().map(|err| err.code)
200 }
201
202 pub fn decoded_rpc_error(
203 &self,
204 ) -> Result<Option<exoware_proto::DecodedConnectError>, buffa::DecodeError> {
205 self.rpc_error().map(proto_decode_connect_error).transpose()
206 }
207}
208
209#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
212pub enum StoreKeyPrefixError {
213 #[error("key does not belong to this store prefix")]
214 PrefixMismatch,
215 #[error("key offset {offset} plus store prefix shift {shift} exceeds u16")]
216 KeyOffsetOverflow { offset: u16, shift: u16 },
217 #[error("key prefix error: {0}")]
218 Prefix(#[from] PrefixError),
219}
220
221#[derive(Clone, Debug, PartialEq, Eq, Hash)]
229pub struct StoreKeyPrefix {
230 inner: Prefix,
231}
232
233impl StoreKeyPrefix {
234 pub fn new(prefix: impl Into<Bytes>) -> Result<Self, StoreKeyPrefixError> {
235 Ok(Self {
236 inner: Prefix::new(prefix)?,
237 })
238 }
239
240 pub const fn identity() -> Self {
244 Self {
245 inner: Prefix::empty(),
246 }
247 }
248
249 #[inline]
251 pub fn prefix(&self) -> &Bytes {
252 self.inner.as_bytes()
253 }
254
255 #[inline]
258 pub fn matches(&self, key: &[u8]) -> bool {
259 self.inner.matches(key)
260 }
261
262 #[inline]
264 pub fn max_logical_key_len(&self) -> usize {
265 self.inner.max_payload_len()
266 }
267
268 pub fn encode_key(&self, key: &Key) -> Result<Key, StoreKeyPrefixError> {
271 Ok(self.inner.encode_key(key)?)
272 }
273
274 pub fn decode_key(&self, key: &Key) -> Result<Key, StoreKeyPrefixError> {
277 self.inner
278 .strip(key)
279 .map_err(|_| StoreKeyPrefixError::PrefixMismatch)
280 }
281
282 pub fn next_key(&self, key: &Key) -> Result<Option<Key>, StoreKeyPrefixError> {
285 let physical = self.encode_key(key)?;
286 Ok(crate::keys::next_key(&physical).and_then(|next| self.decode_key(&next).ok()))
287 }
288
289 pub fn encode_range(&self, start: &Key, end: &Key) -> Result<(Key, Key), StoreKeyPrefixError> {
297 let start = self.encode_key(start)?;
298 let end = if end.is_empty() {
299 self.inner.bounds().1
300 } else {
301 let max_len = self.max_logical_key_len();
302 if end.len() > max_len {
303 self.encode_key(&end.slice(..max_len))?
304 } else {
305 self.encode_key(end)?
306 }
307 };
308 Ok((start, end))
309 }
310
311 fn prefix_selector(
312 &self,
313 selector: &crate::selector::Selector,
314 ) -> Result<crate::selector::Selector, StoreKeyPrefixError> {
315 let prefix = self.inner.join(&Prefix::new(selector.prefix.clone())?)?;
316 Ok(crate::selector::Selector {
317 prefix: prefix.as_bytes().clone(),
318 payload_regex: selector.payload_regex.clone(),
319 })
320 }
321}
322
323#[derive(Clone, Debug)]
327pub struct PrefixedStoreClient {
328 client: StoreClient,
329 prefix: StoreKeyPrefix,
330}
331
332impl PrefixedStoreClient {
333 pub fn new(client: StoreClient, prefix: StoreKeyPrefix) -> Self {
335 Self { client, prefix }
336 }
337
338 pub fn empty(client: StoreClient) -> Self {
341 Self::new(client, StoreKeyPrefix::identity())
342 }
343
344 pub fn key_prefix(&self) -> &StoreKeyPrefix {
346 &self.prefix
347 }
348
349 pub fn client(&self) -> &StoreClient {
352 &self.client
353 }
354
355 pub fn encode_store_key(&self, key: &Key) -> Result<Key, ClientError> {
359 Ok(self.prefix.encode_key(key)?)
360 }
361
362 pub fn decode_store_key(&self, key: &Key) -> Result<Key, ClientError> {
364 Ok(self.prefix.decode_key(key)?)
365 }
366
367 fn encode_store_range(&self, start: &Key, end: &Key) -> Result<(Key, Key), ClientError> {
368 Ok(self.prefix.encode_range(start, end)?)
369 }
370
371 fn prefix_prune_policies(
372 &self,
373 policies: &[crate::prune_policy::PrunePolicy],
374 ) -> Result<Vec<crate::prune_policy::PrunePolicy>, ClientError> {
375 policies
376 .iter()
377 .map(|policy| {
378 use crate::prune_policy::PrunePolicy;
379 let mut scope = policy.scope.clone();
380 scope.selector = self.prefix.prefix_selector(&scope.selector)?;
381 Ok(PrunePolicy {
382 scope,
383 retain: policy.retain.clone(),
384 })
385 })
386 .collect::<Result<Vec<_>, StoreKeyPrefixError>>()
387 .map_err(ClientError::from)
388 }
389
390 fn prefix_stream_filter(
391 &self,
392 filter: crate::stream_filter::StreamFilter,
393 ) -> Result<crate::stream_filter::StreamFilter, ClientError> {
394 let selectors = filter
395 .selectors
396 .iter()
397 .map(|mk| self.prefix.prefix_selector(mk))
398 .collect::<Result<Vec<_>, _>>()?;
399 Ok(crate::stream_filter::StreamFilter {
400 selectors,
401 value_filters: filter.value_filters,
402 })
403 }
404
405 fn prefix_reduce_request(
406 &self,
407 request: &DomainRangeReduceRequest,
408 ) -> Result<DomainRangeReduceRequest, ClientError> {
409 let mut request = request.clone();
410 shift_reduce_request_key_offsets(self.prefix.prefix().len(), &mut request)?;
411 Ok(request)
412 }
413
414 pub fn ingest(&self) -> Ingest<'_> {
418 Ingest { c: self }
419 }
420
421 pub fn query(&self) -> Query<'_> {
423 Query { c: self }
424 }
425
426 pub fn prune(&self) -> Prune<'_> {
428 Prune { c: self }
429 }
430
431 pub fn retention(&self) -> Retention<'_> {
433 Retention { c: self }
434 }
435
436 pub fn stream(&self) -> Stream<'_> {
438 Stream { c: self }
439 }
440
441 pub fn create_session(&self) -> SerializableReadSession {
443 self.create_session_with_sequence(0)
444 }
445
446 pub fn create_session_with_sequence(&self, sequence: u64) -> SerializableReadSession {
448 SerializableReadSession {
449 client: self.clone(),
450 state: Arc::new(SessionState {
451 minimum_sequence: sequence,
452 sequence: Arc::new(AtomicU64::new(0)),
453 init_gate: tokio::sync::Mutex::new(()),
454 }),
455 }
456 }
457
458 pub(crate) async fn put(&self, kvs: &[(&Key, &[u8])]) -> Result<u64, ClientError> {
461 let keys = kvs
462 .iter()
463 .map(|(key, _)| self.encode_store_key(key))
464 .collect::<Result<Vec<_>, _>>()?;
465 let prefixed: Vec<(&Key, &[u8])> = keys
466 .iter()
467 .zip(kvs.iter())
468 .map(|(key, (_, value))| (key, *value))
469 .collect();
470 self.client.put_physical(&prefixed).await
471 }
472
473 pub(crate) async fn send_get(
476 &self,
477 key: &Key,
478 min_sequence_number: Option<u64>,
479 ) -> Result<
480 (
481 exoware_proto::query::GetResponse,
482 Option<proto_query::Detail>,
483 ),
484 ClientError,
485 > {
486 self.client
487 .send_get(&self.encode_store_key(key)?, min_sequence_number)
488 .await
489 }
490
491 pub(crate) async fn get(&self, key: &Key) -> Result<Option<Bytes>, ClientError> {
492 self.client.get(&self.encode_store_key(key)?).await
493 }
494
495 pub(crate) async fn get_with_min_sequence_number(
496 &self,
497 key: &Key,
498 min_sequence_number: u64,
499 ) -> Result<Option<Bytes>, ClientError> {
500 self.client
501 .get_with_min_sequence_number(&self.encode_store_key(key)?, min_sequence_number)
502 .await
503 }
504
505 pub(crate) async fn get_many(
506 &self,
507 keys: &[&Key],
508 batch_size: u32,
509 ) -> Result<GetManyStream, ClientError> {
510 self.get_many_internal(keys, batch_size, None, None).await
511 }
512
513 pub(crate) async fn get_many_with_min_sequence_number(
514 &self,
515 keys: &[&Key],
516 batch_size: u32,
517 min_sequence_number: u64,
518 ) -> Result<GetManyStream, ClientError> {
519 self.get_many_internal(keys, batch_size, Some(min_sequence_number), None)
520 .await
521 }
522
523 pub(crate) async fn get_many_internal(
524 &self,
525 keys: &[&Key],
526 batch_size: u32,
527 min_sequence_number: Option<u64>,
528 observed_sequence: Option<Arc<AtomicU64>>,
529 ) -> Result<GetManyStream, ClientError> {
530 let mut proto_keys: Vec<Vec<u8>> = Vec::with_capacity(keys.len());
531 for key in keys {
532 let encoded = self.encode_store_key(key)?;
533 if !is_valid_key_size(encoded.len()) {
534 return Err(ClientError::WireFormat(format!(
535 "key length {} is outside valid store key range ({}..={})",
536 encoded.len(),
537 keys::MIN_KEY_LEN,
538 MAX_KEY_LEN
539 )));
540 }
541 proto_keys.push(encoded.to_vec());
542 }
543 let mut stream = self
544 .client
545 .get_many_internal(
546 proto_keys,
547 batch_size,
548 min_sequence_number,
549 observed_sequence,
550 )
551 .await?;
552 stream.key_prefix = Some(self.prefix.clone());
553 Ok(stream)
554 }
555
556 pub(crate) async fn range(
557 &self,
558 start: &Key,
559 end: &Key,
560 limit: usize,
561 ) -> Result<Vec<(Key, Bytes)>, ClientError> {
562 self.range_internal(start, end, limit, RangeMode::Forward, None)
563 .await
564 }
565
566 pub(crate) async fn range_with_mode(
567 &self,
568 start: &Key,
569 end: &Key,
570 limit: usize,
571 mode: RangeMode,
572 ) -> Result<Vec<(Key, Bytes)>, ClientError> {
573 self.range_internal(start, end, limit, mode, None).await
574 }
575
576 pub(crate) async fn range_with_min_sequence_number(
577 &self,
578 start: &Key,
579 end: &Key,
580 limit: usize,
581 min_sequence_number: u64,
582 ) -> Result<Vec<(Key, Bytes)>, ClientError> {
583 self.range_internal(
584 start,
585 end,
586 limit,
587 RangeMode::Forward,
588 Some(min_sequence_number),
589 )
590 .await
591 }
592
593 pub(crate) async fn range_with_mode_and_min_sequence_number(
594 &self,
595 start: &Key,
596 end: &Key,
597 limit: usize,
598 mode: RangeMode,
599 min_sequence_number: u64,
600 ) -> Result<Vec<(Key, Bytes)>, ClientError> {
601 self.range_internal(start, end, limit, mode, Some(min_sequence_number))
602 .await
603 }
604
605 async fn range_internal(
606 &self,
607 start: &Key,
608 end: &Key,
609 limit: usize,
610 mode: RangeMode,
611 min_sequence_number: Option<u64>,
612 ) -> Result<Vec<(Key, Bytes)>, ClientError> {
613 self.range_stream_internal(
614 start,
615 end,
616 limit,
617 limit.max(1),
618 mode,
619 QueryStreamReadOptions {
620 min_sequence_number,
621 observed_sequence: None,
622 },
623 )
624 .await?
625 .collect()
626 .await
627 }
628
629 pub(crate) async fn range_stream(
630 &self,
631 start: &Key,
632 end: &Key,
633 limit: usize,
634 batch_size: usize,
635 ) -> Result<RangeStream, ClientError> {
636 self.range_stream_internal(
637 start,
638 end,
639 limit,
640 batch_size,
641 RangeMode::Forward,
642 QueryStreamReadOptions::default(),
643 )
644 .await
645 }
646
647 pub(crate) async fn range_stream_with_mode(
648 &self,
649 start: &Key,
650 end: &Key,
651 limit: usize,
652 batch_size: usize,
653 mode: RangeMode,
654 ) -> Result<RangeStream, ClientError> {
655 self.range_stream_internal(start, end, limit, batch_size, mode, Default::default())
656 .await
657 }
658
659 pub(crate) async fn range_stream_with_min_sequence_number(
660 &self,
661 start: &Key,
662 end: &Key,
663 limit: usize,
664 batch_size: usize,
665 min_sequence_number: u64,
666 ) -> Result<RangeStream, ClientError> {
667 self.range_stream_internal(
668 start,
669 end,
670 limit,
671 batch_size,
672 RangeMode::Forward,
673 QueryStreamReadOptions {
674 min_sequence_number: Some(min_sequence_number),
675 observed_sequence: None,
676 },
677 )
678 .await
679 }
680
681 pub(crate) async fn range_stream_with_mode_and_min_sequence_number(
682 &self,
683 start: &Key,
684 end: &Key,
685 limit: usize,
686 batch_size: usize,
687 mode: RangeMode,
688 min_sequence_number: u64,
689 ) -> Result<RangeStream, ClientError> {
690 self.range_stream_internal(
691 start,
692 end,
693 limit,
694 batch_size,
695 mode,
696 QueryStreamReadOptions {
697 min_sequence_number: Some(min_sequence_number),
698 observed_sequence: None,
699 },
700 )
701 .await
702 }
703
704 pub(crate) async fn range_stream_internal(
705 &self,
706 start: &Key,
707 end: &Key,
708 limit: usize,
709 batch_size: usize,
710 mode: RangeMode,
711 options: QueryStreamReadOptions,
712 ) -> Result<RangeStream, ClientError> {
713 let (start, end) = self.encode_store_range(start, end)?;
714 let mut stream = self
715 .client
716 .range_stream_internal(&start, &end, limit, batch_size, mode, options)
717 .await?;
718 stream.key_prefix = Some(self.prefix.clone());
719 Ok(stream)
720 }
721
722 pub(crate) async fn range_reduce_stream_internal(
723 &self,
724 start: &Key,
725 end: &Key,
726 request: &DomainRangeReduceRequest,
727 options: QueryStreamReadOptions,
728 ) -> Result<ReduceStream, ClientError> {
729 let (start, end) = self.encode_store_range(start, end)?;
730 let request = self.prefix_reduce_request(request)?;
731 self.client
732 .range_reduce_stream_internal(&start, &end, &request, options)
733 .await
734 }
735
736 pub(crate) async fn apply_prune_policies(
737 &self,
738 policies: &[crate::prune_policy::PrunePolicy],
739 ) -> Result<(), ClientError> {
740 let policies = self.prefix_prune_policies(policies)?;
741 self.client.prune(&policies).await
742 }
743
744 pub(crate) async fn subscribe(
745 &self,
746 filter: crate::stream_filter::StreamFilter,
747 since_sequence_number: Option<u64>,
748 ) -> Result<StreamSubscription, ClientError> {
749 let filter = self.prefix_stream_filter(filter)?;
753 let mut sub = self
754 .client
755 .subscribe_physical(filter, since_sequence_number)
756 .await?;
757 sub.key_prefix = Some(self.prefix.clone());
758 Ok(sub)
759 }
760
761 pub(crate) async fn stream_get(
762 &self,
763 sequence_number: u64,
764 ) -> Result<Option<Vec<(Key, Bytes)>>, ClientError> {
765 let Some(owned) = self.client.stream_get_physical(sequence_number).await? else {
766 return Ok(None);
767 };
768 let mut out = Vec::with_capacity(owned.entries.len());
769 for entry in owned.entries {
770 let key = Bytes::from(entry.key);
771 if !self.prefix.matches(&key) {
772 continue;
773 }
774 out.push((self.decode_store_key(&key)?, entry.value));
775 }
776 Ok(Some(out))
777 }
778
779 pub(crate) async fn set_retention(
780 &self,
781 policy: Option<crate::retention::RetentionPolicy>,
782 ) -> Result<Option<u64>, ClientError> {
783 self.client.set_retention(policy).await
786 }
787}
788
789pub async fn scalar_reduce_response(
791 mut stream: ReduceStream,
792 request: &DomainRangeReduceRequest,
793) -> Result<connectrpc::StreamMessage<proto_query::ReduceResponse>, ClientError> {
794 if !request.group_by.is_empty() {
795 return Err(ClientError::WireFormat(
796 "grouped reductions require range_reduce_stream".to_string(),
797 ));
798 }
799 let response = stream.next().await.transpose()?.ok_or_else(|| {
800 ClientError::WireFormat("scalar reduction stream returned no results".to_string())
801 })?;
802 let view = response.view();
803 if !view.groups.is_empty() || view.results.len() != request.reducers.len() {
804 return Err(ClientError::WireFormat(
805 "scalar reduction returned an invalid result shape".to_string(),
806 ));
807 }
808 if stream.next().await.transpose()?.is_some() {
809 return Err(ClientError::WireFormat(
810 "scalar reduction returned more than one frame".to_string(),
811 ));
812 }
813 Ok(response)
814}
815
816async fn scalar_reduce_results(
817 stream: ReduceStream,
818 request: &DomainRangeReduceRequest,
819) -> Result<Vec<Option<KvReducedValue>>, ClientError> {
820 let response = scalar_reduce_response(stream, request).await?;
821 let decoded =
822 proto_to_domain_reduce_response(response.view()).map_err(ClientError::WireFormat)?;
823 Ok(decoded
824 .results
825 .into_iter()
826 .map(|result| result.value)
827 .collect())
828}
829
830#[derive(Clone, Debug, Default)]
836pub struct StoreWriteBatch {
837 entries: Vec<(Key, Bytes)>,
838}
839
840impl StoreWriteBatch {
841 pub fn new() -> Self {
842 Self::default()
843 }
844
845 pub fn len(&self) -> usize {
846 self.entries.len()
847 }
848
849 pub fn is_empty(&self) -> bool {
850 self.entries.is_empty()
851 }
852
853 pub fn clear(&mut self) {
854 self.entries.clear();
855 }
856
857 pub fn reserve(&mut self, additional: usize) {
858 self.entries.reserve(additional);
859 }
860
861 pub fn push(
867 &mut self,
868 client: &PrefixedStoreClient,
869 key: &Key,
870 value: impl IntoStoreWriteValue,
871 ) -> Result<&mut Self, ClientError> {
872 self.entries.push((
873 client.encode_store_key(key)?,
874 value.into_store_write_value(),
875 ));
876 Ok(self)
877 }
878
879 pub fn entries(&self) -> &[(Key, Bytes)] {
881 &self.entries
882 }
883
884 pub async fn commit(&self, client: &StoreClient) -> Result<u64, ClientError> {
885 client.put_prepared_physical(&self.entries).await
886 }
887}
888
889pub trait StoreBatchUpload {
901 type Prepared: Send;
902 type Receipt: Send;
903 type Error: std::fmt::Display + Send;
904
905 fn store_client(&self) -> &PrefixedStoreClient;
906
907 fn stage_upload(
908 &self,
909 prepared: &mut Self::Prepared,
910 batch: &mut StoreWriteBatch,
911 ) -> Result<(), Self::Error>;
912
913 fn commit_error(&self, error: ClientError) -> Self::Error;
914
915 fn mark_upload_persisted<'a>(
916 &'a self,
917 prepared: Self::Prepared,
918 sequence_number: u64,
919 ) -> BoxFuture<'a, Self::Receipt>
920 where
921 Self: Sync + 'a,
922 Self::Prepared: 'a;
923
924 fn mark_upload_failed<'a>(
925 &'a self,
926 prepared: Self::Prepared,
927 error: String,
928 ) -> BoxFuture<'a, ()>
929 where
930 Self: Sync + 'a,
931 Self::Prepared: 'a;
932
933 fn commit_upload<'a>(
934 &'a self,
935 prepared: Self::Prepared,
936 ) -> BoxFuture<'a, Result<Self::Receipt, Self::Error>>
937 where
938 Self: Sync + Sized + 'a,
939 Self::Prepared: 'a,
940 Self::Receipt: 'a,
941 Self::Error: 'a,
942 {
943 Box::pin(async move {
944 let mut prepared = prepared;
945 let mut batch = StoreWriteBatch::new();
946 if let Err(err) = self.stage_upload(&mut prepared, &mut batch) {
947 let message = err.to_string();
948 self.mark_upload_failed(prepared, message).await;
949 return Err(err);
950 }
951 match batch.commit(self.store_client().client()).await {
952 Ok(sequence_number) => {
953 Ok(self.mark_upload_persisted(prepared, sequence_number).await)
954 }
955 Err(err) => {
956 let message = err.to_string();
957 self.mark_upload_failed(prepared, message).await;
958 Err(self.commit_error(err))
959 }
960 }
961 })
962 }
963}
964
965#[derive(Clone, Copy, Debug, Eq, PartialEq)]
967pub enum RangeMode {
968 Forward,
969 Reverse,
970}
971
972#[derive(Clone, Debug)]
973pub struct RangeChunk {
974 pub rows: Vec<(Key, Bytes)>,
976 pub detail: Option<proto_query::Detail>,
978}
979
980#[derive(Clone, Debug)]
981pub struct GetManyChunk {
982 pub entries: Vec<(Key, Option<Bytes>)>,
984 pub detail: Option<proto_query::Detail>,
986}
987
988pub struct RangeStream {
990 stream:
991 ConnectServerStream<ProtoErasedResponseBody, exoware_proto::query::RangeFrameView<'static>>,
992 pending_frame: Option<exoware_proto::query::RangeFrame>,
993 rows_seen: usize,
994 final_count: Option<usize>,
995 finished: bool,
996 observed_sequence: Option<Arc<AtomicU64>>,
997 key_prefix: Option<StoreKeyPrefix>,
998 credential: Credential,
999}
1000
1001impl RangeStream {
1002 fn from_connect_stream(
1003 stream: ConnectServerStream<
1004 ProtoErasedResponseBody,
1005 exoware_proto::query::RangeFrameView<'static>,
1006 >,
1007 observed_sequence: Option<Arc<AtomicU64>>,
1008 key_prefix: Option<StoreKeyPrefix>,
1009 credential: Credential,
1010 ) -> Self {
1011 Self {
1012 stream,
1013 pending_frame: None,
1014 rows_seen: 0,
1015 final_count: None,
1016 finished: false,
1017 observed_sequence,
1018 key_prefix,
1019 credential,
1020 }
1021 }
1022
1023 pub fn final_count(&self) -> Option<usize> {
1024 self.final_count
1025 }
1026
1027 async fn prefetch_first_frame(&mut self) -> Result<(), ConnectError> {
1028 if self.pending_frame.is_some() || self.finished {
1029 return Ok(());
1030 }
1031 match self.stream.message().await? {
1032 Some(frame) => {
1033 let frame = frame.to_owned_message();
1034 if let (Some(sequence_store), Some(detail)) =
1035 (&self.observed_sequence, frame.detail.as_option())
1036 {
1037 sequence_store.fetch_max(detail.sequence_number, Ordering::SeqCst);
1038 }
1039 self.pending_frame = Some(frame);
1040 Ok(())
1041 }
1042 None => {
1043 self.finished = true;
1044 if let Some(err) = self.stream.error() {
1045 Err(err.clone())
1046 } else {
1047 self.final_count = Some(self.rows_seen);
1048 Ok(())
1049 }
1050 }
1051 }
1052 }
1053
1054 pub async fn next_chunk(&mut self) -> Result<Option<RangeChunk>, ClientError> {
1055 loop {
1056 if self.finished {
1057 return Ok(None);
1058 }
1059
1060 let frame = if let Some(frame) = self.pending_frame.take() {
1061 frame
1062 } else {
1063 let Some(frame) = self
1064 .stream
1065 .message()
1066 .await
1067 .map_err(|err| client_error_from_connect(err, self.credential))?
1068 else {
1069 self.finished = true;
1070 if let Some(err) = self.stream.error() {
1071 return Err(client_error_from_connect(err.clone(), self.credential));
1072 }
1073 self.final_count = Some(self.rows_seen);
1074 return Ok(None);
1075 };
1076 let frame = frame.to_owned_message();
1077 if let (Some(sequence_store), Some(detail)) =
1078 (&self.observed_sequence, frame.detail.as_option())
1079 {
1080 sequence_store.fetch_max(detail.sequence_number, Ordering::SeqCst);
1081 }
1082 frame
1083 };
1084
1085 let detail = frame.detail.as_option().cloned();
1086 let n = frame.results.len();
1087
1088 if n == 0 && detail.is_none() {
1090 continue;
1091 }
1092
1093 let mut out = Vec::with_capacity(n);
1094 for entry in frame.results {
1095 let key = Bytes::from(entry.key);
1096 let key = match &self.key_prefix {
1097 Some(prefix) => prefix.decode_key(&key)?,
1098 None => key,
1099 };
1100 out.push((key, entry.value));
1101 }
1102 self.rows_seen += n;
1103 return Ok(Some(RangeChunk { rows: out, detail }));
1104 }
1105 }
1106
1107 pub async fn collect(mut self) -> Result<Vec<(Key, Bytes)>, ClientError> {
1108 let mut entries = Vec::new();
1109 while let Some(chunk) = self.next_chunk().await? {
1110 entries.extend(chunk.rows);
1111 }
1112 Ok(entries)
1113 }
1114}
1115
1116pub type ReduceStream =
1122 BoxStream<'static, Result<connectrpc::StreamMessage<proto_query::ReduceResponse>, ClientError>>;
1123
1124pub struct GetManyStream {
1125 stream: ConnectServerStream<
1126 ProtoErasedResponseBody,
1127 exoware_proto::query::GetManyFrameView<'static>,
1128 >,
1129 pending_frame: Option<exoware_proto::query::GetManyFrame>,
1130 finished: bool,
1131 observed_sequence: Option<Arc<AtomicU64>>,
1132 key_prefix: Option<StoreKeyPrefix>,
1133 credential: Credential,
1134}
1135
1136impl GetManyStream {
1137 fn from_connect_stream(
1138 stream: ConnectServerStream<
1139 ProtoErasedResponseBody,
1140 exoware_proto::query::GetManyFrameView<'static>,
1141 >,
1142 observed_sequence: Option<Arc<AtomicU64>>,
1143 key_prefix: Option<StoreKeyPrefix>,
1144 credential: Credential,
1145 ) -> Self {
1146 Self {
1147 stream,
1148 pending_frame: None,
1149 finished: false,
1150 observed_sequence,
1151 key_prefix,
1152 credential,
1153 }
1154 }
1155
1156 async fn prefetch_first_frame(&mut self) -> Result<(), ConnectError> {
1157 if self.pending_frame.is_some() || self.finished {
1158 return Ok(());
1159 }
1160 match self.stream.message().await? {
1161 Some(frame) => {
1162 let frame = frame.to_owned_message();
1163 if let (Some(sequence_store), Some(detail)) =
1164 (&self.observed_sequence, frame.detail.as_option())
1165 {
1166 sequence_store.fetch_max(detail.sequence_number, Ordering::SeqCst);
1167 }
1168 self.pending_frame = Some(frame);
1169 Ok(())
1170 }
1171 None => {
1172 self.finished = true;
1173 if let Some(err) = self.stream.error() {
1174 Err(err.clone())
1175 } else {
1176 Ok(())
1177 }
1178 }
1179 }
1180 }
1181
1182 pub async fn next_chunk(&mut self) -> Result<Option<GetManyChunk>, ClientError> {
1183 loop {
1184 if self.finished {
1185 return Ok(None);
1186 }
1187 let frame = if let Some(frame) = self.pending_frame.take() {
1188 frame
1189 } else {
1190 let Some(frame) = self
1191 .stream
1192 .message()
1193 .await
1194 .map_err(|err| client_error_from_connect(err, self.credential))?
1195 else {
1196 self.finished = true;
1197 if let Some(err) = self.stream.error() {
1198 return Err(client_error_from_connect(err.clone(), self.credential));
1199 }
1200 return Ok(None);
1201 };
1202 let frame = frame.to_owned_message();
1203 if let (Some(sequence_store), Some(detail)) =
1204 (&self.observed_sequence, frame.detail.as_option())
1205 {
1206 sequence_store.fetch_max(detail.sequence_number, Ordering::SeqCst);
1207 }
1208 frame
1209 };
1210
1211 let detail = frame.detail.as_option().cloned();
1212 let n = frame.results.len();
1213
1214 if n == 0 && detail.is_none() {
1216 continue;
1217 }
1218
1219 let mut out = Vec::with_capacity(n);
1220 for entry in frame.results {
1221 let key = Bytes::from(entry.key);
1222 let key = match &self.key_prefix {
1223 Some(prefix) => prefix.decode_key(&key)?,
1224 None => key,
1225 };
1226 out.push((key, entry.value));
1227 }
1228 return Ok(Some(GetManyChunk {
1229 entries: out,
1230 detail,
1231 }));
1232 }
1233 }
1234
1235 pub async fn collect(mut self) -> Result<HashMap<Key, Bytes>, ClientError> {
1236 let mut map = HashMap::new();
1237 while let Some(chunk) = self.next_chunk().await? {
1238 for (key, value) in chunk.entries {
1239 if let Some(v) = value {
1240 map.insert(key, v);
1241 }
1242 }
1243 }
1244 Ok(map)
1245 }
1246}
1247
1248impl RangeMode {
1249 fn to_proto(self) -> proto_query::TraversalMode {
1250 match self {
1251 Self::Forward => proto_query::TraversalMode::TRAVERSAL_MODE_FORWARD,
1252 Self::Reverse => proto_query::TraversalMode::TRAVERSAL_MODE_REVERSE,
1253 }
1254 }
1255}
1256
1257#[derive(Clone, Debug)]
1261pub struct StreamSubscriptionEntry {
1262 pub key: Key,
1263 pub value: Bytes,
1264}
1265
1266#[derive(Clone, Debug)]
1268pub struct StreamSubscriptionFrame {
1269 pub sequence_number: u64,
1270 pub entries: Vec<StreamSubscriptionEntry>,
1271}
1272
1273pub struct StreamSubscription {
1276 stream: ConnectServerStream<
1277 ProtoErasedResponseBody,
1278 exoware_proto::log::stream::v1::SubscribeResponseView<'static>,
1279 >,
1280 key_prefix: Option<StoreKeyPrefix>,
1281 credential: Credential,
1282}
1283
1284impl std::fmt::Debug for StreamSubscription {
1285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1286 f.debug_struct("StreamSubscription").finish_non_exhaustive()
1287 }
1288}
1289
1290impl StreamSubscription {
1291 pub async fn next(&mut self) -> Result<Option<StreamSubscriptionFrame>, ClientError> {
1293 loop {
1294 match self
1295 .stream
1296 .message()
1297 .await
1298 .map_err(|err| client_error_from_connect(err, self.credential))?
1299 {
1300 Some(view) => {
1301 let owned = view.to_owned_message();
1302 let mut entries = Vec::with_capacity(owned.entries.len());
1303 for entry in owned.entries {
1304 let key = Bytes::from(entry.key);
1305 let key = match &self.key_prefix {
1306 Some(prefix) => prefix.decode_key(&key)?,
1307 None => key,
1308 };
1309 entries.push(StreamSubscriptionEntry {
1310 key,
1311 value: entry.value,
1312 });
1313 }
1314 if entries.is_empty() {
1315 continue;
1316 }
1317 let frame = StreamSubscriptionFrame {
1318 sequence_number: owned.sequence_number,
1319 entries,
1320 };
1321 return Ok(Some(frame));
1322 }
1323 None => {
1324 if let Some(err) = self.stream.error() {
1325 return Err(client_error_from_connect(err.clone(), self.credential));
1326 } else {
1327 return Ok(None);
1328 }
1329 }
1330 }
1331 }
1332 }
1333}
1334
1335fn is_batch_missing_error(err: &ConnectError) -> bool {
1338 match proto_decode_connect_error(err) {
1339 Ok(decoded) => decoded.error_info.is_some_and(|info| {
1340 info.domain == "log.stream"
1341 && matches!(info.reason.as_str(), "BATCH_EVICTED" | "BATCH_NOT_FOUND")
1342 }),
1343 Err(_) => false,
1344 }
1345}
1346
1347#[derive(Clone, Copy, Debug)]
1349pub struct RetryConfig {
1350 max_attempts: usize,
1351 initial_backoff: Duration,
1352 max_backoff: Duration,
1353}
1354
1355impl RetryConfig {
1356 pub fn standard() -> Self {
1357 Self {
1358 max_attempts: DEFAULT_RETRY_MAX_ATTEMPTS,
1359 initial_backoff: Duration::from_millis(DEFAULT_RETRY_INITIAL_BACKOFF_MS),
1360 max_backoff: Duration::from_millis(DEFAULT_RETRY_MAX_BACKOFF_MS),
1361 }
1362 }
1363
1364 pub fn disabled() -> Self {
1365 Self::standard().with_max_attempts(1)
1366 }
1367
1368 pub fn with_max_attempts(mut self, max_attempts: usize) -> Self {
1369 self.max_attempts = max_attempts.max(1);
1370 self
1371 }
1372
1373 pub fn with_initial_backoff(mut self, initial_backoff: Duration) -> Self {
1374 self.initial_backoff = initial_backoff;
1375 self
1376 }
1377
1378 pub fn with_max_backoff(mut self, max_backoff: Duration) -> Self {
1379 self.max_backoff = max_backoff;
1380 self
1381 }
1382
1383 pub(crate) fn sanitized(self) -> Self {
1384 let max_attempts = self.max_attempts.max(1);
1385 let max_backoff = self.max_backoff.max(self.initial_backoff);
1386 Self {
1387 max_attempts,
1388 initial_backoff: self.initial_backoff,
1389 max_backoff,
1390 }
1391 }
1392}
1393
1394impl Default for RetryConfig {
1395 fn default() -> Self {
1396 Self::standard()
1397 }
1398}
1399
1400fn trim_connect_base(url: &str) -> String {
1401 url.trim_end_matches('/').to_string()
1402}
1403
1404fn parse_connect_uri(url: &str) -> Result<http::Uri, ClientBuildError> {
1405 let uri: http::Uri = url.parse().map_err(|source| ClientBuildError::InvalidUrl {
1406 url: url.to_string(),
1407 source,
1408 })?;
1409 if uri.authority().is_none() || !matches!(uri.scheme_str(), Some("http" | "https")) {
1410 return Err(ClientBuildError::InvalidEndpointUrl {
1411 url: url.to_string(),
1412 });
1413 }
1414 Ok(uri)
1415}
1416
1417fn new_health_client() -> reqwest::Client {
1418 reqwest::Client::builder()
1419 .pool_max_idle_per_host(32)
1420 .timeout(Duration::from_secs(30))
1421 .build()
1422 .expect("failed to build HTTP client")
1423}
1424
1425#[derive(Debug, thiserror::Error)]
1427pub enum ClientBuildError {
1428 #[error("StoreClientBuilder: missing health URL (set health_url or url)")]
1429 MissingHealthUrl,
1430 #[error("StoreClientBuilder: missing ingest URL (set ingest_url or url)")]
1431 MissingIngestUrl,
1432 #[error("StoreClientBuilder: missing query URL (set query_url or url)")]
1433 MissingQueryUrl,
1434 #[error("StoreClientBuilder: missing prune URL (set prune_url or url)")]
1435 MissingPruneUrl,
1436 #[error("StoreClientBuilder: missing retention URL (set retention_url or url)")]
1437 MissingRetentionUrl,
1438 #[error("StoreClientBuilder: missing stream URL (set stream_url or url)")]
1439 MissingStreamUrl,
1440 #[error("StoreClientBuilder: invalid URL \"{url}\": {source}")]
1441 InvalidUrl {
1442 url: String,
1443 source: http::uri::InvalidUri,
1444 },
1445 #[error(
1446 "StoreClientBuilder: invalid endpoint URL \"{url}\" (expected absolute HTTP or HTTPS URL)"
1447 )]
1448 InvalidEndpointUrl { url: String },
1449 #[error("StoreClientBuilder: failed to configure platform TLS verifier: {0}")]
1450 TlsConfig(#[source] connectrpc::rustls::Error),
1451 #[error(
1452 "StoreClientBuilder: API key is not valid in an HTTP header (check for control or non-ASCII characters)"
1453 )]
1454 InvalidApiKey,
1455 #[error(
1456 "{API_KEY_ENV} is set to a value that cannot be an HTTP header. Remove any control or non-ASCII characters from it"
1457 )]
1458 InvalidApiKeyEnv,
1459}
1460
1461#[derive(Debug)]
1462enum RpcTransportChoice {
1463 Custom(ProtoErasedClientTransport),
1464 BalancedHttp2(ProtoBalancedHttp2Config),
1465}
1466
1467#[derive(Debug, Default)]
1473pub struct StoreClientBuilder {
1474 health_url: Option<String>,
1475 ingest_url: Option<String>,
1476 query_url: Option<String>,
1477 prune_url: Option<String>,
1478 retention_url: Option<String>,
1479 stream_url: Option<String>,
1480 retry_config: RetryConfig,
1481 connect_request_compression: ConnectRequestCompression,
1482 api_key: Option<ApiKey>,
1483 rpc_transport: Option<RpcTransportChoice>,
1484}
1485
1486impl StoreClientBuilder {
1487 pub fn url(mut self, url: &str) -> Self {
1490 let u = trim_connect_base(url);
1491 self.health_url = Some(u.clone());
1492 self.ingest_url = Some(u.clone());
1493 self.query_url = Some(u.clone());
1494 self.prune_url = Some(u.clone());
1495 self.retention_url = Some(u.clone());
1496 self.stream_url = Some(u);
1497 self
1498 }
1499
1500 pub fn health_url(mut self, url: &str) -> Self {
1502 self.health_url = Some(trim_connect_base(url));
1503 self
1504 }
1505
1506 pub fn ingest_url(mut self, url: &str) -> Self {
1508 self.ingest_url = Some(trim_connect_base(url));
1509 self
1510 }
1511
1512 pub fn query_url(mut self, url: &str) -> Self {
1514 self.query_url = Some(trim_connect_base(url));
1515 self
1516 }
1517
1518 pub fn prune_url(mut self, url: &str) -> Self {
1520 self.prune_url = Some(trim_connect_base(url));
1521 self
1522 }
1523
1524 pub fn retention_url(mut self, url: &str) -> Self {
1526 self.retention_url = Some(trim_connect_base(url));
1527 self
1528 }
1529
1530 pub fn stream_url(mut self, url: &str) -> Self {
1532 self.stream_url = Some(trim_connect_base(url));
1533 self
1534 }
1535
1536 pub fn api_key(mut self, key: &str) -> Self {
1542 self.api_key = Some(ApiKey(key.to_string()));
1543 self
1544 }
1545
1546 pub fn retry_config(mut self, retry: RetryConfig) -> Self {
1548 self.retry_config = retry.sanitized();
1549 self
1550 }
1551
1552 pub fn connect_request_compression(mut self, compression: ConnectRequestCompression) -> Self {
1554 self.connect_request_compression = compression;
1555 self
1556 }
1557
1558 pub fn client_transport<T>(mut self, transport: T) -> Self
1567 where
1568 T: connectrpc::client::ClientTransport,
1569 <T::ResponseBody as http_body::Body>::Error: std::fmt::Display,
1570 {
1571 self.rpc_transport = Some(RpcTransportChoice::Custom(ProtoErasedClientTransport::new(
1572 transport,
1573 )));
1574 self
1575 }
1576
1577 pub fn balanced_http2_transport(mut self, config: ProtoBalancedHttp2Config) -> Self {
1584 self.rpc_transport = Some(RpcTransportChoice::BalancedHttp2(config));
1585 self
1586 }
1587
1588 pub fn build(self) -> Result<StoreClient, ClientBuildError> {
1592 self.build_with(std::env::var(API_KEY_ENV).ok(), UnusableEnvKey::Reject)
1593 }
1594
1595 fn build_with(
1598 self,
1599 env_api_key: Option<String>,
1600 unusable_env_key: UnusableEnvKey,
1601 ) -> Result<StoreClient, ClientBuildError> {
1602 let health_url = self.health_url.ok_or(ClientBuildError::MissingHealthUrl)?;
1603 let ingest_url = self.ingest_url.ok_or(ClientBuildError::MissingIngestUrl)?;
1604 let query_url = self.query_url.ok_or(ClientBuildError::MissingQueryUrl)?;
1605 let prune_url = self.prune_url.ok_or(ClientBuildError::MissingPruneUrl)?;
1606 let retention_url = self
1607 .retention_url
1608 .ok_or(ClientBuildError::MissingRetentionUrl)?;
1609 let stream_url = self.stream_url.ok_or(ClientBuildError::MissingStreamUrl)?;
1610 let ingest_uri = parse_connect_uri(&ingest_url)?;
1611 let query_uri = parse_connect_uri(&query_url)?;
1612 let prune_uri = parse_connect_uri(&prune_url)?;
1613 let retention_uri = parse_connect_uri(&retention_url)?;
1614 let stream_uri = parse_connect_uri(&stream_url)?;
1615 let rpc_uris = [
1616 &ingest_uri,
1617 &query_uri,
1618 &prune_uri,
1619 &retention_uri,
1620 &stream_uri,
1621 ];
1622 let uses_tls = rpc_uris
1623 .into_iter()
1624 .any(|uri| uri.scheme_str() == Some("https"));
1625
1626 let resolved =
1627 credential::resolve(self.api_key.map(|key| key.0), env_api_key, unusable_env_key)?;
1628 let credential = resolved.credential;
1629 let (connect_http, rpc_timeout) = match self.rpc_transport {
1630 None => {
1631 let connect_http = if uses_tls {
1632 let tls_config = connectrpc::rustls::ClientConfig::with_platform_verifier()
1633 .map_err(ClientBuildError::TlsConfig)?;
1634 ProtoPreferZstdHttpClient::with_tls(Arc::new(tls_config))
1635 } else {
1636 ProtoPreferZstdHttpClient::plaintext()
1637 };
1638 let connect_http = match resolved.header {
1639 Some(value) => connect_http.with_authorization(value),
1640 None => connect_http,
1641 };
1642 (ProtoErasedClientTransport::new(connect_http), None)
1643 }
1644 Some(RpcTransportChoice::Custom(transport)) => {
1645 (transport.with_metadata(resolved.header), None)
1646 }
1647 Some(RpcTransportChoice::BalancedHttp2(mut config)) => {
1648 let rpc_timeout = config.request_timeout;
1649 if uses_tls && config.tls_config.is_none() {
1650 config = config.with_tls_config(Arc::new(
1651 connectrpc::rustls::ClientConfig::with_platform_verifier()
1652 .map_err(ClientBuildError::TlsConfig)?,
1653 ));
1654 }
1655 let transport =
1656 ProtoBalancedHttp2Transport::new(rpc_uris.into_iter().cloned(), config);
1657 (
1658 ProtoErasedClientTransport::new(transport).with_metadata(resolved.header),
1659 Some(rpc_timeout),
1660 )
1661 }
1662 };
1663 Ok(StoreClient {
1664 health_url,
1665 ingest_uri,
1666 query_uri,
1667 prune_uri,
1668 retention_uri,
1669 stream_uri,
1670 health_http: new_health_client(),
1671 connect_http,
1672 rpc_timeout,
1673 retry_config: self.retry_config,
1674 connect_request_compression: self.connect_request_compression,
1675 credential,
1676 })
1677 }
1678}
1679
1680#[derive(Clone, Debug)]
1682pub struct StoreClient {
1683 pub(crate) health_url: String,
1685 ingest_uri: http::Uri,
1686 query_uri: http::Uri,
1687 prune_uri: http::Uri,
1688 retention_uri: http::Uri,
1689 stream_uri: http::Uri,
1690 health_http: reqwest::Client,
1691 connect_http: ProtoErasedClientTransport,
1692 rpc_timeout: Option<Duration>,
1693 retry_config: RetryConfig,
1694 connect_request_compression: ConnectRequestCompression,
1695 credential: Credential,
1696}
1697
1698#[derive(Clone, Debug)]
1707pub struct SerializableReadSession {
1708 client: PrefixedStoreClient,
1709 state: Arc<SessionState>,
1710}
1711
1712#[derive(Debug)]
1713struct SessionState {
1714 minimum_sequence: u64,
1715 sequence: Arc<AtomicU64>,
1716 init_gate: tokio::sync::Mutex<()>,
1717}
1718
1719impl SessionState {
1720 fn fixed_sequence(&self) -> Option<u64> {
1721 let sequence = self.sequence.load(Ordering::Acquire);
1722 let floor = self.minimum_sequence.max(sequence);
1723 (floor > 0).then_some(floor)
1724 }
1725
1726 fn evaluated_sequence(&self) -> Option<u64> {
1727 let sequence = self.sequence.load(Ordering::Acquire);
1728 (sequence > 0).then_some(sequence)
1729 }
1730}
1731
1732#[derive(Default)]
1733struct QueryStreamReadOptions {
1734 min_sequence_number: Option<u64>,
1735 observed_sequence: Option<Arc<AtomicU64>>,
1736}
1737
1738impl StoreClient {
1739 pub fn builder() -> StoreClientBuilder {
1741 StoreClientBuilder::default()
1742 }
1743
1744 pub fn new(url: &str) -> Self {
1745 Self::with_retry_config(url, RetryConfig::standard())
1746 }
1747
1748 pub fn with_retry_config(url: &str, retry_config: RetryConfig) -> Self {
1751 Self::builder()
1752 .url(url)
1753 .retry_config(retry_config)
1754 .build_with(std::env::var(API_KEY_ENV).ok(), UnusableEnvKey::Tolerate)
1755 .expect("failed to configure Store client")
1756 }
1757
1758 pub fn prefixed(&self, prefix: StoreKeyPrefix) -> PrefixedStoreClient {
1761 PrefixedStoreClient::new(self.clone(), prefix)
1762 }
1763
1764 pub fn connect_request_compression(&self) -> ConnectRequestCompression {
1766 self.connect_request_compression
1767 }
1768
1769 fn unary_client_config(&self, base_uri: http::Uri) -> ClientConfig {
1770 store_connect_client_config(base_uri, self.connect_request_compression, self.rpc_timeout)
1771 }
1772
1773 fn streaming_client_config(&self, base_uri: http::Uri) -> ClientConfig {
1774 store_connect_client_config(base_uri, self.connect_request_compression, None)
1775 }
1776
1777 async fn with_streaming_timeout<T>(
1778 &self,
1779 future: impl std::future::Future<Output = Result<T, ConnectError>>,
1780 ) -> Result<T, ConnectError> {
1781 match self.rpc_timeout {
1782 Some(timeout) => tokio::time::timeout(timeout, future)
1783 .await
1784 .map_err(|_| ConnectError::deadline_exceeded("client-side deadline exceeded"))?,
1785 None => future.await,
1786 }
1787 }
1788
1789 pub fn decode_error_details(
1790 err: &ConnectError,
1791 ) -> Result<exoware_proto::DecodedConnectError, buffa::DecodeError> {
1792 proto_decode_connect_error(err)
1793 }
1794
1795 pub(crate) async fn put_physical(&self, kvs: &[(&Key, &[u8])]) -> Result<u64, ClientError> {
1802 let mut proto_kvs = Vec::with_capacity(kvs.len());
1803 for (key, value) in kvs {
1804 if !is_valid_key_size(key.len()) {
1805 return Err(ClientError::WireFormat(format!(
1806 "key length {} is outside valid store key range ({}..={})",
1807 key.len(),
1808 keys::MIN_KEY_LEN,
1809 MAX_KEY_LEN
1810 )));
1811 }
1812 proto_kvs.push(exoware_proto::common::Entry {
1813 key: key.to_vec(),
1814 value: Bytes::copy_from_slice(value),
1815 ..Default::default()
1816 });
1817 }
1818 self.send_put(proto_kvs).await
1819 }
1820
1821 async fn put_prepared_physical(&self, kvs: &[(Key, Bytes)]) -> Result<u64, ClientError> {
1822 let mut proto_kvs = Vec::with_capacity(kvs.len());
1823 for (key, value) in kvs {
1824 if !is_valid_key_size(key.len()) {
1825 return Err(ClientError::WireFormat(format!(
1826 "key length {} is outside valid store key range ({}..={})",
1827 key.len(),
1828 keys::MIN_KEY_LEN,
1829 MAX_KEY_LEN
1830 )));
1831 }
1832 proto_kvs.push(exoware_proto::common::Entry {
1833 key: key.to_vec(),
1834 value: value.clone(),
1835 ..Default::default()
1836 });
1837 }
1838 self.send_put(proto_kvs).await
1839 }
1840
1841 async fn send_put(&self, kvs: Vec<exoware_proto::common::Entry>) -> Result<u64, ClientError> {
1842 let config = self.unary_client_config(self.ingest_uri.clone());
1843 let client = IngestServiceClient::new(self.connect_http.clone(), config);
1844 let response = client
1845 .put(ProtoPutRequest {
1846 kvs,
1847 ..Default::default()
1848 })
1849 .await
1850 .map_err(|err| client_error_from_connect(err, self.credential))?;
1851 Ok(response.into_owned().sequence_number)
1852 }
1853
1854 pub(crate) async fn get(&self, key: &Key) -> Result<Option<Bytes>, ClientError> {
1855 self.get_internal(key, None).await
1856 }
1857
1858 pub(crate) async fn get_with_min_sequence_number(
1859 &self,
1860 key: &Key,
1861 min_sequence_number: u64,
1862 ) -> Result<Option<Bytes>, ClientError> {
1863 self.get_internal(key, Some(min_sequence_number)).await
1864 }
1865
1866 async fn get_internal(
1867 &self,
1868 key: &Key,
1869 min_sequence_number: Option<u64>,
1870 ) -> Result<Option<Bytes>, ClientError> {
1871 let (response, _detail) = self
1872 .send_get(key, self.normalize_min_sequence_number(min_sequence_number))
1873 .await?;
1874 Ok(response.value)
1875 }
1876
1877 pub(crate) async fn get_many_internal(
1881 &self,
1882 proto_keys: Vec<Vec<u8>>,
1883 batch_size: u32,
1884 min_sequence_number: Option<u64>,
1885 observed_sequence: Option<Arc<AtomicU64>>,
1886 ) -> Result<GetManyStream, ClientError> {
1887 let config = self.streaming_client_config(self.query_uri.clone());
1888 let client = QueryServiceClient::new(self.connect_http.clone(), config);
1889 let effective_min = self.normalize_min_sequence_number(min_sequence_number);
1890 let max_attempts = self.retry_config.max_attempts.max(1);
1891 let mut attempt = 1usize;
1892 loop {
1893 let result = self
1894 .with_streaming_timeout(async {
1895 let response = client
1896 .get_many(ProtoGetManyRequest {
1897 keys: proto_keys.clone(),
1898 min_sequence_number: effective_min,
1899 batch_size,
1900 ..Default::default()
1901 })
1902 .await?;
1903 let mut stream = GetManyStream::from_connect_stream(
1904 response,
1905 observed_sequence.clone(),
1906 None,
1907 self.credential,
1908 );
1909 stream.prefetch_first_frame().await?;
1910 Ok(stream)
1911 })
1912 .await;
1913 match result {
1914 Ok(stream) => return Ok(stream),
1915 Err(err) => {
1916 if attempt < max_attempts && is_retryable_error(&err) {
1917 let delay = retry_delay_for_error(&err, attempt, self.retry_config);
1918 tokio::time::sleep(delay).await;
1919 attempt += 1;
1920 continue;
1921 }
1922 return Err(client_error_from_connect(err, self.credential));
1923 }
1924 }
1925 }
1926 }
1927
1928 pub(crate) async fn prune(
1929 &self,
1930 policies: &[crate::prune_policy::PrunePolicy],
1931 ) -> Result<(), ClientError> {
1932 let config = self.unary_client_config(self.prune_uri.clone());
1933 let client = PruneServiceClient::new(self.connect_http.clone(), config);
1934 client
1935 .prune(ProtoPruneRequest {
1936 policies: exoware_proto::prune_policies_to_proto(policies),
1937 ..Default::default()
1938 })
1939 .await
1940 .map_err(|err| client_error_from_connect(err, self.credential))?;
1941 Ok(())
1942 }
1943
1944 async fn subscribe_physical(
1948 &self,
1949 filter: crate::stream_filter::StreamFilter,
1950 since_sequence_number: Option<u64>,
1951 ) -> Result<StreamSubscription, ClientError> {
1952 crate::stream_filter::validate_filter(&filter)
1953 .map_err(|e| ClientError::WireFormat(e.to_string()))?;
1954 let selectors = filter
1955 .selectors
1956 .into_iter()
1957 .map(|mk| exoware_proto::common::kv::v1::Selector {
1958 prefix: mk.prefix,
1959 payload_regex: mk.payload_regex.0,
1960 ..Default::default()
1961 })
1962 .collect();
1963 let value_filters = filter
1964 .value_filters
1965 .into_iter()
1966 .map(|vf| {
1967 use crate::stream_filter::Filter;
1968 use exoware_proto::common::kv::v1::filter::Kind as ProtoKind;
1969 let kind = match vf {
1970 Filter::Exact(bytes) => ProtoKind::Exact(bytes),
1971 Filter::Prefix(bytes) => ProtoKind::Prefix(bytes),
1972 Filter::Regex(pattern) => ProtoKind::Regex(pattern),
1973 };
1974 exoware_proto::common::kv::v1::Filter {
1975 kind: Some(kind),
1976 ..Default::default()
1977 }
1978 })
1979 .collect();
1980 let request = exoware_proto::log::stream::v1::SubscribeRequest {
1981 selectors,
1982 value_filters,
1983 since_sequence_number,
1984 ..Default::default()
1985 };
1986 let config = self.streaming_client_config(self.stream_uri.clone());
1987 let client =
1988 exoware_proto::log::stream::v1::ServiceClient::new(self.connect_http.clone(), config);
1989 let stream = self
1990 .with_streaming_timeout(client.subscribe(request))
1991 .await
1992 .map_err(|err| client_error_from_connect(err, self.credential))?;
1993 Ok(StreamSubscription {
1994 stream,
1995 key_prefix: None,
1996 credential: self.credential,
1997 })
1998 }
1999
2000 async fn stream_get_physical(
2005 &self,
2006 sequence_number: u64,
2007 ) -> Result<Option<exoware_proto::log::stream::v1::GetResponse>, ClientError> {
2008 let config = self.unary_client_config(self.stream_uri.clone());
2009 let client =
2010 exoware_proto::log::stream::v1::ServiceClient::new(self.connect_http.clone(), config);
2011 match client
2012 .get(exoware_proto::log::stream::v1::GetRequest {
2013 sequence_number,
2014 ..Default::default()
2015 })
2016 .await
2017 {
2018 Ok(resp) => Ok(Some(resp.into_owned())),
2019 Err(err) => {
2020 if is_batch_missing_error(&err) {
2021 Ok(None)
2022 } else {
2023 Err(client_error_from_connect(err, self.credential))
2024 }
2025 }
2026 }
2027 }
2028
2029 async fn set_retention(
2034 &self,
2035 policy: Option<crate::retention::RetentionPolicy>,
2036 ) -> Result<Option<u64>, ClientError> {
2037 let request = exoware_proto::log::retention::v1::SetRetentionRequest {
2038 policy: policy
2039 .as_ref()
2040 .map(exoware_proto::retention_policy_to_proto)
2041 .into(),
2042 ..Default::default()
2043 };
2044 let config = self.unary_client_config(self.retention_uri.clone());
2045 let client = exoware_proto::log::retention::v1::ServiceClient::new(
2046 self.connect_http.clone(),
2047 config,
2048 );
2049 let response = client
2050 .set_retention(request)
2051 .await
2052 .map_err(|err| client_error_from_connect(err, self.credential))?;
2053 Ok(response.into_owned().oldest_retained_sequence)
2054 }
2055
2056 pub async fn health(&self) -> Result<bool, ClientError> {
2057 let resp = self
2058 .health_http
2059 .get(format!("{}/health", self.health_url))
2060 .send()
2061 .await?;
2062 Ok(resp.status().is_success())
2063 }
2064
2065 pub async fn ready(&self) -> Result<bool, ClientError> {
2066 let resp = self
2067 .health_http
2068 .get(format!("{}/ready", self.health_url))
2069 .send()
2070 .await?;
2071 Ok(resp.status().is_success())
2072 }
2073
2074 fn normalize_min_sequence_number(&self, requested_sequence: Option<u64>) -> Option<u64> {
2075 requested_sequence.filter(|sequence| *sequence > 0)
2076 }
2077
2078 async fn send_get(
2079 &self,
2080 key: &Key,
2081 min_sequence_number: Option<u64>,
2082 ) -> Result<
2083 (
2084 exoware_proto::query::GetResponse,
2085 Option<proto_query::Detail>,
2086 ),
2087 ClientError,
2088 > {
2089 if !is_valid_key_size(key.len()) {
2090 return Err(ClientError::WireFormat(format!(
2091 "key length {} is outside valid store key range ({}..={})",
2092 key.len(),
2093 keys::MIN_KEY_LEN,
2094 MAX_KEY_LEN
2095 )));
2096 }
2097
2098 let config = self.unary_client_config(self.query_uri.clone());
2099 let client = QueryServiceClient::new(self.connect_http.clone(), config);
2100 let response = self
2101 .send_with_retry(|| async {
2102 client
2103 .get(ProtoGetRequest {
2104 key: key.clone().into(),
2105 min_sequence_number,
2106 ..Default::default()
2107 })
2108 .await
2109 })
2110 .await?;
2111 let owned = response.into_owned();
2112 let detail = owned.detail.as_option().cloned();
2113 Ok((owned, detail))
2114 }
2115
2116 #[cfg(test)]
2117 pub async fn send_get_for_tests(
2118 &self,
2119 key: &Key,
2120 min_sequence_number: Option<u64>,
2121 ) -> Result<
2122 (
2123 exoware_proto::query::GetResponse,
2124 Option<proto_query::Detail>,
2125 ),
2126 ClientError,
2127 > {
2128 self.send_get(key, min_sequence_number).await
2129 }
2130
2131 async fn range_stream_internal(
2132 &self,
2133 start: &Key,
2134 end: &Key,
2135 limit: usize,
2136 batch_size: usize,
2137 mode: RangeMode,
2138 options: QueryStreamReadOptions,
2139 ) -> Result<RangeStream, ClientError> {
2140 if !is_valid_key_size(start.len()) || !is_valid_key_size(end.len()) {
2141 return Err(ClientError::WireFormat(
2142 "range start/end key length is outside valid store key range".to_string(),
2143 ));
2144 }
2145 if batch_size == 0 {
2146 return Err(ClientError::WireFormat(
2147 "batch_size must be positive".to_string(),
2148 ));
2149 }
2150
2151 let config = self.streaming_client_config(self.query_uri.clone());
2152 let client = QueryServiceClient::new(self.connect_http.clone(), config);
2153 let min_sequence_number = self.normalize_min_sequence_number(options.min_sequence_number);
2154 let max_attempts = self.retry_config.max_attempts.max(1);
2155 let mut attempt = 1usize;
2156 loop {
2157 let result = self
2162 .with_streaming_timeout(async {
2163 let response = client
2164 .range(ProtoRangeRequest {
2165 start: start.clone().into(),
2166 end: end.clone().into(),
2167 limit: (limit != usize::MAX)
2168 .then(|| u32::try_from(limit).unwrap_or(u32::MAX)),
2169 batch_size: u32::try_from(batch_size).unwrap_or(u32::MAX),
2170 mode: mode.to_proto().into(),
2171 min_sequence_number,
2172 ..Default::default()
2173 })
2174 .await?;
2175 let mut stream = RangeStream::from_connect_stream(
2176 response,
2177 options.observed_sequence.clone(),
2178 None,
2179 self.credential,
2180 );
2181 stream.prefetch_first_frame().await?;
2182 Ok(stream)
2183 })
2184 .await;
2185 match result {
2186 Ok(stream) => return Ok(stream),
2187 Err(err) => {
2188 if attempt < max_attempts && is_retryable_error(&err) {
2189 let delay = retry_delay_for_error(&err, attempt, self.retry_config);
2190 tracing::debug!(
2191 attempt,
2192 max_attempts,
2193 code = err.code.as_str(),
2194 delay_ms = delay.as_millis() as u64,
2195 "store client retrying transient stream-open error",
2196 );
2197 tokio::time::sleep(delay).await;
2198 attempt += 1;
2199 continue;
2200 }
2201 return Err(client_error_from_connect(err, self.credential));
2202 }
2203 }
2204 }
2205 }
2206
2207 async fn range_reduce_stream_internal(
2208 &self,
2209 start: &Key,
2210 end: &Key,
2211 request: &DomainRangeReduceRequest,
2212 options: QueryStreamReadOptions,
2213 ) -> Result<ReduceStream, ClientError> {
2214 let config = self.streaming_client_config(self.query_uri.clone());
2215 let client = QueryServiceClient::new(self.connect_http.clone(), config);
2216 let proto_params = proto_to_proto_reduce_params(request.clone());
2217 let min_sequence_number = self.normalize_min_sequence_number(options.min_sequence_number);
2218 let (stream, first) = self
2219 .send_with_retry(|| {
2220 self.with_streaming_timeout(async {
2221 let mut stream = client
2222 .reduce(ProtoWireReduceRequest {
2223 start: start.clone().into(),
2224 end: end.clone().into(),
2225 params: Some(proto_params.clone()).into(),
2226 min_sequence_number,
2227 ..Default::default()
2228 })
2229 .await?;
2230 let first = stream
2231 .message::<proto_query::ReduceResponse>()
2232 .await?
2233 .ok_or_else(|| {
2234 ConnectError::new(
2235 ErrorCode::Internal,
2236 "reduction stream returned no frames",
2237 )
2238 })?;
2239 Ok((stream, first))
2240 })
2241 })
2242 .await?;
2243 let observed_sequence = options.observed_sequence;
2244 if let (Some(sequence), Some(detail)) =
2245 (&observed_sequence, first.view().detail.as_option())
2246 {
2247 sequence.fetch_max(detail.sequence_number, Ordering::SeqCst);
2248 }
2249 let credential = self.credential;
2250 Ok(
2251 futures::stream::try_unfold((stream, Some(first)), move |(mut stream, first)| {
2252 let observed_sequence = observed_sequence.clone();
2253 async move {
2254 let frame = if let Some(frame) = first {
2255 frame
2256 } else {
2257 match stream
2258 .message()
2259 .await
2260 .map_err(|err| client_error_from_connect(err, credential))?
2261 {
2262 Some(frame) => {
2263 if let (Some(sequence), Some(detail)) =
2264 (&observed_sequence, frame.view().detail.as_option())
2265 {
2266 sequence.fetch_max(detail.sequence_number, Ordering::SeqCst);
2267 }
2268 frame
2269 }
2270 None => return Ok(None),
2271 }
2272 };
2273 Ok(Some((frame, (stream, None))))
2274 }
2275 })
2276 .boxed(),
2277 )
2278 }
2279
2280 async fn send_with_retry<F, Fut, T>(&self, mut make_request: F) -> Result<T, ClientError>
2281 where
2282 F: FnMut() -> Fut,
2283 Fut: std::future::Future<Output = Result<T, ConnectError>>,
2284 {
2285 let max_attempts = self.retry_config.max_attempts.max(1);
2286 let mut attempt = 1usize;
2287 loop {
2288 match make_request().await {
2289 Ok(response) => return Ok(response),
2290 Err(err) => {
2291 if attempt < max_attempts && is_retryable_error(&err) {
2292 let delay = retry_delay_for_error(&err, attempt, self.retry_config);
2293 tracing::debug!(
2294 attempt,
2295 max_attempts,
2296 code = err.code.as_str(),
2297 delay_ms = delay.as_millis() as u64,
2298 "store client retrying transient RPC error",
2299 );
2300 tokio::time::sleep(delay).await;
2301 attempt += 1;
2302 continue;
2303 }
2304 return Err(client_error_from_connect(err, self.credential));
2305 }
2306 }
2307 }
2308 }
2309}
2310
2311fn shift_reduce_request_key_offsets(
2312 prefix_len: usize,
2313 request: &mut DomainRangeReduceRequest,
2314) -> Result<(), StoreKeyPrefixError> {
2315 debug_assert!(prefix_len <= MAX_KEY_LEN);
2321 let shift_bytes = prefix_len as u16;
2322 let shift_bits = shift_bytes * 8;
2323 for reducer in &mut request.reducers {
2324 if let Some(expr) = &mut reducer.expr {
2325 shift_expr_key_offsets(shift_bytes, shift_bits, expr)?;
2326 }
2327 }
2328 for expr in &mut request.group_by {
2329 shift_expr_key_offsets(shift_bytes, shift_bits, expr)?;
2330 }
2331 for filter in request.filter.iter_mut().chain(
2332 request
2333 .reducers
2334 .iter_mut()
2335 .filter_map(|reducer| reducer.filter.as_mut()),
2336 ) {
2337 for check in &mut filter.checks {
2338 shift_field_ref_key_offset(shift_bytes, shift_bits, &mut check.field)?;
2339 }
2340 }
2341 Ok(())
2342}
2343
2344fn shift_expr_key_offsets(
2345 shift_bytes: u16,
2346 shift_bits: u16,
2347 expr: &mut KvExpr,
2348) -> Result<(), StoreKeyPrefixError> {
2349 match expr {
2350 KvExpr::Field(field) => shift_field_ref_key_offset(shift_bytes, shift_bits, field),
2351 KvExpr::Literal(_) => Ok(()),
2352 KvExpr::Add(left, right)
2353 | KvExpr::Sub(left, right)
2354 | KvExpr::Mul(left, right)
2355 | KvExpr::Div(left, right) => {
2356 shift_expr_key_offsets(shift_bytes, shift_bits, left)?;
2357 shift_expr_key_offsets(shift_bytes, shift_bits, right)
2358 }
2359 KvExpr::Lower(inner) | KvExpr::DateTruncDay(inner) | KvExpr::CastFloat64(inner) => {
2360 shift_expr_key_offsets(shift_bytes, shift_bits, inner)
2361 }
2362 }
2363}
2364
2365fn shift_field_ref_key_offset(
2366 shift_bytes: u16,
2367 shift_bits: u16,
2368 field: &mut KvFieldRef,
2369) -> Result<(), StoreKeyPrefixError> {
2370 match field {
2371 KvFieldRef::Key { byte_offset, .. } => {
2372 *byte_offset = byte_offset.checked_add(shift_bytes).ok_or(
2373 StoreKeyPrefixError::KeyOffsetOverflow {
2374 offset: *byte_offset,
2375 shift: shift_bytes,
2376 },
2377 )?;
2378 Ok(())
2379 }
2380 KvFieldRef::ZOrderKey { bit_offset, .. } => {
2381 *bit_offset = bit_offset.checked_add(shift_bits).ok_or(
2382 StoreKeyPrefixError::KeyOffsetOverflow {
2383 offset: *bit_offset,
2384 shift: shift_bits,
2385 },
2386 )?;
2387 Ok(())
2388 }
2389 KvFieldRef::Value { .. } => Ok(()),
2390 }
2391}
2392
2393#[derive(Clone, Copy, Debug)]
2396pub struct Ingest<'a> {
2397 c: &'a PrefixedStoreClient,
2398}
2399
2400#[derive(Clone, Copy, Debug)]
2401pub struct Query<'a> {
2402 c: &'a PrefixedStoreClient,
2403}
2404
2405#[derive(Clone, Copy, Debug)]
2406pub struct Prune<'a> {
2407 c: &'a PrefixedStoreClient,
2408}
2409
2410#[derive(Clone, Copy, Debug)]
2411pub struct Retention<'a> {
2412 c: &'a PrefixedStoreClient,
2413}
2414
2415#[derive(Clone, Copy, Debug)]
2416pub struct Stream<'a> {
2417 c: &'a PrefixedStoreClient,
2418}
2419
2420impl<'a> Ingest<'a> {
2421 pub async fn put(&self, kvs: &[(&Key, &[u8])]) -> Result<u64, ClientError> {
2422 self.c.put(kvs).await
2423 }
2424
2425 pub async fn put_prepared(&self, batch: &StoreWriteBatch) -> Result<u64, ClientError> {
2428 batch.commit(self.c.client()).await
2429 }
2430}
2431
2432impl<'a> Query<'a> {
2433 pub async fn get(&self, key: &Key) -> Result<Option<Bytes>, ClientError> {
2434 self.c.get(key).await
2435 }
2436
2437 pub async fn get_with_min_sequence_number(
2438 &self,
2439 key: &Key,
2440 min_sequence_number: u64,
2441 ) -> Result<Option<Bytes>, ClientError> {
2442 self.c
2443 .get_with_min_sequence_number(key, min_sequence_number)
2444 .await
2445 }
2446
2447 pub async fn get_many(
2448 &self,
2449 keys: &[&Key],
2450 batch_size: u32,
2451 ) -> Result<GetManyStream, ClientError> {
2452 self.c.get_many(keys, batch_size).await
2453 }
2454
2455 pub async fn get_many_with_min_sequence_number(
2456 &self,
2457 keys: &[&Key],
2458 batch_size: u32,
2459 min_sequence_number: u64,
2460 ) -> Result<GetManyStream, ClientError> {
2461 self.c
2462 .get_many_with_min_sequence_number(keys, batch_size, min_sequence_number)
2463 .await
2464 }
2465
2466 pub async fn range(
2468 &self,
2469 start: &Key,
2470 end: &Key,
2471 limit: usize,
2472 ) -> Result<Vec<(Key, Bytes)>, ClientError> {
2473 self.c.range(start, end, limit).await
2474 }
2475
2476 pub async fn range_with_mode(
2477 &self,
2478 start: &Key,
2479 end: &Key,
2480 limit: usize,
2481 mode: RangeMode,
2482 ) -> Result<Vec<(Key, Bytes)>, ClientError> {
2483 self.c.range_with_mode(start, end, limit, mode).await
2484 }
2485
2486 pub async fn range_with_min_sequence_number(
2487 &self,
2488 start: &Key,
2489 end: &Key,
2490 limit: usize,
2491 min_sequence_number: u64,
2492 ) -> Result<Vec<(Key, Bytes)>, ClientError> {
2493 self.c
2494 .range_with_min_sequence_number(start, end, limit, min_sequence_number)
2495 .await
2496 }
2497
2498 pub async fn range_with_mode_and_min_sequence_number(
2499 &self,
2500 start: &Key,
2501 end: &Key,
2502 limit: usize,
2503 mode: RangeMode,
2504 min_sequence_number: u64,
2505 ) -> Result<Vec<(Key, Bytes)>, ClientError> {
2506 self.c
2507 .range_with_mode_and_min_sequence_number(start, end, limit, mode, min_sequence_number)
2508 .await
2509 }
2510
2511 pub async fn range_stream(
2512 &self,
2513 start: &Key,
2514 end: &Key,
2515 limit: usize,
2516 batch_size: usize,
2517 ) -> Result<RangeStream, ClientError> {
2518 self.c.range_stream(start, end, limit, batch_size).await
2519 }
2520
2521 pub async fn range_stream_with_mode(
2522 &self,
2523 start: &Key,
2524 end: &Key,
2525 limit: usize,
2526 batch_size: usize,
2527 mode: RangeMode,
2528 ) -> Result<RangeStream, ClientError> {
2529 self.c
2530 .range_stream_with_mode(start, end, limit, batch_size, mode)
2531 .await
2532 }
2533
2534 pub async fn range_stream_with_min_sequence_number(
2535 &self,
2536 start: &Key,
2537 end: &Key,
2538 limit: usize,
2539 batch_size: usize,
2540 min_sequence_number: u64,
2541 ) -> Result<RangeStream, ClientError> {
2542 self.c
2543 .range_stream_with_min_sequence_number(
2544 start,
2545 end,
2546 limit,
2547 batch_size,
2548 min_sequence_number,
2549 )
2550 .await
2551 }
2552
2553 pub async fn range_stream_with_mode_and_min_sequence_number(
2554 &self,
2555 start: &Key,
2556 end: &Key,
2557 limit: usize,
2558 batch_size: usize,
2559 mode: RangeMode,
2560 min_sequence_number: u64,
2561 ) -> Result<RangeStream, ClientError> {
2562 self.c
2563 .range_stream_with_mode_and_min_sequence_number(
2564 start,
2565 end,
2566 limit,
2567 batch_size,
2568 mode,
2569 min_sequence_number,
2570 )
2571 .await
2572 }
2573
2574 pub async fn range_reduce(
2575 &self,
2576 start: &Key,
2577 end: &Key,
2578 request: &DomainRangeReduceRequest,
2579 ) -> Result<Vec<Option<KvReducedValue>>, ClientError> {
2580 scalar_reduce_results(
2581 self.range_reduce_stream(start, end, request).await?,
2582 request,
2583 )
2584 .await
2585 }
2586
2587 pub async fn range_reduce_with_min_sequence_number(
2588 &self,
2589 start: &Key,
2590 end: &Key,
2591 request: &DomainRangeReduceRequest,
2592 min_sequence_number: u64,
2593 ) -> Result<Vec<Option<KvReducedValue>>, ClientError> {
2594 scalar_reduce_results(
2595 self.range_reduce_stream_with_min_sequence_number(
2596 start,
2597 end,
2598 request,
2599 min_sequence_number,
2600 )
2601 .await?,
2602 request,
2603 )
2604 .await
2605 }
2606
2607 pub async fn range_reduce_stream(
2608 &self,
2609 start: &Key,
2610 end: &Key,
2611 request: &DomainRangeReduceRequest,
2612 ) -> Result<ReduceStream, ClientError> {
2613 self.c
2614 .range_reduce_stream_internal(start, end, request, QueryStreamReadOptions::default())
2615 .await
2616 }
2617
2618 pub async fn range_reduce_stream_with_min_sequence_number(
2619 &self,
2620 start: &Key,
2621 end: &Key,
2622 request: &DomainRangeReduceRequest,
2623 min_sequence_number: u64,
2624 ) -> Result<ReduceStream, ClientError> {
2625 self.c
2626 .range_reduce_stream_internal(
2627 start,
2628 end,
2629 request,
2630 QueryStreamReadOptions {
2631 min_sequence_number: Some(min_sequence_number),
2632 observed_sequence: None,
2633 },
2634 )
2635 .await
2636 }
2637}
2638
2639impl<'a> Prune<'a> {
2640 pub async fn prune(
2642 &self,
2643 policies: &[crate::prune_policy::PrunePolicy],
2644 ) -> Result<(), ClientError> {
2645 self.c.apply_prune_policies(policies).await
2646 }
2647}
2648
2649impl<'a> Stream<'a> {
2650 pub async fn subscribe(
2656 &self,
2657 filter: crate::stream_filter::StreamFilter,
2658 since_sequence_number: Option<u64>,
2659 ) -> Result<StreamSubscription, ClientError> {
2660 self.c.subscribe(filter, since_sequence_number).await
2661 }
2662
2663 pub async fn get(
2666 &self,
2667 sequence_number: u64,
2668 ) -> Result<Option<Vec<(Key, Bytes)>>, ClientError> {
2669 self.c.stream_get(sequence_number).await
2670 }
2671}
2672
2673impl<'a> Retention<'a> {
2674 pub async fn set_retention(
2681 &self,
2682 policy: Option<crate::retention::RetentionPolicy>,
2683 ) -> Result<Option<u64>, ClientError> {
2684 self.c.set_retention(policy).await
2685 }
2686}
2687
2688impl SerializableReadSession {
2689 pub fn fixed_sequence(&self) -> Option<u64> {
2694 self.state.fixed_sequence()
2695 }
2696
2697 pub fn evaluated_sequence(&self) -> Option<u64> {
2701 self.state.evaluated_sequence()
2702 }
2703
2704 pub fn with_client(&self, client: PrefixedStoreClient) -> Self {
2706 Self {
2707 client,
2708 state: self.state.clone(),
2709 }
2710 }
2711
2712 pub async fn get(&self, key: &Key) -> Result<Option<Bytes>, ClientError> {
2713 self.run_read(|sequence, observed_sequence| async move {
2714 let (response, detail) = self.client.send_get(key, sequence).await?;
2715 if let Some(detail) = detail {
2716 observed_sequence.fetch_max(detail.sequence_number, Ordering::SeqCst);
2717 }
2718 Ok(response.value)
2719 })
2720 .await
2721 }
2722
2723 pub async fn get_many(
2724 &self,
2725 keys: &[&Key],
2726 batch_size: u32,
2727 ) -> Result<GetManyStream, ClientError> {
2728 self.run_read(|sequence, observed_sequence| {
2729 self.client
2730 .get_many_internal(keys, batch_size, sequence, Some(observed_sequence))
2731 })
2732 .await
2733 }
2734
2735 pub async fn range(
2736 &self,
2737 start: &Key,
2738 end: &Key,
2739 limit: usize,
2740 ) -> Result<Vec<(Key, Bytes)>, ClientError> {
2741 self.range_with_mode(start, end, limit, RangeMode::Forward)
2742 .await
2743 }
2744
2745 pub async fn range_with_mode(
2746 &self,
2747 start: &Key,
2748 end: &Key,
2749 limit: usize,
2750 mode: RangeMode,
2751 ) -> Result<Vec<(Key, Bytes)>, ClientError> {
2752 self.run_read(|sequence, observed_sequence| async move {
2753 self.client
2754 .range_stream_internal(
2755 start,
2756 end,
2757 limit,
2758 limit.max(1),
2759 mode,
2760 QueryStreamReadOptions {
2761 min_sequence_number: sequence,
2762 observed_sequence: Some(observed_sequence),
2763 },
2764 )
2765 .await?
2766 .collect()
2767 .await
2768 })
2769 .await
2770 }
2771
2772 pub async fn range_stream(
2773 &self,
2774 start: &Key,
2775 end: &Key,
2776 limit: usize,
2777 batch_size: usize,
2778 ) -> Result<RangeStream, ClientError> {
2779 self.range_stream_with_mode(start, end, limit, batch_size, RangeMode::Forward)
2780 .await
2781 }
2782
2783 pub async fn range_stream_with_mode(
2784 &self,
2785 start: &Key,
2786 end: &Key,
2787 limit: usize,
2788 batch_size: usize,
2789 mode: RangeMode,
2790 ) -> Result<RangeStream, ClientError> {
2791 self.run_read(|sequence, observed_sequence| {
2792 self.client.range_stream_internal(
2793 start,
2794 end,
2795 limit,
2796 batch_size,
2797 mode,
2798 QueryStreamReadOptions {
2799 min_sequence_number: sequence,
2800 observed_sequence: Some(observed_sequence),
2801 },
2802 )
2803 })
2804 .await
2805 }
2806
2807 pub async fn range_reduce(
2808 &self,
2809 start: &Key,
2810 end: &Key,
2811 request: &DomainRangeReduceRequest,
2812 ) -> Result<Vec<Option<KvReducedValue>>, ClientError> {
2813 scalar_reduce_results(
2814 self.range_reduce_stream(start, end, request).await?,
2815 request,
2816 )
2817 .await
2818 }
2819
2820 pub async fn range_reduce_stream(
2821 &self,
2822 start: &Key,
2823 end: &Key,
2824 request: &DomainRangeReduceRequest,
2825 ) -> Result<ReduceStream, ClientError> {
2826 self.run_read(|sequence, observed_sequence| {
2827 self.client.range_reduce_stream_internal(
2828 start,
2829 end,
2830 request,
2831 QueryStreamReadOptions {
2832 min_sequence_number: sequence,
2833 observed_sequence: Some(observed_sequence),
2834 },
2835 )
2836 })
2837 .await
2838 }
2839
2840 async fn run_read<T, Call, Fut>(&self, call: Call) -> Result<T, ClientError>
2841 where
2842 Call: FnOnce(Option<u64>, Arc<AtomicU64>) -> Fut,
2843 Fut: std::future::Future<Output = Result<T, ClientError>>,
2844 {
2845 if let Some(sequence) = self.fixed_sequence() {
2846 return call(Some(sequence), self.state.sequence.clone()).await;
2847 }
2848
2849 let gate = self.state.init_gate.lock().await;
2850
2851 if let Some(sequence) = self.fixed_sequence() {
2852 drop(gate);
2853 return call(Some(sequence), self.state.sequence.clone()).await;
2854 }
2855
2856 let result = call(None, self.state.sequence.clone()).await;
2857 drop(gate);
2858 result
2859 }
2860}
2861
2862fn is_retryable_error(err: &ConnectError) -> bool {
2863 matches!(
2864 err.code,
2865 ErrorCode::Aborted
2866 | ErrorCode::ResourceExhausted
2867 | ErrorCode::Unavailable
2868 | ErrorCode::Unknown
2869 | ErrorCode::Internal
2874 )
2875}
2876
2877fn retry_delay_for_error(
2878 err: &ConnectError,
2879 attempt: usize,
2880 retry_config: RetryConfig,
2881) -> Duration {
2882 if let Ok(decoded) = proto_decode_connect_error(err) {
2883 if let Some(retry_info) = decoded.retry_info {
2884 if let Some(delay) = retry_info.retry_delay.as_option() {
2885 let secs = u64::try_from(delay.seconds).unwrap_or(0);
2886 let nanos = u32::try_from(delay.nanos.max(0)).unwrap_or(0);
2887 let hinted = Duration::new(secs, nanos);
2888 if !hinted.is_zero() {
2889 return hinted.min(retry_config.max_backoff);
2890 }
2891 }
2892 }
2893 }
2894 retry_backoff_delay(attempt, retry_config)
2895}
2896
2897fn retry_backoff_delay(attempt: usize, retry_config: RetryConfig) -> Duration {
2898 let exponent = (attempt.saturating_sub(1)).min(20) as u32;
2899 let factor = 1u128 << exponent;
2900 let base_ms = retry_config.initial_backoff.as_millis();
2901 let capped_ms = base_ms
2902 .saturating_mul(factor)
2903 .min(retry_config.max_backoff.as_millis());
2904 Duration::from_millis(capped_ms.min(u64::MAX as u128) as u64)
2905}
2906
2907#[cfg(test)]
2908mod tests {
2909 use super::*;
2910 use crate::kv_codec::{KvFieldKind, KvPredicate, KvPredicateCheck, KvPredicateConstraint};
2911 use buffa::Message as _;
2912 use exoware_proto::query::TraversalMode as ProtoTraversalMode;
2913 use http::header::{ACCEPT_ENCODING, AUTHORIZATION};
2914
2915 #[derive(Clone, Debug, Default)]
2916 struct RecordingTransport {
2917 requests: Arc<std::sync::Mutex<Vec<(http::Uri, http::HeaderMap)>>>,
2918 }
2919
2920 impl RecordingTransport {
2921 fn requests(&self) -> Vec<(http::Uri, http::HeaderMap)> {
2922 self.requests.lock().unwrap().clone()
2923 }
2924 }
2925
2926 impl connectrpc::client::ClientTransport for RecordingTransport {
2927 type ResponseBody =
2928 http_body_util::combinators::UnsyncBoxBody<Bytes, std::convert::Infallible>;
2929 type Error = ConnectError;
2930
2931 fn send(
2932 &self,
2933 request: http::Request<connectrpc::client::ClientBody>,
2934 ) -> connectrpc::client::BoxFuture<
2935 'static,
2936 Result<http::Response<Self::ResponseBody>, Self::Error>,
2937 > {
2938 self.requests
2939 .lock()
2940 .unwrap()
2941 .push((request.uri().clone(), request.headers().clone()));
2942 Box::pin(async { Err(ConnectError::unavailable("recorded test request")) })
2943 }
2944 }
2945
2946 #[derive(Clone, Copy, Debug)]
2947 struct StalledStreamTransport;
2948
2949 impl connectrpc::client::ClientTransport for StalledStreamTransport {
2950 type ResponseBody =
2951 http_body_util::combinators::UnsyncBoxBody<Bytes, std::convert::Infallible>;
2952 type Error = ConnectError;
2953
2954 fn send(
2955 &self,
2956 _request: http::Request<connectrpc::client::ClientBody>,
2957 ) -> connectrpc::client::BoxFuture<
2958 'static,
2959 Result<http::Response<Self::ResponseBody>, Self::Error>,
2960 > {
2961 Box::pin(async {
2962 let frames = futures::stream::pending::<
2963 Result<http_body::Frame<Bytes>, std::convert::Infallible>,
2964 >();
2965 let body =
2966 http_body_util::BodyExt::boxed_unsync(http_body_util::StreamBody::new(frames));
2967 Ok(http::Response::new(body))
2968 })
2969 }
2970 }
2971
2972 #[derive(Clone)]
2973 struct ReduceTransport {
2974 bodies: Arc<std::sync::Mutex<std::collections::VecDeque<Bytes>>>,
2975 requests: Arc<std::sync::Mutex<Vec<proto_query::ReduceRequest>>>,
2976 }
2977
2978 impl ReduceTransport {
2979 fn new(bodies: impl IntoIterator<Item = Bytes>) -> Self {
2980 Self {
2981 bodies: Arc::new(std::sync::Mutex::new(bodies.into_iter().collect())),
2982 requests: Arc::default(),
2983 }
2984 }
2985 }
2986
2987 impl connectrpc::client::ClientTransport for ReduceTransport {
2988 type ResponseBody = http_body_util::Full<Bytes>;
2989 type Error = ConnectError;
2990
2991 fn send(
2992 &self,
2993 request: http::Request<connectrpc::client::ClientBody>,
2994 ) -> connectrpc::client::BoxFuture<
2995 'static,
2996 Result<http::Response<Self::ResponseBody>, Self::Error>,
2997 > {
2998 use buffa::Message;
2999 use http_body_util::BodyExt;
3000
3001 let transport = self.clone();
3002 Box::pin(async move {
3003 assert_eq!(request.uri().path(), "/store.query.v1.Service/Reduce");
3004 let body = request.into_body().collect().await.unwrap().to_bytes();
3005 let mut body = bytes::BytesMut::from(body.as_ref());
3006 let envelope = connectrpc::envelope::Envelope::decode(&mut body)
3007 .unwrap()
3008 .unwrap();
3009 assert!(!envelope.is_compressed());
3010 assert!(body.is_empty());
3011 let request =
3012 proto_query::ReduceRequest::decode_from_slice(&envelope.data).unwrap();
3013 transport.requests.lock().unwrap().push(request);
3014 let body = transport
3015 .bodies
3016 .lock()
3017 .unwrap()
3018 .pop_front()
3019 .expect("unexpected retry");
3020 Ok(http::Response::builder()
3021 .header("content-type", "application/connect+proto")
3022 .body(http_body_util::Full::new(body))
3023 .unwrap())
3024 })
3025 }
3026 }
3027
3028 type ReduceBodyReceiver = futures::channel::mpsc::UnboundedReceiver<
3029 Result<http_body::Frame<Bytes>, std::convert::Infallible>,
3030 >;
3031
3032 #[derive(Clone)]
3033 struct PendingReduceTransport {
3034 receiver: Arc<std::sync::Mutex<Option<ReduceBodyReceiver>>>,
3035 }
3036
3037 impl connectrpc::client::ClientTransport for PendingReduceTransport {
3038 type ResponseBody =
3039 http_body_util::combinators::UnsyncBoxBody<Bytes, std::convert::Infallible>;
3040 type Error = ConnectError;
3041
3042 fn send(
3043 &self,
3044 _request: http::Request<connectrpc::client::ClientBody>,
3045 ) -> connectrpc::client::BoxFuture<
3046 'static,
3047 Result<http::Response<Self::ResponseBody>, Self::Error>,
3048 > {
3049 use http_body_util::BodyExt;
3050
3051 let receiver = self.receiver.lock().unwrap().take().unwrap();
3052 Box::pin(async move {
3053 Ok(http::Response::builder()
3054 .header("content-type", "application/connect+proto")
3055 .body(http_body_util::StreamBody::new(receiver).boxed_unsync())
3056 .unwrap())
3057 })
3058 }
3059 }
3060
3061 #[tokio::test]
3062 async fn reduce_delivers_a_frame_before_eof_and_drop_cancels_the_body() {
3063 let (sender, receiver) = futures::channel::mpsc::unbounded();
3064 let transport = PendingReduceTransport {
3065 receiver: Arc::new(std::sync::Mutex::new(Some(receiver))),
3066 };
3067 let client = StoreClient::builder()
3068 .url("http://query.internal")
3069 .retry_config(RetryConfig::disabled())
3070 .client_transport(transport)
3071 .build()
3072 .unwrap()
3073 .prefixed(StoreKeyPrefix::new("tenant/").unwrap());
3074 use buffa::Message;
3075 let first =
3076 connectrpc::envelope::Envelope::data(count_frame(None).encode_to_bytes()).encode();
3077 sender
3078 .unbounded_send(Ok(http_body::Frame::data(first)))
3079 .unwrap();
3080 let mut stream = client
3081 .query()
3082 .range_reduce_stream(
3083 &Bytes::from_static(b"a"),
3084 &Bytes::from_static(b"z"),
3085 &count_request(),
3086 )
3087 .await
3088 .unwrap();
3089 assert_eq!(
3090 stream.next().await.unwrap().unwrap().view().results.len(),
3091 1
3092 );
3093 assert!(!sender.is_closed());
3094 drop(stream);
3095 assert!(sender.is_closed());
3096 }
3097
3098 fn reduce_body(frames: &[proto_query::ReduceResponse], final_status: &[u8]) -> Bytes {
3099 use buffa::Message;
3100
3101 let mut body = Vec::new();
3102 for frame in frames {
3103 body.extend_from_slice(
3104 &connectrpc::envelope::Envelope::data(frame.encode_to_bytes()).encode(),
3105 );
3106 }
3107 body.extend_from_slice(
3108 &connectrpc::envelope::Envelope::end_stream(Bytes::copy_from_slice(final_status))
3109 .encode(),
3110 );
3111 body.into()
3112 }
3113
3114 fn count_request() -> DomainRangeReduceRequest {
3115 DomainRangeReduceRequest {
3116 reducers: vec![RangeReducerSpec {
3117 filter: None,
3118 op: RangeReduceOp::CountAll,
3119 expr: None,
3120 }],
3121 group_by: Vec::new(),
3122 filter: None,
3123 }
3124 }
3125
3126 fn count_frame(sequence: Option<u64>) -> proto_query::ReduceResponse {
3127 proto_query::ReduceResponse {
3128 results: vec![proto_query::RangeReduceResult {
3129 value: Some(to_proto_reduced_value(KvReducedValue::UInt64(3))).into(),
3130 ..Default::default()
3131 }],
3132 detail: sequence
3133 .map(|sequence_number| proto_query::Detail {
3134 sequence_number,
3135 ..Default::default()
3136 })
3137 .into(),
3138 ..Default::default()
3139 }
3140 }
3141
3142 fn reduce_test_client(transport: ReduceTransport) -> PrefixedStoreClient {
3143 StoreClient::builder()
3144 .url("http://query.internal")
3145 .retry_config(RetryConfig {
3146 max_attempts: 2,
3147 initial_backoff: Duration::ZERO,
3148 max_backoff: Duration::ZERO,
3149 })
3150 .client_transport(transport)
3151 .build()
3152 .unwrap()
3153 .prefixed(StoreKeyPrefix::new("tenant/").unwrap())
3154 }
3155
3156 #[tokio::test]
3157 async fn reduce_retries_before_first_frame_and_observes_response_details() {
3158 let transport = ReduceTransport::new([
3159 reduce_body(
3160 &[],
3161 br#"{"error":{"code":"unavailable","message":"retry open"}}"#,
3162 ),
3163 reduce_body(&[count_frame(None), count_frame(Some(47))], b"{}"),
3164 reduce_body(&[count_frame(Some(50))], b"{}"),
3165 ]);
3166 let session = reduce_test_client(transport.clone()).create_session();
3167 let start = Bytes::from_static(b"a");
3168 let end = Bytes::from_static(b"z");
3169 let request = count_request();
3170 let mut stream = session
3171 .range_reduce_stream(&start, &end, &request)
3172 .await
3173 .unwrap();
3174 assert_eq!(transport.requests.lock().unwrap().len(), 2);
3175 assert_eq!(session.fixed_sequence(), None);
3176 assert!(stream
3177 .next()
3178 .await
3179 .unwrap()
3180 .unwrap()
3181 .view()
3182 .detail
3183 .as_option()
3184 .is_none());
3185 assert_eq!(session.fixed_sequence(), None);
3186 assert_eq!(
3187 stream
3188 .next()
3189 .await
3190 .unwrap()
3191 .unwrap()
3192 .view()
3193 .detail
3194 .sequence_number,
3195 47
3196 );
3197 assert_eq!(session.fixed_sequence(), Some(47));
3198 assert!(stream.next().await.is_none());
3199 assert_eq!(
3200 session.range_reduce(&start, &end, &request).await.unwrap(),
3201 vec![Some(KvReducedValue::UInt64(3))]
3202 );
3203 assert_eq!(session.fixed_sequence(), Some(50));
3204 let requests = transport.requests.lock().unwrap();
3205 assert_eq!(requests.len(), 3);
3206 assert_eq!(requests[0].min_sequence_number, None);
3207 assert_eq!(requests[1].min_sequence_number, None);
3208 assert_eq!(requests[2].min_sequence_number, Some(47));
3209 assert_eq!(requests[2].start.as_slice(), b"tenant/a");
3210 assert_eq!(requests[2].end.as_slice(), b"tenant/z");
3211 }
3212
3213 #[tokio::test]
3214 async fn reduce_never_retries_after_a_result_frame() {
3215 let transport = ReduceTransport::new([reduce_body(
3216 &[count_frame(None)],
3217 br#"{"error":{"code":"unavailable","message":"failed after results"}}"#,
3218 )]);
3219 let client = reduce_test_client(transport.clone());
3220 let mut stream = client
3221 .query()
3222 .range_reduce_stream(
3223 &Bytes::from_static(b"a"),
3224 &Bytes::from_static(b"z"),
3225 &count_request(),
3226 )
3227 .await
3228 .unwrap();
3229 assert_eq!(
3230 stream.next().await.unwrap().unwrap().view().results.len(),
3231 1
3232 );
3233 assert_eq!(
3234 stream.next().await.unwrap().unwrap_err().rpc_code(),
3235 Some(ErrorCode::Unavailable)
3236 );
3237 assert!(stream.next().await.is_none());
3238 assert_eq!(transport.requests.lock().unwrap().len(), 1);
3239 }
3240
3241 #[tokio::test]
3242 async fn scalar_reduce_validates_frame_count_shape_and_final_status() {
3243 let request = count_request();
3244 for frames in [
3245 vec![],
3246 vec![proto_query::ReduceResponse::default()],
3247 vec![count_frame(Some(1)), count_frame(Some(1))],
3248 vec![proto_query::ReduceResponse {
3249 groups: vec![proto_query::RangeReduceGroup::default()],
3250 ..count_frame(Some(1))
3251 }],
3252 ] {
3253 let stream = futures::stream::iter(
3254 frames
3255 .into_iter()
3256 .map(|frame| Ok(connectrpc::StreamMessage::from_message(&frame))),
3257 )
3258 .boxed();
3259 assert!(matches!(
3260 scalar_reduce_results(stream, &request).await,
3261 Err(ClientError::WireFormat(_))
3262 ));
3263 }
3264 let stream = futures::stream::iter([
3265 Ok(connectrpc::StreamMessage::from_message(&count_frame(Some(
3266 1,
3267 )))),
3268 Err(ClientError::Rpc(Box::new(ConnectError::unavailable(
3269 "final status",
3270 )))),
3271 ])
3272 .boxed();
3273 assert_eq!(
3274 scalar_reduce_results(stream, &request)
3275 .await
3276 .unwrap_err()
3277 .rpc_code(),
3278 Some(ErrorCode::Unavailable)
3279 );
3280 }
3281
3282 #[derive(Clone, Debug, Default)]
3283 struct SessionSequenceTransport {
3284 requested_floors: Arc<std::sync::Mutex<Vec<Option<u64>>>>,
3285 }
3286
3287 impl connectrpc::client::ClientTransport for SessionSequenceTransport {
3288 type ResponseBody =
3289 http_body_util::combinators::UnsyncBoxBody<Bytes, std::convert::Infallible>;
3290 type Error = ConnectError;
3291
3292 fn send(
3293 &self,
3294 request: http::Request<connectrpc::client::ClientBody>,
3295 ) -> connectrpc::client::BoxFuture<
3296 'static,
3297 Result<http::Response<Self::ResponseBody>, Self::Error>,
3298 > {
3299 let requested_floors = self.requested_floors.clone();
3300 Box::pin(async move {
3301 let (parts, body) = request.into_parts();
3302 let body = http_body_util::BodyExt::collect(body)
3303 .await
3304 .unwrap()
3305 .to_bytes();
3306 let path = parts.uri.path();
3307 let streaming = path.ends_with("/GetMany")
3308 || path.ends_with("/Range")
3309 || path.ends_with("/Reduce");
3310 let payload = if streaming { &body[5..] } else { &body[..] };
3311 let requested_floor = match path {
3312 "/store.query.v1.Service/Get" => {
3313 proto_query::GetRequest::decode_from_slice(payload)
3314 .unwrap()
3315 .min_sequence_number
3316 }
3317 "/store.query.v1.Service/GetMany" => {
3318 proto_query::GetManyRequest::decode_from_slice(payload)
3319 .unwrap()
3320 .min_sequence_number
3321 }
3322 "/store.query.v1.Service/Range" => {
3323 proto_query::RangeRequest::decode_from_slice(payload)
3324 .unwrap()
3325 .min_sequence_number
3326 }
3327 "/store.query.v1.Service/Reduce" => {
3328 proto_query::ReduceRequest::decode_from_slice(payload)
3329 .unwrap()
3330 .min_sequence_number
3331 }
3332 path => {
3333 return Err(ConnectError::unimplemented(format!(
3334 "unexpected test request path {path}"
3335 )));
3336 }
3337 };
3338 let sequence_number = {
3339 let mut floors = requested_floors.lock().unwrap();
3340 floors.push(requested_floor);
3341 40 + u64::try_from(floors.len()).unwrap()
3342 };
3343 let detail = proto_query::Detail {
3344 sequence_number,
3345 ..Default::default()
3346 };
3347 let payload = match path {
3348 "/store.query.v1.Service/Get" => proto_query::GetResponse {
3349 detail: detail.into(),
3350 ..Default::default()
3351 }
3352 .encode_to_bytes(),
3353 "/store.query.v1.Service/GetMany" => proto_query::GetManyFrame {
3354 detail: detail.into(),
3355 ..Default::default()
3356 }
3357 .encode_to_bytes(),
3358 "/store.query.v1.Service/Range" => proto_query::RangeFrame {
3359 detail: detail.into(),
3360 ..Default::default()
3361 }
3362 .encode_to_bytes(),
3363 "/store.query.v1.Service/Reduce" => {
3364 count_frame(Some(sequence_number)).encode_to_bytes()
3365 }
3366 _ => unreachable!(),
3367 };
3368 let (body, content_type) = if streaming {
3369 let mut envelope = Vec::with_capacity(payload.len() + 12);
3370 envelope.push(0);
3371 envelope
3372 .extend_from_slice(&u32::try_from(payload.len()).unwrap().to_be_bytes());
3373 envelope.extend_from_slice(&payload);
3374 envelope.extend_from_slice(&[2, 0, 0, 0, 2, b'{', b'}']);
3375 (Bytes::from(envelope), "application/connect+proto")
3376 } else {
3377 (payload, "application/proto")
3378 };
3379 let body = http_body_util::BodyExt::boxed_unsync(http_body_util::Full::new(body));
3380 Ok(http::Response::builder()
3381 .header(http::header::CONTENT_TYPE, content_type)
3382 .body(body)
3383 .unwrap())
3384 })
3385 }
3386 }
3387
3388 async fn assert_streaming_deadline<T>(
3389 future: impl std::future::Future<Output = Result<T, ClientError>>,
3390 ) {
3391 let result = tokio::time::timeout(Duration::from_secs(1), future)
3392 .await
3393 .expect("stream open did not honor the request timeout");
3394 assert_eq!(
3395 result.err().and_then(|err| err.rpc_code()),
3396 Some(ErrorCode::DeadlineExceeded)
3397 );
3398 }
3399
3400 #[test]
3401 fn hex_round_trip() {
3402 let data = vec![0x00, 0x42, 0xFF, 0xAB];
3403 let encoded = hex_encode(&data);
3404 assert_eq!(encoded, "0042ffab");
3405 let decoded = hex_decode(&encoded).unwrap();
3406 assert_eq!(decoded, data);
3407 }
3408
3409 #[test]
3410 fn client_creation() {
3411 let client = StoreClient::new("http://localhost:10000");
3412 assert_eq!(client.health_url, "http://localhost:10000");
3413 assert_eq!(client.ingest_uri.to_string(), "http://localhost:10000/");
3414 assert_eq!(client.query_uri.to_string(), "http://localhost:10000/");
3415 assert_eq!(client.stream_uri.to_string(), "http://localhost:10000/");
3416 }
3417
3418 #[test]
3419 fn an_explicit_key_records_that_one_will_be_sent() {
3420 assert_eq!(built_with_api_key("token").credential, Credential::Sent);
3421 }
3422
3423 #[test]
3424 fn client_creation_enables_tls_for_https() {
3425 let client = StoreClient::new("https://store.example.com");
3426 assert_eq!(client.health_url, "https://store.example.com");
3427 assert_eq!(client.ingest_uri.to_string(), "https://store.example.com/");
3428 assert_eq!(client.query_uri.to_string(), "https://store.example.com/");
3429 assert_eq!(client.stream_uri.to_string(), "https://store.example.com/");
3430 }
3431
3432 #[test]
3433 fn builder_supports_mixed_http_and_https_services() {
3434 let client = StoreClient::builder()
3435 .health_url("https://health.example.com")
3436 .ingest_url("http://ingest.internal")
3437 .query_url("https://query.example.com")
3438 .prune_url("http://prune.internal")
3439 .retention_url("http://retention.internal")
3440 .stream_url("https://stream.example.com")
3441 .build()
3442 .unwrap();
3443
3444 assert_eq!(client.ingest_uri.scheme_str(), Some("http"));
3445 assert_eq!(client.query_uri.scheme_str(), Some("https"));
3446 assert_eq!(client.prune_uri.scheme_str(), Some("http"));
3447 assert_eq!(client.retention_uri.scheme_str(), Some("http"));
3448 assert_eq!(client.stream_uri.scheme_str(), Some("https"));
3449 }
3450
3451 #[test]
3452 fn builder_rejects_non_http_connect_urls() {
3453 for url in ["store.example.com", "ftp://store.example.com", "/relative"] {
3454 assert!(matches!(
3455 StoreClient::builder().url(url).build(),
3456 Err(ClientBuildError::InvalidEndpointUrl { .. })
3457 ));
3458 }
3459 }
3460
3461 #[tokio::test]
3462 async fn custom_transport_keeps_high_level_features_and_split_origins() {
3463 let transport = RecordingTransport::default();
3464 let client = StoreClient::builder()
3465 .health_url("http://health.internal")
3466 .ingest_url("http://ingest.internal/base")
3467 .query_url("http://query.internal/base")
3468 .prune_url("http://prune.internal/base")
3469 .retention_url("http://retention.internal/base")
3470 .stream_url("http://stream.internal/base")
3471 .api_key("token-abc")
3472 .retry_config(RetryConfig::disabled())
3473 .client_transport(transport.clone())
3474 .build()
3475 .unwrap();
3476 let prefixed = client.prefixed(StoreKeyPrefix::new("tenant/").unwrap());
3477
3478 let _ = prefixed.put(&[]).await;
3479 let _ = prefixed
3480 .create_session()
3481 .get(&Bytes::from_static(b"key"))
3482 .await;
3483 let _ = prefixed.client().prune(&[]).await;
3484 let _ = prefixed.client().set_retention(None).await;
3485 let _ = prefixed.client().stream_get_physical(1).await;
3486
3487 let requests = transport.requests();
3488 let origins = requests
3489 .iter()
3490 .map(|(uri, _)| uri.authority().unwrap().as_str())
3491 .collect::<Vec<_>>();
3492 assert_eq!(
3493 origins,
3494 [
3495 "ingest.internal",
3496 "query.internal",
3497 "prune.internal",
3498 "retention.internal",
3499 "stream.internal",
3500 ]
3501 );
3502 for (_, headers) in requests {
3503 assert_eq!(headers.get(AUTHORIZATION).unwrap(), "Bearer token-abc");
3504 assert_eq!(headers.get(ACCEPT_ENCODING).unwrap(), "zstd, gzip");
3505 }
3506 }
3507
3508 #[tokio::test]
3509 async fn generated_reduce_stream_prefers_zstd() {
3510 let transport = RecordingTransport::default();
3511 let client = StoreClient::builder()
3512 .url("http://query.internal")
3513 .retry_config(RetryConfig::disabled())
3514 .client_transport(transport.clone())
3515 .build()
3516 .unwrap()
3517 .prefixed(StoreKeyPrefix::identity());
3518
3519 let _ = client
3520 .query()
3521 .range_reduce_stream(
3522 &Bytes::from_static(b"a"),
3523 &Bytes::from_static(b"z"),
3524 &count_request(),
3525 )
3526 .await;
3527
3528 let requests = transport.requests();
3529 assert_eq!(requests.len(), 1);
3530 let (uri, headers) = &requests[0];
3531 assert_eq!(uri.path(), "/store.query.v1.Service/Reduce");
3532 assert_eq!(
3533 headers.get("content-type").unwrap(),
3534 "application/connect+proto"
3535 );
3536 assert_eq!(headers.get(ACCEPT_ENCODING).unwrap(), "zstd, gzip");
3537 let stream_encoding = connectrpc::Protocol::Connect.accept_encoding_header();
3538 assert_eq!(headers.get(stream_encoding).unwrap(), "zstd, gzip");
3539 assert_eq!(headers.get_all(stream_encoding).iter().count(), 1);
3540 }
3541
3542 #[tokio::test]
3543 async fn streaming_timeout_includes_first_frame_prefetch() {
3544 let mut client = StoreClient::builder()
3545 .url("http://query.internal")
3546 .retry_config(RetryConfig::disabled())
3547 .client_transport(StalledStreamTransport)
3548 .build()
3549 .unwrap();
3550 client.rpc_timeout = Some(Duration::from_millis(25));
3551 let client = client.prefixed(StoreKeyPrefix::new("timeout/").unwrap());
3552 let key = Key::from(b"key".to_vec());
3553 let start = Key::from(b"a".to_vec());
3554 let end = Key::from(b"z".to_vec());
3555
3556 assert_streaming_deadline(client.get_many(&[&key], 1)).await;
3557 assert_streaming_deadline(client.range_stream(&start, &end, 1, 1)).await;
3558 assert_streaming_deadline(client.query().range_reduce_stream(
3559 &start,
3560 &end,
3561 &count_request(),
3562 ))
3563 .await;
3564 }
3565
3566 #[tokio::test]
3567 async fn session_stream_prefetch_records_sequence_before_returning() {
3568 let client = StoreClient::builder()
3569 .url("http://query.internal")
3570 .retry_config(RetryConfig::disabled())
3571 .client_transport(SessionSequenceTransport::default())
3572 .build()
3573 .unwrap()
3574 .prefixed(StoreKeyPrefix::new("prefetch/").unwrap());
3575 let key = Key::from(b"key".to_vec());
3576 let start = Key::from(b"a".to_vec());
3577 let end = Key::from(b"z".to_vec());
3578
3579 let get_many_session = client.create_session();
3580 let _stream = get_many_session.get_many(&[&key], 1).await.unwrap();
3581 assert_eq!(get_many_session.fixed_sequence(), Some(41));
3582 assert_eq!(get_many_session.evaluated_sequence(), Some(41));
3583
3584 let range_session = client.create_session();
3585 let _stream = range_session
3586 .range_stream(&start, &end, 1, 1)
3587 .await
3588 .unwrap();
3589 assert_eq!(range_session.fixed_sequence(), Some(42));
3590 assert_eq!(range_session.evaluated_sequence(), Some(42));
3591
3592 let reduce_session = client.create_session();
3593 let _stream = reduce_session
3594 .range_reduce_stream(&start, &end, &count_request())
3595 .await
3596 .unwrap();
3597 assert_eq!(reduce_session.fixed_sequence(), Some(43));
3598 assert_eq!(reduce_session.evaluated_sequence(), Some(43));
3599 }
3600
3601 #[tokio::test]
3602 async fn session_reads_advance_the_floor_across_all_read_methods() {
3603 for initial_sequence in [0, 27] {
3604 let transport = SessionSequenceTransport::default();
3605 let client = StoreClient::builder()
3606 .url("http://query.internal")
3607 .retry_config(RetryConfig::disabled())
3608 .client_transport(transport.clone())
3609 .build()
3610 .unwrap()
3611 .prefixed(StoreKeyPrefix::new("session/").unwrap());
3612 let session = client.create_session_with_sequence(initial_sequence);
3613 let shared_session = session.clone();
3614 let key = Key::from(b"key".to_vec());
3615 let start = Key::from(b"a".to_vec());
3616 let end = Key::from(b"z".to_vec());
3617 let reduction = count_request();
3618
3619 assert_eq!(session.evaluated_sequence(), None);
3620 assert_eq!(session.get(&key).await.unwrap(), None);
3621 assert_eq!(session.evaluated_sequence(), Some(41));
3622 shared_session.get_many(&[&key], 1).await.unwrap();
3623 assert_eq!(session.evaluated_sequence(), Some(42));
3624 session.range_stream(&start, &end, 1, 1).await.unwrap();
3625 assert_eq!(session.evaluated_sequence(), Some(43));
3626 session.range(&start, &end, 1).await.unwrap();
3627 assert_eq!(session.evaluated_sequence(), Some(44));
3628 session
3629 .range_reduce(&start, &end, &reduction)
3630 .await
3631 .unwrap();
3632 assert_eq!(session.evaluated_sequence(), Some(45));
3633 let _stream = session
3634 .range_reduce_stream(&start, &end, &reduction)
3635 .await
3636 .unwrap();
3637
3638 assert_eq!(session.fixed_sequence(), Some(46));
3639 assert_eq!(shared_session.evaluated_sequence(), Some(46));
3640 assert_eq!(
3641 *transport.requested_floors.lock().unwrap(),
3642 vec![
3643 (initial_sequence > 0).then_some(initial_sequence),
3644 Some(41),
3645 Some(42),
3646 Some(43),
3647 Some(44),
3648 Some(45),
3649 ]
3650 );
3651 }
3652 }
3653
3654 #[test]
3655 fn public_streams_remain_send_and_sync() {
3656 fn assert_send_sync<T: Send + Sync>() {}
3657
3658 assert_send_sync::<RangeStream>();
3659 assert_send_sync::<GetManyStream>();
3660 assert_send_sync::<StreamSubscription>();
3661 }
3662
3663 #[test]
3664 fn balanced_http2_builder_supports_mixed_service_schemes() {
3665 let timeout = Duration::from_millis(123);
3666 let client = StoreClient::builder()
3667 .health_url("https://health.example.com")
3668 .ingest_url("http://ingest.internal")
3669 .query_url("https://query.example.com")
3670 .prune_url("http://prune.internal")
3671 .retention_url("http://retention.internal")
3672 .stream_url("https://stream.example.com")
3673 .balanced_http2_transport(
3674 ProtoBalancedHttp2Config::default().with_request_timeout(timeout),
3675 )
3676 .build()
3677 .unwrap();
3678
3679 assert_eq!(client.ingest_uri.scheme_str(), Some("http"));
3680 assert_eq!(client.query_uri.scheme_str(), Some("https"));
3681 assert_eq!(client.rpc_timeout, Some(timeout));
3682 }
3683
3684 #[test]
3685 fn builder_fails_until_all_urls_set() {
3686 assert!(matches!(
3687 StoreClient::builder().health_url("http://h").build(),
3688 Err(ClientBuildError::MissingIngestUrl)
3689 ));
3690 assert!(matches!(
3691 StoreClient::builder()
3692 .health_url("http://h")
3693 .ingest_url("http://i")
3694 .build(),
3695 Err(ClientBuildError::MissingQueryUrl)
3696 ));
3697 assert!(matches!(
3698 StoreClient::builder()
3699 .health_url("http://h")
3700 .ingest_url("http://i")
3701 .query_url("http://q")
3702 .build(),
3703 Err(ClientBuildError::MissingPruneUrl)
3704 ));
3705 assert!(matches!(
3706 StoreClient::builder()
3707 .health_url("http://h")
3708 .ingest_url("http://i")
3709 .query_url("http://q")
3710 .prune_url("http://p")
3711 .build(),
3712 Err(ClientBuildError::MissingRetentionUrl)
3713 ));
3714 assert!(matches!(
3715 StoreClient::builder()
3716 .health_url("http://h")
3717 .ingest_url("http://i")
3718 .query_url("http://q")
3719 .prune_url("http://p")
3720 .retention_url("http://r")
3721 .build(),
3722 Err(ClientBuildError::MissingStreamUrl)
3723 ));
3724 }
3725
3726 fn built_with_api_key(key: &str) -> StoreClient {
3727 StoreClient::builder()
3728 .url("https://example.test")
3729 .api_key(key)
3730 .build()
3731 .expect("builder should accept this key")
3732 }
3733
3734 #[test]
3735 fn api_key_becomes_a_bearer_header() {
3736 let client = built_with_api_key("token-abc");
3737 assert_eq!(client.credential, Credential::Sent);
3738 }
3739
3740 #[test]
3741 fn api_key_is_redacted_in_debug_output() {
3742 let rendered = format!(
3743 "{:?}",
3744 StoreClient::builder()
3745 .url("https://example.test")
3746 .api_key("token-abc")
3747 );
3748
3749 assert!(!rendered.contains("token-abc"), "builder leaked the key");
3750 assert!(rendered.contains("<redacted>"));
3751 }
3752
3753 #[test]
3754 fn a_bearer_header_never_renders_the_key() {
3755 let rendered = format!("{client:?}", client = built_with_api_key("token-abc"));
3756
3757 assert!(!rendered.contains("token-abc"));
3758 }
3759
3760 fn built_with_env_key(key: Option<&str>, unusable_env_key: UnusableEnvKey) -> StoreClient {
3763 StoreClient::builder()
3764 .url("https://example.test")
3765 .build_with(key.map(str::to_string), unusable_env_key)
3766 .expect("builder should accept this environment key")
3767 }
3768
3769 #[test]
3770 fn no_api_key_sends_no_header() {
3771 let client = built_with_env_key(None, UnusableEnvKey::Reject);
3774
3775 assert_eq!(client.credential, Credential::Absent);
3776 }
3777
3778 #[test]
3779 fn an_environment_key_reaches_the_transport() {
3780 let client = built_with_env_key(Some("from-env"), UnusableEnvKey::Reject);
3781
3782 assert_eq!(client.credential, Credential::Sent);
3783 }
3784
3785 #[test]
3786 fn a_key_that_cannot_be_a_header_fails_the_build() {
3787 assert!(matches!(
3788 StoreClient::builder()
3789 .url("https://example.test")
3790 .api_key("has\nnewline")
3791 .build(),
3792 Err(ClientBuildError::InvalidApiKey)
3793 ));
3794 }
3795
3796 #[test]
3799 fn a_tolerated_unusable_key_yields_a_client_that_explains_itself() {
3800 let client = built_with_env_key(Some("has\nnewline"), UnusableEnvKey::Tolerate);
3801 assert_eq!(client.credential, Credential::Unusable);
3802
3803 let rendered = client_error_from_connect(
3804 ConnectError::new(ErrorCode::Unauthenticated, "HTTP error 401"),
3805 client.credential,
3806 )
3807 .to_string();
3808 assert!(rendered.contains(API_KEY_ENV), "{rendered}");
3809 assert!(rendered.contains("cannot be an HTTP header"), "{rendered}");
3810 }
3811
3812 #[test]
3813 fn client_trims_trailing_slash() {
3814 let client = StoreClient::new("http://localhost:10000/");
3815 assert_eq!(client.health_url, "http://localhost:10000");
3816 }
3817
3818 #[test]
3819 fn create_session_starts_unseeded() {
3820 let client = PrefixedStoreClient::empty(StoreClient::new("http://localhost:10000/"));
3821 let session = client.create_session();
3822 assert_eq!(session.fixed_sequence(), None);
3823 assert_eq!(session.evaluated_sequence(), None);
3824 }
3825
3826 #[test]
3827 fn range_mode_maps_to_proto_traversal() {
3828 assert_eq!(
3829 RangeMode::Forward.to_proto(),
3830 ProtoTraversalMode::TRAVERSAL_MODE_FORWARD
3831 );
3832 assert_eq!(
3833 RangeMode::Reverse.to_proto(),
3834 ProtoTraversalMode::TRAVERSAL_MODE_REVERSE
3835 );
3836 }
3837
3838 #[test]
3839 fn retry_config_standard_defaults_match_expected() {
3840 let config = RetryConfig::standard();
3841 assert_eq!(config.max_attempts, 3);
3842 assert_eq!(config.initial_backoff, Duration::from_millis(100));
3843 assert_eq!(config.max_backoff, Duration::from_millis(2_000));
3844 }
3845
3846 #[test]
3847 fn retry_config_clamps_attempts_and_backoff_bounds() {
3848 let config = RetryConfig::standard()
3849 .with_max_attempts(0)
3850 .with_initial_backoff(Duration::from_millis(250))
3851 .with_max_backoff(Duration::from_millis(50))
3852 .sanitized();
3853 assert_eq!(config.max_attempts, 1);
3854 assert_eq!(config.initial_backoff, Duration::from_millis(250));
3855 assert_eq!(config.max_backoff, Duration::from_millis(250));
3856 }
3857
3858 #[test]
3859 fn retryable_codes_include_connect_transients() {
3860 assert!(is_retryable_error(&ConnectError::aborted("retry")));
3861 assert!(is_retryable_error(&ConnectError::resource_exhausted(
3862 "retry"
3863 )));
3864 assert!(is_retryable_error(&ConnectError::unavailable("retry")));
3865 assert!(is_retryable_error(&ConnectError::internal("retry")));
3866 assert!(!is_retryable_error(&ConnectError::invalid_argument(
3867 "no retry"
3868 )));
3869 }
3870
3871 #[test]
3872 fn retry_backoff_delay_is_exponential_and_capped() {
3873 let config = RetryConfig::standard()
3874 .with_initial_backoff(Duration::from_millis(100))
3875 .with_max_backoff(Duration::from_millis(250));
3876 assert_eq!(retry_backoff_delay(1, config), Duration::from_millis(100));
3877 assert_eq!(retry_backoff_delay(2, config), Duration::from_millis(200));
3878 assert_eq!(retry_backoff_delay(3, config), Duration::from_millis(250));
3879 assert_eq!(retry_backoff_delay(4, config), Duration::from_millis(250));
3880 }
3881
3882 #[test]
3883 fn create_session_with_sequence_pins_explicit_floor() {
3884 let client = PrefixedStoreClient::empty(StoreClient::new("http://localhost:10000/"));
3885 let session = client.create_session_with_sequence(27);
3886 assert_eq!(session.fixed_sequence(), Some(27));
3887 assert_eq!(session.evaluated_sequence(), None);
3888 }
3889
3890 #[test]
3891 fn store_key_prefix_round_trips_logical_keys() {
3892 let prefix = StoreKeyPrefix::new(vec![0x0A]).unwrap();
3893 let logical = Bytes::from_static(b"hello");
3894 let physical = prefix.encode_key(&logical).unwrap();
3895 assert!(prefix.matches(&physical));
3896 assert_eq!(prefix.decode_key(&physical).unwrap(), logical);
3897 }
3898
3899 #[test]
3900 fn store_key_prefix_next_key_respects_namespace_capacity() {
3901 for prefix in [
3902 StoreKeyPrefix::identity(),
3903 StoreKeyPrefix::new(Bytes::from_static(b"tenant/")).unwrap(),
3904 ] {
3905 assert_eq!(
3906 prefix.next_key(&Bytes::from_static(b"row")).unwrap(),
3907 Some(Bytes::from_static(b"row\0")),
3908 );
3909 let capacity = prefix.max_logical_key_len();
3910 let mut key = vec![0; capacity];
3911 key[capacity - 2] = 0x12;
3912 key[capacity - 1] = 0xFF;
3913 let next = prefix.next_key(&Bytes::from(key)).unwrap().unwrap();
3914 assert_eq!(next.len(), capacity - 1);
3915 assert_eq!(next[capacity - 2], 0x13);
3916 assert!(prefix.encode_key(&next).is_ok());
3917 assert_eq!(
3918 prefix.next_key(&Bytes::from(vec![0xFF; capacity])).unwrap(),
3919 None,
3920 );
3921 assert!(prefix
3922 .next_key(&Bytes::from(vec![0; capacity + 1]))
3923 .is_err());
3924 }
3925 let prefix = StoreKeyPrefix::new(vec![0; MAX_KEY_LEN]).unwrap();
3926 assert_eq!(prefix.next_key(&Bytes::new()).unwrap(), None);
3927 }
3928
3929 #[test]
3930 fn identity_prefix_encode_decode_are_zero_copy() {
3931 let prefix = StoreKeyPrefix::identity();
3932 let logical = Bytes::from_static(b"passthrough-key");
3933 let physical = prefix.encode_key(&logical).unwrap();
3934 assert_eq!(physical.as_ptr(), logical.as_ptr());
3936 let decoded = prefix.decode_key(&physical).unwrap();
3937 assert_eq!(decoded.as_ptr(), logical.as_ptr());
3938 assert_eq!(decoded, logical);
3939 }
3940
3941 #[test]
3942 fn uniform_width_prefixes_are_pairwise_disjoint() {
3943 let all = [
3945 StoreKeyPrefix::new(vec![0]).unwrap(),
3946 StoreKeyPrefix::new(vec![1]).unwrap(),
3947 StoreKeyPrefix::new(vec![2]).unwrap(),
3948 StoreKeyPrefix::new(vec![3]).unwrap(),
3949 StoreKeyPrefix::new(vec![4]).unwrap(),
3950 StoreKeyPrefix::new(vec![5]).unwrap(),
3951 ];
3952 let logical = Bytes::from_static(b"\x00\x10whatever-block-meta-or-op-log-key");
3956 for (i, pa) in all.iter().enumerate() {
3957 let physical = pa.encode_key(&logical).unwrap();
3958 assert!(pa.matches(&physical));
3959 assert_eq!(pa.decode_key(&physical).unwrap(), logical);
3960 for (j, pb) in all.iter().enumerate() {
3961 if i == j {
3962 continue;
3963 }
3964 assert!(
3965 !pb.matches(&physical),
3966 "prefix {j} matched a key encoded under prefix {i}",
3967 );
3968 }
3969 }
3970 }
3971
3972 #[test]
3973 fn prefixed_store_client_always_carries_its_prefix() {
3974 let base = StoreClient::new("http://localhost:8090");
3975 let prefix = StoreKeyPrefix::new(vec![0]).unwrap();
3976 let client = base.prefixed(prefix.clone());
3977 assert_eq!(client.key_prefix(), &prefix);
3978 let logical = Bytes::from_static(b"row");
3980 assert_eq!(
3981 client.encode_store_key(&logical).unwrap(),
3982 prefix.encode_key(&logical).unwrap(),
3983 );
3984 }
3985
3986 #[test]
3987 fn store_key_prefix_clamps_long_logical_range_upper_bound() {
3988 let prefix = StoreKeyPrefix::new(vec![0x02]).unwrap();
3989 let logical_start = Bytes::new();
3992 let logical_end = Bytes::from(vec![0xFFu8; MAX_KEY_LEN]);
3993 assert_eq!(logical_end.len(), MAX_KEY_LEN);
3994
3995 let (physical_start, physical_end) =
3996 prefix.encode_range(&logical_start, &logical_end).unwrap();
3997 assert!(prefix.matches(&physical_start));
3998 assert!(prefix.matches(&physical_end));
3999 assert_eq!(prefix.max_logical_key_len(), MAX_KEY_LEN - 1);
4002 assert_eq!(physical_end.len(), MAX_KEY_LEN);
4003 assert_eq!(prefix.decode_key(&physical_start).unwrap(), logical_start);
4004 }
4005
4006 #[test]
4007 fn store_key_prefix_rewrites_selector_family() {
4008 let prefix = StoreKeyPrefix::new(vec![0x05]).unwrap();
4009 let logical = crate::selector::Selector {
4010 prefix: Bytes::copy_from_slice(&[0x06]),
4011 payload_regex: crate::kv_codec::Utf8::from("(?s).*"),
4012 };
4013 let physical = prefix.prefix_selector(&logical).unwrap();
4014 assert_eq!(physical.prefix.as_ref(), &[0x05, 0x06]);
4015 assert_eq!(physical.payload_regex, logical.payload_regex);
4016 }
4017
4018 #[test]
4019 fn store_key_prefix_composed_selector_reconstructs_and_strips_cleanly() {
4020 let prefix = StoreKeyPrefix::new(vec![0x05, 0x06]).unwrap();
4031 let logical = crate::selector::Selector {
4032 prefix: Bytes::copy_from_slice(&[0x07]),
4033 payload_regex: crate::kv_codec::Utf8::from("(?s).*"),
4034 };
4035
4036 let merged = prefix.prefix_selector(&logical).unwrap();
4039 assert_eq!(merged.prefix.as_ref(), &[0x05, 0x06, 0x07]);
4040
4041 let codec = Prefix::new(merged.prefix.clone()).unwrap();
4043
4044 let payload = [0xAA, 0xBB, 0xCC];
4046 let key = codec.encode(&payload).unwrap();
4047 assert_eq!(key.as_ref(), &[0x05, 0x06, 0x07, 0xAA, 0xBB, 0xCC]);
4048 assert!(codec.matches(&key));
4049 assert_eq!(
4052 codec.strip(&key).unwrap(),
4053 Bytes::copy_from_slice(&[0xAA, 0xBB, 0xCC])
4054 );
4055
4056 let (start, end) = codec.bounds();
4059 assert!(start <= key && key <= end);
4060
4061 let sibling = Prefix::new(vec![0x05, 0x06, 0x08])
4063 .unwrap()
4064 .encode(&[0x11])
4065 .unwrap();
4066 assert!(!codec.matches(&sibling));
4067 }
4068
4069 #[test]
4070 fn store_key_prefix_stream_filter_passes_payload_regex_through() {
4071 let client = StoreClient::builder()
4075 .url("http://localhost:10000")
4076 .build()
4077 .unwrap()
4078 .prefixed(StoreKeyPrefix::new(vec![0x05]).unwrap());
4079 let filter = crate::stream_filter::StreamFilter {
4080 selectors: vec![crate::selector::Selector {
4081 prefix: Bytes::copy_from_slice(&[0x06]),
4082 payload_regex: crate::kv_codec::Utf8::from("(?s)foo.*"),
4083 }],
4084 value_filters: vec![],
4085 };
4086 let physical = client.prefix_stream_filter(filter.clone()).unwrap();
4087 assert_eq!(physical.selectors[0].prefix.as_ref(), &[0x05, 0x06]);
4088 assert_eq!(
4089 physical.selectors[0].payload_regex,
4090 filter.selectors[0].payload_regex
4091 );
4092 }
4093
4094 #[test]
4095 fn prefixed_reduce_request_shifts_nested_cast_fields() {
4096 let client = StoreClient::builder()
4097 .url("http://localhost:10000")
4098 .build()
4099 .unwrap()
4100 .prefixed(StoreKeyPrefix::new(vec![1, 2, 3]).unwrap());
4101 let request_at = |byte_offset, bit_offset| DomainRangeReduceRequest {
4102 reducers: vec![crate::RangeReducerSpec {
4103 op: crate::RangeReduceOp::SumField,
4104 expr: Some(KvExpr::CastFloat64(Box::new(KvExpr::Add(
4105 Box::new(KvExpr::Field(KvFieldRef::Key {
4106 byte_offset,
4107 kind: KvFieldKind::Int64,
4108 })),
4109 Box::new(KvExpr::CastFloat64(Box::new(KvExpr::Field(
4110 KvFieldRef::Value {
4111 index: 2,
4112 kind: KvFieldKind::UInt64,
4113 nullable: true,
4114 },
4115 )))),
4116 )))),
4117 filter: None,
4118 }],
4119 group_by: vec![KvExpr::CastFloat64(Box::new(KvExpr::Field(
4120 KvFieldRef::ZOrderKey {
4121 bit_offset,
4122 field_position: 0,
4123 field_widths: vec![8],
4124 kind: KvFieldKind::UInt64,
4125 },
4126 )))],
4127 filter: None,
4128 };
4129 let request = request_at(9, 12);
4130 assert_eq!(
4131 client.prefix_reduce_request(&request).unwrap(),
4132 request_at(12, 36)
4133 );
4134 assert_eq!(request, request_at(9, 12));
4135 }
4136
4137 #[test]
4138 fn prefixed_reduce_request_shifts_key_field_offsets() {
4139 let client = StoreClient::builder()
4140 .url("http://localhost:10000")
4141 .build()
4142 .unwrap()
4143 .prefixed(StoreKeyPrefix::new(vec![0x01, 0x02, 0x03]).unwrap());
4144 let request = DomainRangeReduceRequest {
4145 reducers: vec![crate::RangeReducerSpec {
4146 filter: Some(KvPredicate {
4147 checks: vec![
4148 KvPredicateCheck {
4149 field: KvFieldRef::Key {
4150 byte_offset: 9,
4151 kind: KvFieldKind::UInt64,
4152 },
4153 constraint: KvPredicateConstraint::IsNotNull,
4154 },
4155 KvPredicateCheck {
4156 field: KvFieldRef::ZOrderKey {
4157 bit_offset: 12,
4158 field_position: 0,
4159 field_widths: vec![8],
4160 kind: KvFieldKind::UInt64,
4161 },
4162 constraint: KvPredicateConstraint::IsNotNull,
4163 },
4164 KvPredicateCheck {
4165 field: KvFieldRef::Value {
4166 index: 2,
4167 kind: KvFieldKind::UInt64,
4168 nullable: true,
4169 },
4170 constraint: KvPredicateConstraint::IsNotNull,
4171 },
4172 ],
4173 contradiction: false,
4174 }),
4175 op: crate::RangeReduceOp::SumField,
4176 expr: Some(KvExpr::Field(KvFieldRef::Key {
4177 byte_offset: 9,
4178 kind: KvFieldKind::UInt64,
4179 })),
4180 }],
4181 group_by: vec![KvExpr::Field(KvFieldRef::ZOrderKey {
4182 bit_offset: 12,
4183 field_position: 0,
4184 field_widths: vec![8],
4185 kind: KvFieldKind::UInt64,
4186 })],
4187 filter: Some(KvPredicate {
4188 checks: vec![KvPredicateCheck {
4189 field: KvFieldRef::Value {
4190 index: 0,
4191 kind: KvFieldKind::UInt64,
4192 nullable: false,
4193 },
4194 constraint: KvPredicateConstraint::UInt64Range {
4195 min: Some(1),
4196 max: Some(9),
4197 },
4198 }],
4199 contradiction: false,
4200 }),
4201 };
4202
4203 let shifted = client.prefix_reduce_request(&request).unwrap();
4204 let Some(KvExpr::Field(KvFieldRef::Key { byte_offset, .. })) =
4205 shifted.reducers[0].expr.as_ref()
4206 else {
4207 panic!("expected key field reducer");
4208 };
4209 assert_eq!(*byte_offset, 12);
4212 let KvExpr::Field(KvFieldRef::ZOrderKey { bit_offset, .. }) = &shifted.group_by[0] else {
4213 panic!("expected z-order group field");
4214 };
4215 assert_eq!(*bit_offset, 36);
4218 let checks = &shifted.reducers[0].filter.as_ref().unwrap().checks;
4219 assert_eq!(
4220 checks[0].field,
4221 KvFieldRef::Key {
4222 byte_offset: 12,
4223 kind: KvFieldKind::UInt64,
4224 }
4225 );
4226 assert_eq!(
4227 checks[1].field,
4228 KvFieldRef::ZOrderKey {
4229 bit_offset: 36,
4230 field_position: 0,
4231 field_widths: vec![8],
4232 kind: KvFieldKind::UInt64,
4233 }
4234 );
4235 let original_checks = &request.reducers[0].filter.as_ref().unwrap().checks;
4236 assert_eq!(checks[2], original_checks[2]);
4237 assert_eq!(shifted.filter, request.filter);
4238 assert_eq!(
4239 original_checks[0].field,
4240 KvFieldRef::Key {
4241 byte_offset: 9,
4242 kind: KvFieldKind::UInt64,
4243 }
4244 );
4245 }
4246
4247 #[test]
4248 fn store_write_batch_uses_each_clients_prefix() {
4249 let base = StoreClient::new("http://localhost:10000");
4250 let a = base.prefixed(StoreKeyPrefix::new(vec![1]).unwrap());
4251 let b = base.prefixed(StoreKeyPrefix::new(vec![2]).unwrap());
4252 let key_a = Bytes::from_static(b"a");
4253 let key_b = Bytes::from_static(b"b");
4254
4255 let mut batch = StoreWriteBatch::new();
4256 batch.push(&a, &key_a, b"va").unwrap();
4257 batch.push(&b, &key_b, b"vb").unwrap();
4258
4259 assert_eq!(
4260 batch.entries[0].0,
4261 a.key_prefix().encode_key(&key_a).unwrap()
4262 );
4263 assert_eq!(
4264 batch.entries[1].0,
4265 b.key_prefix().encode_key(&key_b).unwrap()
4266 );
4267 }
4268
4269 #[test]
4270 fn pushed_rows_round_trip_only_through_their_own_client() {
4271 let base = StoreClient::new("http://localhost:10000");
4272 let a = base.prefixed(StoreKeyPrefix::new(vec![1]).unwrap());
4273 let b = base.prefixed(StoreKeyPrefix::new(vec![2]).unwrap());
4274 let key = Bytes::from_static(b"shared-logical-key");
4275
4276 let mut batch = StoreWriteBatch::new();
4277 batch.push(&a, &key, b"va").unwrap();
4278 batch.push(&b, &key, b"vb").unwrap();
4279
4280 let entries = batch.entries();
4283 assert_eq!(entries.len(), 2);
4284 assert_ne!(entries[0].0, entries[1].0);
4285
4286 assert_eq!(a.decode_store_key(&entries[0].0).unwrap(), key);
4289 assert_eq!(b.decode_store_key(&entries[1].0).unwrap(), key);
4290 assert!(a.decode_store_key(&entries[1].0).is_err());
4291 assert!(b.decode_store_key(&entries[0].0).is_err());
4292 }
4293
4294 #[test]
4295 fn identity_prefix_stages_keys_verbatim() {
4296 let base = StoreClient::new("http://localhost:10000");
4297 let plain = PrefixedStoreClient::empty(base);
4298 let key = Bytes::from_static(b"raw-key");
4299
4300 let mut batch = StoreWriteBatch::new();
4301 batch.push(&plain, &key, b"v").unwrap();
4302
4303 assert_eq!(batch.entries()[0].0, key);
4304 }
4305
4306 #[test]
4307 fn push_rejects_keys_exceeding_prefixed_capacity() {
4308 let base = StoreClient::new("http://localhost:10000");
4309 let a = base.prefixed(StoreKeyPrefix::new(vec![1]).unwrap());
4310 let max = a.key_prefix().max_logical_key_len();
4311
4312 let mut batch = StoreWriteBatch::new();
4313 let at_capacity = Key::from(vec![7u8; max]);
4314 batch.push(&a, &at_capacity, b"v").unwrap();
4315
4316 let oversize = Key::from(vec![7u8; max + 1]);
4317 assert!(batch.push(&a, &oversize, b"v").is_err());
4318 assert_eq!(batch.len(), 1);
4319 }
4320
4321 fn hex_encode(data: &[u8]) -> String {
4322 hex::encode(data)
4323 }
4324
4325 fn hex_decode(s: &str) -> Option<Vec<u8>> {
4326 hex::decode(s).ok()
4327 }
4328}