1#![doc = include_str!("../README.md")]
25#![cfg_attr(not(test), warn(clippy::unwrap_used, clippy::expect_used))]
28
29mod attempt;
30mod budget;
31mod channel_pool;
32mod driver;
33mod driver_supervisor;
34mod error;
35mod leader_hint;
36mod response;
37mod retry;
38mod retry_policy;
39mod seq_attempt;
40mod transport;
41mod worklist;
42
43#[cfg(test)]
44mod test_support;
45
46pub use error::ClientError;
47pub use retry_policy::RetryPolicy;
48pub use seq_attempt::SeqBlock;
49pub use transport::BoxError;
50
51use std::sync::Arc;
52use std::time::Duration;
53use tsoracle_core::{Epoch, LOGICAL_MAX, Timestamp};
54
55pub(crate) const MAX_TIMESTAMPS_PER_RPC: u32 = LOGICAL_MAX + 1;
60
61use crate::channel_pool::ChannelPool;
62
63pub struct ClientBuilder {
64 endpoints: Vec<String>,
65 flush_interval: Duration,
66 connector: Option<Arc<crate::transport::ChannelConnector>>,
67 tls_required: bool,
68 retry_policy: RetryPolicy,
69}
70
71impl ClientBuilder {
72 pub fn endpoints(endpoints: Vec<String>) -> Self {
73 ClientBuilder {
74 endpoints,
75 flush_interval: Duration::from_millis(1),
76 connector: None,
77 tls_required: false,
78 retry_policy: RetryPolicy::default(),
79 }
80 }
81
82 pub fn batch_flush_interval(mut self, flush_interval: Duration) -> Self {
83 self.flush_interval = flush_interval;
84 self
85 }
86
87 pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
99 self.retry_policy = policy;
100 self
101 }
102
103 #[cfg(any(feature = "tls-rustls", feature = "tls-native"))]
119 pub fn tls_config(mut self, cfg: tonic::transport::ClientTlsConfig) -> Self {
120 self.connector = Some(crate::transport::tls_connector(
121 cfg,
122 self.retry_policy.clone(),
123 ));
124 self.tls_required = true;
125 self
126 }
127
128 pub fn channel_connector<F, Fut>(mut self, connector: F) -> Self
139 where
140 F: Fn(&str) -> Fut + Send + Sync + 'static,
141 Fut: std::future::Future<Output = Result<tonic::transport::Channel, crate::BoxError>>
142 + Send
143 + 'static,
144 {
145 let wrapped: Arc<crate::transport::ChannelConnector> = Arc::new(move |endpoint: &str| {
146 let fut = connector(endpoint);
147 Box::pin(async move { fut.await.map_err(ClientError::Connector) })
148 });
149 self.connector = Some(wrapped);
150 self.tls_required = false;
151 self
152 }
153
154 pub async fn build(self) -> Result<Client, ClientError> {
155 if self.endpoints.is_empty() {
156 return Err(ClientError::NoReachableEndpoints);
157 }
158 let pool = Arc::new(ChannelPool::new(
159 self.endpoints,
160 self.connector,
161 self.tls_required,
162 self.retry_policy,
163 ));
164 let pool_for_rpc = pool.clone();
165 let driver = driver::Driver::spawn(
166 move |count| {
167 let pool = pool_for_rpc.clone();
168 Box::pin(async move { retry::issue_rpc(&pool, count).await })
169 },
170 self.flush_interval,
171 );
172 Ok(Client { pool, driver })
173 }
174}
175
176pub struct Client {
177 pool: Arc<ChannelPool>,
178 driver: driver::Driver,
179}
180
181impl Client {
182 pub async fn connect(endpoints: Vec<String>) -> Result<Self, ClientError> {
183 ClientBuilder::endpoints(endpoints).build().await
184 }
185
186 pub fn cached_leader(&self) -> Option<String> {
196 self.pool.cached_leader()
197 }
198
199 pub async fn get_ts(&self) -> Result<Timestamp, ClientError> {
200 Ok(self.driver.request(1).await?[0])
201 }
202
203 pub async fn get_ts_batch(&self, count: u32) -> Result<Vec<Timestamp>, ClientError> {
204 if count == 0 || count > MAX_TIMESTAMPS_PER_RPC {
205 return Err(ClientError::InvalidCount(count));
206 }
207 self.driver.request(count).await
208 }
209
210 pub async fn get_seq(&self, key: &str, count: u32) -> Result<SeqBlock, ClientError> {
231 if key.is_empty() || key.len() > tsoracle_core::MAX_SEQ_KEY_LEN {
232 return Err(ClientError::InvalidSeqKey);
233 }
234 if count == 0 {
235 return Err(ClientError::InvalidCount(0));
236 }
237 retry::issue_seq_rpc(&self.pool, key, count).await
238 }
239
240 pub async fn get_current_max_safe(&self) -> Result<MaxSafe, ClientError> {
256 let endpoint = self
257 .pool
258 .cached_leader()
259 .or_else(|| self.pool.iter_round_robin().into_iter().next())
260 .ok_or(ClientError::NoReachableEndpoints)?;
261 let budget = self.pool.retry_policy().per_attempt_deadline;
262 let pair = crate::budget::PairBudget::start(budget);
266 let (mut svc, cell) =
267 match tokio::time::timeout(budget, self.pool.client_with_cell(&endpoint)).await {
268 Ok(Ok(leased)) => leased,
269 Ok(Err(err)) => return Err(err),
272 Err(_) => {
273 return Err(ClientError::Rpc(tonic::Status::deadline_exceeded(format!(
274 "connect exceeded per_attempt_deadline of {budget:?}"
275 ))));
276 }
277 };
278 let rpc_budget = pair.remaining();
279 let rpc = svc.get_current_max_safe(tsoracle_proto::v1::GetCurrentMaxSafeRequest {});
280 let err = match tokio::time::timeout(rpc_budget, rpc).await {
281 Ok(Ok(response)) => {
282 let inner = response.into_inner();
283 return Ok(MaxSafe {
284 max_safe_physical_ms: inner.max_safe_physical_ms,
285 epoch: Epoch::from_wire(inner.epoch_hi, inner.epoch_lo),
286 });
287 }
288 Ok(Err(status)) => ClientError::Rpc(status),
289 Err(_) => ClientError::Rpc(tonic::Status::deadline_exceeded(format!(
293 "rpc exceeded its share of per_attempt_deadline \
294 ({rpc_budget:?} of {budget:?})"
295 ))),
296 };
297 if crate::retry_policy::is_transport_failure(&err) {
298 self.pool.evict_if_current(&endpoint, &cell);
299 }
300 Err(err)
301 }
302}
303
304#[derive(Copy, Clone, Debug, PartialEq, Eq)]
307pub struct MaxSafe {
308 pub max_safe_physical_ms: u64,
311 pub epoch: Epoch,
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[tokio::test]
320 async fn cached_leader_is_none_before_any_rpc() {
321 let client = Client::connect(vec!["http://127.0.0.1:1".into()])
327 .await
328 .expect("build with a non-empty endpoint list must succeed");
329 assert_eq!(client.cached_leader(), None);
330 }
331
332 #[tokio::test]
333 async fn build_rejects_empty_endpoint_list() {
334 match ClientBuilder::endpoints(Vec::new()).build().await {
338 Err(ClientError::NoReachableEndpoints) => {}
339 Err(other) => panic!("expected NoReachableEndpoints, got {other:?}"),
340 Ok(_) => panic!("expected Err, got Ok(Client)"),
341 }
342 }
343
344 #[tokio::test]
345 async fn channel_connector_error_surfaces_as_connector_variant() {
346 let builder = ClientBuilder::endpoints(vec!["a:1".into()]).channel_connector(
347 |_endpoint: &str| async move {
348 Err::<tonic::transport::Channel, crate::BoxError>(
349 std::io::Error::other("boom").into(),
350 )
351 },
352 );
353 let client = builder.build().await.expect("build must not fail");
354 let result = client.get_ts().await;
355 match result {
356 Err(ClientError::Connector(inner)) => {
357 assert!(inner.to_string().contains("boom"));
358 }
359 other => panic!("expected ClientError::Connector, got {other:?}"),
360 }
361 }
362
363 #[cfg(any(feature = "tls-rustls", feature = "tls-native"))]
368 async fn marker_connector_failure() -> Result<tonic::transport::Channel, crate::BoxError> {
369 Err("MARKER".into())
370 }
371
372 #[cfg(any(feature = "tls-rustls", feature = "tls-native"))]
373 #[tokio::test]
374 async fn tls_config_then_channel_connector_last_wins() {
375 let builder = ClientBuilder::endpoints(vec!["a:1".into()])
379 .tls_config(tonic::transport::ClientTlsConfig::new())
380 .channel_connector(|_endpoint: &str| marker_connector_failure());
381 let client = builder.build().await.expect("build must not fail");
382 match client.get_ts().await {
383 Err(ClientError::Connector(inner)) => {
384 assert!(inner.to_string().contains("MARKER"));
385 }
386 other => panic!("expected Connector(MARKER), got {other:?}"),
387 }
388 }
389
390 #[cfg(any(feature = "tls-rustls", feature = "tls-native"))]
391 #[tokio::test]
392 async fn channel_connector_then_tls_config_last_wins() {
393 let builder = ClientBuilder::endpoints(vec!["a:1".into()])
397 .channel_connector(|_endpoint: &str| marker_connector_failure())
398 .tls_config(tonic::transport::ClientTlsConfig::new());
399 let client = builder.build().await.expect("build must not fail");
400 let result = client.get_ts().await;
401 if let Err(ClientError::Connector(inner)) = &result
402 && inner.to_string().contains("MARKER")
403 {
404 panic!("tls_config set last must overwrite the prior channel_connector");
405 }
406 }
407
408 #[tokio::test]
409 async fn batch_flush_interval_overrides_default() {
410 let custom = Duration::from_millis(25);
415 let builder = ClientBuilder::endpoints(vec!["http://127.0.0.1:1".into()])
416 .batch_flush_interval(custom);
417 assert_eq!(builder.flush_interval, custom);
418 }
419
420 #[tokio::test]
421 async fn retry_policy_override_propagates_to_builder() {
422 let policy = RetryPolicy {
427 max_attempts: 7,
428 per_attempt_deadline: Duration::from_millis(11),
429 overall_deadline: Duration::from_millis(13),
430 base_backoff: Duration::from_millis(17),
431 leader_ttl: Duration::from_millis(19),
432 };
433 let builder = ClientBuilder::endpoints(vec!["http://127.0.0.1:1".into()])
434 .retry_policy(policy.clone());
435 assert_eq!(builder.retry_policy.max_attempts, policy.max_attempts);
436 assert_eq!(
437 builder.retry_policy.per_attempt_deadline,
438 policy.per_attempt_deadline
439 );
440 assert_eq!(
441 builder.retry_policy.overall_deadline,
442 policy.overall_deadline
443 );
444 assert_eq!(builder.retry_policy.base_backoff, policy.base_backoff);
445 assert_eq!(builder.retry_policy.leader_ttl, policy.leader_ttl);
446 }
447
448 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
461 async fn get_current_max_safe_returns_within_per_attempt_deadline_when_connector_hangs() {
462 let policy = RetryPolicy {
463 max_attempts: 1,
464 per_attempt_deadline: Duration::from_millis(100),
465 overall_deadline: Duration::from_millis(300),
466 base_backoff: Duration::ZERO,
467 leader_ttl: Duration::from_secs(30),
468 };
469 let client = ClientBuilder::endpoints(vec!["hang:1".into()])
470 .channel_connector(|_endpoint: &str| async {
471 std::future::pending::<Result<tonic::transport::Channel, crate::BoxError>>().await
477 })
478 .retry_policy(policy)
479 .build()
480 .await
481 .expect("builder must accept the policy");
482 let outer_safety = Duration::from_secs(5);
489 let start = std::time::Instant::now();
490 let result = match tokio::time::timeout(outer_safety, client.get_current_max_safe()).await {
491 Ok(r) => r,
492 Err(_) => panic!(
493 "get_current_max_safe failed to honor its own per_attempt_deadline; \
494 the {outer_safety:?} outer safety net had to fire — this is the \
495 security finding's exact symptom (channel acquisition or RPC was \
496 never bounded)",
497 ),
498 };
499 let elapsed = start.elapsed();
500 assert!(
501 result.is_err(),
502 "a hanging connector must surface as Err, got {result:?}",
503 );
504 assert!(
505 elapsed < Duration::from_secs(2),
506 "deadline must short-circuit; took {elapsed:?} (per_attempt_deadline was 100ms)",
507 );
508 }
509
510 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
511 async fn get_ts_returns_within_overall_deadline_when_all_endpoints_unreachable() {
512 let policy = RetryPolicy {
519 max_attempts: 3,
520 per_attempt_deadline: Duration::from_millis(100),
521 overall_deadline: Duration::from_millis(300),
522 base_backoff: Duration::ZERO,
523 leader_ttl: Duration::from_secs(30),
524 };
525 let client = ClientBuilder::endpoints(vec![
526 "http://127.0.0.1:1".into(),
527 "http://127.0.0.1:2".into(),
528 "http://127.0.0.1:3".into(),
529 ])
530 .retry_policy(policy)
531 .build()
532 .await
533 .expect("builder must accept the policy");
534 let start = std::time::Instant::now();
535 let result = client.get_ts().await;
536 let elapsed = start.elapsed();
537 assert!(result.is_err(), "no listener can reply: {result:?}");
538 assert!(
539 elapsed < Duration::from_secs(2),
540 "deadline must short-circuit; took {elapsed:?}"
541 );
542 }
543
544 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
550 async fn get_seq_returns_gapless_blocks_and_rejects_invalid_inputs() {
551 use std::sync::Arc;
552 use std::sync::atomic::{AtomicU64, Ordering};
553 const EPOCH: u128 = 42;
554
555 let counter = Arc::new(AtomicU64::new(0));
556 let server_counter = counter.clone();
557 let addr = crate::test_support::FakeTso::new()
560 .on_get_seq(move |req| {
561 let counter = server_counter.clone();
562 async move {
563 if req.key.is_empty() {
564 return Err(tonic::Status::invalid_argument("invalid sequence key"));
565 }
566 if req.count == 0 {
567 return Err(tonic::Status::invalid_argument(
568 "count must be between 1 and the maximum",
569 ));
570 }
571 let start = counter.fetch_add(u64::from(req.count), Ordering::SeqCst);
572 let (hi, lo) = tsoracle_core::Epoch(EPOCH).to_wire();
573 Ok(tsoracle_proto::v1::GetSeqResponse {
574 key: req.key,
575 start,
576 count: req.count,
577 epoch: Some(tsoracle_proto::v1::EpochWire { hi, lo }),
578 })
579 }
580 })
581 .spawn()
582 .await;
583
584 let endpoint = format!("http://{addr}");
585 let client = ClientBuilder::endpoints(vec![endpoint])
586 .retry_policy(RetryPolicy {
587 max_attempts: 3,
588 per_attempt_deadline: Duration::from_secs(2),
589 overall_deadline: Duration::from_secs(5),
590 base_backoff: Duration::ZERO,
591 leader_ttl: Duration::from_secs(30),
592 })
593 .build()
594 .await
595 .expect("client must build");
596
597 let block1 = client
599 .get_seq("orders", 5)
600 .await
601 .expect("first get_seq must succeed");
602 assert_eq!(block1.start, 0, "first block must start at 0");
603 assert_eq!(block1.count, 5, "first block must have count 5");
604 assert_eq!(block1.epoch, EPOCH, "epoch must match the server's epoch");
605
606 let block2 = client
608 .get_seq("orders", 3)
609 .await
610 .expect("second get_seq must succeed");
611 assert_eq!(block2.start, 5, "second block must start at 5 (gapless)");
612 assert_eq!(block2.count, 3);
613 assert_eq!(block2.epoch, EPOCH);
614
615 match client.get_seq("", 1).await {
617 Err(ClientError::InvalidSeqKey) => {}
618 other => panic!("empty key must return InvalidSeqKey, got {other:?}"),
619 }
620
621 match client.get_seq("orders", 0).await {
623 Err(ClientError::InvalidCount(0)) => {}
624 other => panic!("count=0 must return InvalidCount, got {other:?}"),
625 }
626 }
627
628 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
634 async fn get_seq_forwards_large_count_to_server() {
635 let addr = crate::test_support::FakeTso::new()
636 .on_get_seq(|req| async move {
637 let (hi, lo) = tsoracle_core::Epoch(1).to_wire();
638 Ok(tsoracle_proto::v1::GetSeqResponse {
639 key: req.key,
640 start: 0,
641 count: req.count,
642 epoch: Some(tsoracle_proto::v1::EpochWire { hi, lo }),
643 })
644 })
645 .spawn()
646 .await;
647
648 let client = ClientBuilder::endpoints(vec![format!("http://{addr}")])
649 .retry_policy(RetryPolicy {
650 max_attempts: 3,
651 per_attempt_deadline: Duration::from_secs(2),
652 overall_deadline: Duration::from_secs(5),
653 base_backoff: Duration::ZERO,
654 leader_ttl: Duration::from_secs(30),
655 })
656 .build()
657 .await
658 .unwrap();
659
660 let big = tsoracle_core::DEFAULT_MAX_SEQ_COUNT + 1;
663 let block = client
664 .get_seq("orders", big)
665 .await
666 .expect("a large count must be forwarded to the server, not locally rejected");
667 assert_eq!(block.count, big);
668 }
669
670 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
679 async fn get_seq_over_cap_count_surfaces_rpc_invalid_argument() {
680 use std::sync::Arc;
681 use std::sync::atomic::{AtomicU64, Ordering};
682 let calls = Arc::new(AtomicU64::new(0));
683 let server_calls = calls.clone();
684 let addr = crate::test_support::FakeTso::new()
688 .on_get_seq(move |_req| {
689 let calls = server_calls.clone();
690 async move {
691 calls.fetch_add(1, Ordering::SeqCst);
692 Err(tonic::Status::invalid_argument(
693 "count must be between 1 and the maximum",
694 ))
695 }
696 })
697 .spawn()
698 .await;
699
700 let client = ClientBuilder::endpoints(vec![format!("http://{addr}")])
701 .retry_policy(RetryPolicy {
702 max_attempts: 3,
703 per_attempt_deadline: Duration::from_secs(2),
704 overall_deadline: Duration::from_secs(5),
705 base_backoff: Duration::ZERO,
706 leader_ttl: Duration::from_secs(30),
707 })
708 .build()
709 .await
710 .unwrap();
711
712 let big = tsoracle_core::DEFAULT_MAX_SEQ_COUNT + 1;
715 match client.get_seq("orders", big).await {
716 Err(ClientError::Rpc(status)) => {
717 assert_eq!(status.code(), tonic::Code::InvalidArgument);
718 assert!(
719 status.message().contains("maximum"),
720 "must surface the server's over-cap message, not a synthetic \
721 or mislabelled error; got {:?}",
722 status.message()
723 );
724 }
725 other => panic!(
726 "an over-cap count with a valid key must surface as \
727 ClientError::Rpc(InvalidArgument), not InvalidSeqKey; got {other:?}"
728 ),
729 }
730 let n = calls.load(Ordering::SeqCst);
732 assert!((1..=3).contains(&n), "expected bounded attempts, got {n}");
733 }
734
735 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
741 async fn get_seq_bare_failed_precondition_surfaces_definitive_error() {
742 use std::sync::Arc;
743 use std::sync::atomic::{AtomicU64, Ordering};
744 let calls = Arc::new(AtomicU64::new(0));
745 let server_calls = calls.clone();
746 let addr = crate::test_support::FakeTso::new()
749 .on_get_seq(move |_req| {
750 let calls = server_calls.clone();
751 async move {
752 calls.fetch_add(1, Ordering::SeqCst);
753 Err(tonic::Status::failed_precondition("dense counter overflow"))
754 }
755 })
756 .spawn()
757 .await;
758
759 let client = ClientBuilder::endpoints(vec![format!("http://{addr}")])
760 .retry_policy(RetryPolicy {
761 max_attempts: 3,
762 per_attempt_deadline: Duration::from_secs(2),
763 overall_deadline: Duration::from_secs(5),
764 base_backoff: Duration::ZERO,
765 leader_ttl: Duration::from_secs(30),
766 })
767 .build()
768 .await
769 .unwrap();
770
771 match client.get_seq("orders", 1).await {
772 Err(ClientError::Rpc(status)) => {
773 assert_eq!(status.code(), tonic::Code::FailedPrecondition);
774 assert!(
775 status.message().contains("overflow"),
776 "must surface the server's overflow message, not a synthetic \
777 'no leader yet'; got {:?}",
778 status.message()
779 );
780 }
781 other => panic!("expected the overflow FailedPrecondition, got {other:?}"),
782 }
783 let n = calls.load(Ordering::SeqCst);
785 assert!((1..=3).contains(&n), "expected bounded attempts, got {n}");
786 }
787
788 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
794 async fn get_seq_ambiguous_post_send_failure_is_uncertain_without_retry() {
795 use std::sync::Arc;
796 use std::sync::atomic::{AtomicU64, Ordering};
797 let calls = Arc::new(AtomicU64::new(0));
798 let server_calls = calls.clone();
799 let addr = crate::test_support::FakeTso::new()
802 .on_get_seq(move |_req| {
803 let calls = server_calls.clone();
804 async move {
805 calls.fetch_add(1, Ordering::SeqCst);
806 Err(tonic::Status::unavailable("connection reset mid-call"))
807 }
808 })
809 .spawn()
810 .await;
811
812 let client = ClientBuilder::endpoints(vec![format!("http://{addr}")])
813 .retry_policy(RetryPolicy {
814 max_attempts: 3,
815 per_attempt_deadline: Duration::from_secs(2),
816 overall_deadline: Duration::from_secs(5),
817 base_backoff: Duration::ZERO,
818 leader_ttl: Duration::from_secs(30),
819 })
820 .build()
821 .await
822 .unwrap();
823
824 match client.get_seq("orders", 1).await {
825 Err(ClientError::SeqUncertain) => {}
826 other => panic!("post-send Unavailable must be SeqUncertain, got {other:?}"),
827 }
828 assert_eq!(
829 calls.load(Ordering::SeqCst),
830 1,
831 "non-idempotent get_seq must NOT retry an ambiguous post-send failure"
832 );
833 }
834
835 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
842 async fn get_seq_post_send_internal_is_uncertain_without_retry() {
843 use std::sync::Arc;
844 use std::sync::atomic::{AtomicU64, Ordering};
845 let calls = Arc::new(AtomicU64::new(0));
846 let server_calls = calls.clone();
847 let addr = crate::test_support::FakeTso::new()
850 .on_get_seq(move |_req| {
851 let calls = server_calls.clone();
852 async move {
853 calls.fetch_add(1, Ordering::SeqCst);
854 Err(tonic::Status::internal("permanent dense driver fault"))
855 }
856 })
857 .spawn()
858 .await;
859
860 let client = ClientBuilder::endpoints(vec![format!("http://{addr}")])
861 .retry_policy(RetryPolicy {
862 max_attempts: 3,
863 per_attempt_deadline: Duration::from_secs(2),
864 overall_deadline: Duration::from_secs(5),
865 base_backoff: Duration::ZERO,
866 leader_ttl: Duration::from_secs(30),
867 })
868 .build()
869 .await
870 .unwrap();
871
872 match client.get_seq("orders", 1).await {
873 Err(ClientError::SeqUncertain) => {}
874 other => panic!("post-send INTERNAL must be SeqUncertain, got {other:?}"),
875 }
876 assert_eq!(
877 calls.load(Ordering::SeqCst),
878 1,
879 "post-send INTERNAL must NOT be retried (possible committed advance)"
880 );
881 }
882
883 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
889 async fn get_seq_response_count_mismatch_is_uncertain() {
890 use std::sync::Arc;
891 use std::sync::atomic::{AtomicU64, Ordering};
892 let calls = Arc::new(AtomicU64::new(0));
893 let server_calls = calls.clone();
894 let addr = crate::test_support::FakeTso::new()
897 .on_get_seq(move |req| {
898 let calls = server_calls.clone();
899 async move {
900 calls.fetch_add(1, Ordering::SeqCst);
901 let (hi, lo) = tsoracle_core::Epoch(7).to_wire();
902 Ok(tsoracle_proto::v1::GetSeqResponse {
903 key: req.key,
904 start: 0,
905 count: req.count + 1,
906 epoch: Some(tsoracle_proto::v1::EpochWire { hi, lo }),
907 })
908 }
909 })
910 .spawn()
911 .await;
912
913 let client = ClientBuilder::endpoints(vec![format!("http://{addr}")])
914 .retry_policy(RetryPolicy {
915 max_attempts: 3,
916 per_attempt_deadline: Duration::from_secs(2),
917 overall_deadline: Duration::from_secs(5),
918 base_backoff: Duration::ZERO,
919 leader_ttl: Duration::from_secs(30),
920 })
921 .build()
922 .await
923 .unwrap();
924
925 match client.get_seq("orders", 5).await {
926 Err(ClientError::SeqUncertain) => {}
927 other => panic!("count-mismatch success must be SeqUncertain, got {other:?}"),
928 }
929 assert_eq!(
930 calls.load(Ordering::SeqCst),
931 1,
932 "a malformed success must not trigger a (double-spending) retry"
933 );
934 }
935
936 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
939 async fn get_seq_response_key_mismatch_is_uncertain() {
940 use std::sync::Arc;
941 use std::sync::atomic::{AtomicU64, Ordering};
942 let calls = Arc::new(AtomicU64::new(0));
943 let server_calls = calls.clone();
944 let addr = crate::test_support::FakeTso::new()
947 .on_get_seq(move |req| {
948 let calls = server_calls.clone();
949 async move {
950 calls.fetch_add(1, Ordering::SeqCst);
951 let (hi, lo) = tsoracle_core::Epoch(7).to_wire();
952 Ok(tsoracle_proto::v1::GetSeqResponse {
953 key: format!("{}-tampered", req.key),
954 start: 0,
955 count: req.count,
956 epoch: Some(tsoracle_proto::v1::EpochWire { hi, lo }),
957 })
958 }
959 })
960 .spawn()
961 .await;
962
963 let client = ClientBuilder::endpoints(vec![format!("http://{addr}")])
964 .retry_policy(RetryPolicy {
965 max_attempts: 3,
966 per_attempt_deadline: Duration::from_secs(2),
967 overall_deadline: Duration::from_secs(5),
968 base_backoff: Duration::ZERO,
969 leader_ttl: Duration::from_secs(30),
970 })
971 .build()
972 .await
973 .unwrap();
974
975 match client.get_seq("orders", 5).await {
976 Err(ClientError::SeqUncertain) => {}
977 other => panic!("key-mismatch success must be SeqUncertain, got {other:?}"),
978 }
979 assert_eq!(
980 calls.load(Ordering::SeqCst),
981 1,
982 "malformed success: one call"
983 );
984 }
985}