1use std::time::Duration;
40
41use reddb_wire::replication::CausalBookmark;
42use reddb_wire::topology::{Endpoint, ReplicaInfo};
43
44use crate::topology::ClusterMembership;
45
46pub type BookmarkTarget = CausalBookmark;
48
49#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct WriteEndpoint {
53 pub addr: String,
54 pub region: String,
55 pub term: u64,
56}
57
58impl WriteEndpoint {
59 pub fn serves_term(&self, term: u64) -> bool {
63 self.term == term
64 }
65
66 pub fn endpoint(&self) -> Endpoint {
68 Endpoint {
69 addr: self.addr.clone(),
70 region: self.region.clone(),
71 }
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct ReadEndpoint {
79 pub addr: String,
80 pub region: String,
81 pub healthy: bool,
82 pub lag_ms: u32,
84 pub frontier_lsn: u64,
86 pub rebootstrapping: bool,
91 pub bookmark_eligible: bool,
95}
96
97impl ReadEndpoint {
98 fn from_replica(info: &ReplicaInfo, bookmark: BookmarkTarget) -> Self {
99 let eligible =
103 info.healthy && !info.rebootstrapping && info.last_applied_lsn >= bookmark.commit_lsn();
104 Self {
105 addr: info.addr.clone(),
106 region: info.region.clone(),
107 healthy: info.healthy,
108 lag_ms: info.lag_ms,
109 frontier_lsn: info.last_applied_lsn,
110 rebootstrapping: info.rebootstrapping,
111 bookmark_eligible: eligible,
112 }
113 }
114
115 fn endpoint(&self) -> Endpoint {
116 Endpoint {
117 addr: self.addr.clone(),
118 region: self.region.clone(),
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum RouteKind {
126 StrongPrimary,
129 EligibleTarget,
132 CaughtUpTarget,
135 FallbackReplica,
138 FallbackPrimary,
141 BoundedStalenessReplica,
144 LocalReplica,
147}
148
149impl RouteKind {
150 pub fn is_fallback(self) -> bool {
153 matches!(self, Self::FallbackReplica | Self::FallbackPrimary)
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct RouteDecision {
161 pub endpoint: Endpoint,
163 pub kind: RouteKind,
165 pub waited: Duration,
167}
168
169pub trait BookmarkWaiter {
180 fn elapsed(&self) -> Duration;
182 fn poll(&mut self, target_addr: &str) -> u64;
185}
186
187#[derive(Debug, Clone, Default, PartialEq, Eq)]
189pub struct CausalReadOptions {
190 pub preferred_region: Option<String>,
194 pub deadline: Duration,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
201pub enum ReadConsistency {
202 Strong,
205 Causal {
207 bookmark: BookmarkTarget,
208 opts: CausalReadOptions,
209 },
210 BoundedStaleness { max_lag: Duration },
213 Local,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct QueryOptions {
220 pub consistency: ReadConsistency,
221}
222
223impl QueryOptions {
224 pub fn strong() -> Self {
225 Self {
226 consistency: ReadConsistency::Strong,
227 }
228 }
229
230 pub fn causal(bookmark: BookmarkTarget, opts: CausalReadOptions) -> Self {
231 Self {
232 consistency: ReadConsistency::Causal { bookmark, opts },
233 }
234 }
235
236 pub fn bounded_staleness(max_lag: Duration) -> Self {
237 Self {
238 consistency: ReadConsistency::BoundedStaleness { max_lag },
239 }
240 }
241
242 pub fn local() -> Self {
243 Self {
244 consistency: ReadConsistency::Local,
245 }
246 }
247}
248
249impl Default for QueryOptions {
250 fn default() -> Self {
251 Self::local()
252 }
253}
254
255impl CausalReadOptions {
256 pub fn with_deadline(deadline: Duration) -> Self {
258 Self {
259 preferred_region: None,
260 deadline,
261 }
262 }
263
264 pub fn prefer_region(mut self, region: impl Into<String>) -> Self {
266 self.preferred_region = Some(region.into());
267 self
268 }
269}
270
271#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct RoutingTable {
275 write: WriteEndpoint,
276 replicas: Vec<ReplicaInfo>,
277 epoch: u64,
278}
279
280impl RoutingTable {
281 pub fn from_membership(membership: ClusterMembership, term: u64) -> Self {
285 let ClusterMembership {
286 primary,
287 replicas,
288 epoch,
289 } = membership;
290 Self {
291 write: WriteEndpoint {
292 addr: primary.addr,
293 region: primary.region,
294 term,
295 },
296 replicas,
297 epoch,
298 }
299 }
300
301 pub fn epoch(&self) -> u64 {
303 self.epoch
304 }
305
306 pub fn write_endpoint(&self) -> &WriteEndpoint {
308 &self.write
309 }
310
311 pub fn read_endpoints(&self, bookmark: BookmarkTarget) -> Vec<ReadEndpoint> {
315 self.replicas
316 .iter()
317 .map(|r| ReadEndpoint::from_replica(r, bookmark))
318 .collect()
319 }
320
321 fn pick_target_index(&self, preferred_region: Option<&str>) -> Option<usize> {
332 if let Some(region) = preferred_region {
333 if let Some(i) = self
334 .replicas
335 .iter()
336 .position(|r| r.healthy && !r.rebootstrapping && r.region == region)
337 {
338 return Some(i);
339 }
340 }
341 self.replicas
342 .iter()
343 .position(|r| r.healthy && !r.rebootstrapping)
344 }
345
346 pub fn route_causal_read(
351 &self,
352 bookmark: BookmarkTarget,
353 opts: &CausalReadOptions,
354 waiter: &mut dyn BookmarkWaiter,
355 ) -> RouteDecision {
356 let target_idx = self.pick_target_index(opts.preferred_region.as_deref());
357
358 if let Some(idx) = target_idx {
359 let target = &self.replicas[idx];
360
361 if target.last_applied_lsn >= bookmark.commit_lsn() {
364 return RouteDecision {
365 endpoint: Endpoint {
366 addr: target.addr.clone(),
367 region: target.region.clone(),
368 },
369 kind: RouteKind::EligibleTarget,
370 waited: Duration::ZERO,
371 };
372 }
373
374 let addr = target.addr.clone();
377 let region = target.region.clone();
378 while waiter.elapsed() < opts.deadline {
379 let frontier = waiter.poll(&addr);
380 if frontier >= bookmark.commit_lsn() {
381 return RouteDecision {
382 endpoint: Endpoint { addr, region },
383 kind: RouteKind::CaughtUpTarget,
384 waited: waiter.elapsed(),
385 };
386 }
387 }
388
389 return self.fall_back(bookmark, Some(idx), waiter.elapsed());
391 }
392
393 self.fall_back(bookmark, None, waiter.elapsed())
395 }
396
397 pub fn route_read(
402 &self,
403 options: &QueryOptions,
404 waiter: &mut dyn BookmarkWaiter,
405 ) -> RouteDecision {
406 match &options.consistency {
407 ReadConsistency::Strong => RouteDecision {
408 endpoint: self.write.endpoint(),
409 kind: RouteKind::StrongPrimary,
410 waited: Duration::ZERO,
411 },
412 ReadConsistency::Causal { bookmark, opts } => {
413 self.route_causal_read(*bookmark, opts, waiter)
414 }
415 ReadConsistency::BoundedStaleness { max_lag } => self.route_bounded_staleness(*max_lag),
416 ReadConsistency::Local => self.route_local(),
417 }
418 }
419
420 fn route_bounded_staleness(&self, max_lag: Duration) -> RouteDecision {
421 let max_lag_ms = max_lag.as_millis().min(u32::MAX as u128) as u32;
422 match self
423 .replicas
424 .iter()
425 .find(|r| r.healthy && !r.rebootstrapping && r.lag_ms <= max_lag_ms)
426 {
427 Some(r) => RouteDecision {
428 endpoint: Endpoint {
429 addr: r.addr.clone(),
430 region: r.region.clone(),
431 },
432 kind: RouteKind::BoundedStalenessReplica,
433 waited: Duration::ZERO,
434 },
435 None => RouteDecision {
436 endpoint: self.write.endpoint(),
437 kind: RouteKind::FallbackPrimary,
438 waited: Duration::ZERO,
439 },
440 }
441 }
442
443 fn route_local(&self) -> RouteDecision {
444 match self.replicas.iter().find(|r| r.healthy) {
445 Some(r) => RouteDecision {
446 endpoint: Endpoint {
447 addr: r.addr.clone(),
448 region: r.region.clone(),
449 },
450 kind: RouteKind::LocalReplica,
451 waited: Duration::ZERO,
452 },
453 None => RouteDecision {
454 endpoint: self.write.endpoint(),
455 kind: RouteKind::FallbackPrimary,
456 waited: Duration::ZERO,
457 },
458 }
459 }
460
461 fn fall_back(
464 &self,
465 bookmark: BookmarkTarget,
466 exclude: Option<usize>,
467 waited: Duration,
468 ) -> RouteDecision {
469 let caught_up = self.replicas.iter().enumerate().find(|(i, r)| {
470 Some(*i) != exclude
471 && r.healthy
472 && !r.rebootstrapping
473 && r.last_applied_lsn >= bookmark.commit_lsn()
474 });
475 match caught_up {
476 Some((_, r)) => RouteDecision {
477 endpoint: ReadEndpoint::from_replica(r, bookmark).endpoint(),
478 kind: RouteKind::FallbackReplica,
479 waited,
480 },
481 None => RouteDecision {
482 endpoint: self.write.endpoint(),
483 kind: RouteKind::FallbackPrimary,
484 waited,
485 },
486 }
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493 use reddb_wire::topology::Endpoint as WireEndpoint;
494
495 fn primary() -> WireEndpoint {
496 WireEndpoint {
497 addr: "primary:5050".into(),
498 region: "us-east-1".into(),
499 }
500 }
501
502 fn replica(addr: &str, region: &str, healthy: bool, frontier: u64) -> ReplicaInfo {
503 ReplicaInfo {
504 addr: addr.into(),
505 region: region.into(),
506 healthy,
507 lag_ms: if healthy { 5 } else { u32::MAX },
508 last_applied_lsn: frontier,
509 rebootstrapping: false,
510 }
511 }
512
513 fn lagging_replica(
514 addr: &str,
515 region: &str,
516 healthy: bool,
517 frontier: u64,
518 lag_ms: u32,
519 ) -> ReplicaInfo {
520 ReplicaInfo {
521 addr: addr.into(),
522 region: region.into(),
523 healthy,
524 lag_ms,
525 last_applied_lsn: frontier,
526 rebootstrapping: false,
527 }
528 }
529
530 fn rebuilding_replica(addr: &str, region: &str, frontier: u64) -> ReplicaInfo {
534 ReplicaInfo {
535 addr: addr.into(),
536 region: region.into(),
537 healthy: true,
538 lag_ms: 5,
539 last_applied_lsn: frontier,
540 rebootstrapping: true,
541 }
542 }
543
544 fn membership(replicas: Vec<ReplicaInfo>) -> ClusterMembership {
545 ClusterMembership {
546 primary: primary(),
547 replicas,
548 epoch: 3,
549 }
550 }
551
552 struct ScriptedWaiter {
557 steps: Vec<u64>,
558 idx: usize,
559 tick: Duration,
560 elapsed: Duration,
561 polled_addrs: Vec<String>,
562 }
563
564 impl ScriptedWaiter {
565 fn new(steps: Vec<u64>, tick: Duration) -> Self {
566 Self {
567 steps,
568 idx: 0,
569 tick,
570 elapsed: Duration::ZERO,
571 polled_addrs: Vec::new(),
572 }
573 }
574 }
575
576 impl BookmarkWaiter for ScriptedWaiter {
577 fn elapsed(&self) -> Duration {
578 self.elapsed
579 }
580 fn poll(&mut self, target_addr: &str) -> u64 {
581 self.polled_addrs.push(target_addr.to_string());
582 self.elapsed += self.tick;
583 let v = self
584 .steps
585 .get(self.idx)
586 .copied()
587 .or_else(|| self.steps.last().copied())
588 .unwrap_or(0);
589 self.idx += 1;
590 v
591 }
592 }
593
594 #[test]
597 fn write_endpoint_is_primary_keyed_by_term() {
598 let table = RoutingTable::from_membership(membership(vec![]), 9);
599 let w = table.write_endpoint();
600 assert_eq!(w.addr, "primary:5050");
601 assert_eq!(w.region, "us-east-1");
602 assert_eq!(w.term, 9);
603 assert!(w.serves_term(9));
604 assert!(!w.serves_term(10));
605 }
606
607 #[test]
610 fn read_endpoints_carry_frontier_and_eligibility() {
611 let table = RoutingTable::from_membership(
612 membership(vec![
613 replica("r-ahead:5050", "us-east-1", true, 200),
614 replica("r-behind:5050", "us-east-1", true, 90),
615 replica("r-down:5050", "us-west-2", false, 500),
616 ]),
617 1,
618 );
619 let bookmark = BookmarkTarget::new(1, 100);
620 let reads = table.read_endpoints(bookmark);
621 assert_eq!(reads.len(), 3);
622
623 assert_eq!(reads[0].frontier_lsn, 200);
625 assert!(reads[0].bookmark_eligible);
626
627 assert_eq!(reads[1].frontier_lsn, 90);
629 assert!(!reads[1].bookmark_eligible);
630
631 assert!(reads[2].frontier_lsn >= 100);
633 assert!(!reads[2].healthy);
634 assert!(!reads[2].bookmark_eligible);
635 }
636
637 #[test]
638 fn eligibility_boundary_is_inclusive_at_commit_lsn() {
639 let table =
640 RoutingTable::from_membership(membership(vec![replica("r:5050", "r1", true, 100)]), 1);
641 let reads = table.read_endpoints(BookmarkTarget::new(1, 100));
643 assert!(reads[0].bookmark_eligible);
644 }
645
646 #[test]
649 fn rebootstrapping_replica_is_never_bookmark_eligible_despite_frontier() {
650 let table = RoutingTable::from_membership(
653 membership(vec![rebuilding_replica("rebuild:5050", "us-east-1", 999)]),
654 1,
655 );
656 let reads = table.read_endpoints(BookmarkTarget::new(1, 100));
657 assert_eq!(reads[0].frontier_lsn, 999);
658 assert!(reads[0].rebootstrapping);
659 assert!(
660 !reads[0].bookmark_eligible,
661 "a rebuilding node must never be bookmark-eligible"
662 );
663 }
664
665 #[test]
666 fn route_skips_rebuilding_node_and_falls_back_to_caught_up_peer() {
667 let table = RoutingTable::from_membership(
671 membership(vec![
672 rebuilding_replica("rebuild:5050", "us-east-1", 999),
673 replica("caught-up:5050", "us-east-1", true, 300),
674 ]),
675 1,
676 );
677 let mut waiter = ScriptedWaiter::new(vec![], Duration::from_millis(10));
678 let decision = table.route_causal_read(
679 BookmarkTarget::new(1, 100),
680 &CausalReadOptions::with_deadline(Duration::from_millis(500)),
681 &mut waiter,
682 );
683 assert_eq!(decision.endpoint.addr, "caught-up:5050");
686 assert!(!decision.kind.is_fallback());
687 assert!(
688 waiter.polled_addrs.iter().all(|a| a != "rebuild:5050"),
689 "must never poll a rebuilding node"
690 );
691 }
692
693 #[test]
694 fn route_falls_back_to_primary_when_every_replica_is_rebuilding() {
695 let table = RoutingTable::from_membership(
698 membership(vec![
699 rebuilding_replica("rebuild-a:5050", "us-east-1", 999),
700 rebuilding_replica("rebuild-b:5050", "us-west-2", 999),
701 ]),
702 4,
703 );
704 let mut waiter = ScriptedWaiter::new(vec![], Duration::from_millis(10));
705 let decision = table.route_causal_read(
706 BookmarkTarget::new(4, 100),
707 &CausalReadOptions::with_deadline(Duration::from_millis(500)),
708 &mut waiter,
709 );
710 assert_eq!(decision.kind, RouteKind::FallbackPrimary);
711 assert_eq!(decision.endpoint.addr, "primary:5050");
712 assert!(
713 waiter.polled_addrs.is_empty(),
714 "no rebuilding node should be polled"
715 );
716 }
717
718 #[test]
719 fn rebuilding_node_excluded_as_fallback_target() {
720 let table = RoutingTable::from_membership(
725 membership(vec![
726 replica("east-lag:5050", "us-east-1", true, 10),
727 rebuilding_replica("west-rebuild:5050", "us-west-2", 999),
728 ]),
729 2,
730 );
731 let mut waiter = ScriptedWaiter::new(vec![10, 20, 30], Duration::from_millis(10));
732 let decision = table.route_causal_read(
733 BookmarkTarget::new(2, 100),
734 &CausalReadOptions::with_deadline(Duration::from_millis(25)).prefer_region("us-east-1"),
735 &mut waiter,
736 );
737 assert_eq!(decision.kind, RouteKind::FallbackPrimary);
738 assert_eq!(decision.endpoint.addr, "primary:5050");
739 }
740
741 #[test]
744 fn route_picks_eligible_target_without_waiting() {
745 let table = RoutingTable::from_membership(
746 membership(vec![replica("r-ok:5050", "us-east-1", true, 150)]),
747 1,
748 );
749 let mut waiter = ScriptedWaiter::new(vec![], Duration::from_millis(10));
750 let decision = table.route_causal_read(
751 BookmarkTarget::new(1, 100),
752 &CausalReadOptions::with_deadline(Duration::from_millis(500)),
753 &mut waiter,
754 );
755 assert_eq!(decision.kind, RouteKind::EligibleTarget);
756 assert_eq!(decision.endpoint.addr, "r-ok:5050");
757 assert_eq!(decision.waited, Duration::ZERO);
758 assert!(waiter.polled_addrs.is_empty(), "must not poll on fast path");
759 }
760
761 #[test]
762 fn route_prefers_region_for_the_target() {
763 let table = RoutingTable::from_membership(
764 membership(vec![
765 replica("east:5050", "us-east-1", true, 150),
766 replica("west:5050", "us-west-2", true, 150),
767 ]),
768 1,
769 );
770 let mut waiter = ScriptedWaiter::new(vec![], Duration::from_millis(10));
771 let decision = table.route_causal_read(
772 BookmarkTarget::new(1, 100),
773 &CausalReadOptions::with_deadline(Duration::from_millis(500))
774 .prefer_region("us-west-2"),
775 &mut waiter,
776 );
777 assert_eq!(decision.endpoint.addr, "west:5050");
778 }
779
780 #[test]
783 fn route_waits_and_routes_to_target_once_it_catches_up() {
784 let table = RoutingTable::from_membership(
785 membership(vec![replica("r-lag:5050", "us-east-1", true, 50)]),
786 1,
787 );
788 let mut waiter = ScriptedWaiter::new(vec![60, 80, 100], Duration::from_millis(10));
791 let decision = table.route_causal_read(
792 BookmarkTarget::new(1, 100),
793 &CausalReadOptions::with_deadline(Duration::from_millis(500)),
794 &mut waiter,
795 );
796 assert_eq!(decision.kind, RouteKind::CaughtUpTarget);
797 assert_eq!(decision.endpoint.addr, "r-lag:5050");
798 assert_eq!(decision.waited, Duration::from_millis(30));
799 assert_eq!(waiter.polled_addrs.len(), 3);
800 }
801
802 #[test]
805 fn route_falls_back_to_caught_up_replica_when_target_stays_behind() {
806 let table = RoutingTable::from_membership(
807 membership(vec![
808 replica("east-lag:5050", "us-east-1", true, 10),
810 replica("west-ok:5050", "us-west-2", true, 300),
812 ]),
813 1,
814 );
815 let mut waiter = ScriptedWaiter::new(vec![10, 20, 30], Duration::from_millis(10));
818 let decision = table.route_causal_read(
819 BookmarkTarget::new(1, 100),
820 &CausalReadOptions::with_deadline(Duration::from_millis(25)).prefer_region("us-east-1"),
821 &mut waiter,
822 );
823 assert_eq!(decision.kind, RouteKind::FallbackReplica);
824 assert_eq!(decision.endpoint.addr, "west-ok:5050");
825 assert!(decision.waited >= Duration::from_millis(25));
826 assert!(waiter.polled_addrs.iter().all(|a| a == "east-lag:5050"));
828 }
829
830 #[test]
833 fn route_falls_back_to_primary_when_no_replica_is_caught_up() {
834 let table = RoutingTable::from_membership(
835 membership(vec![
836 replica("r1:5050", "us-east-1", true, 10),
837 replica("r2:5050", "us-east-1", true, 20),
838 ]),
839 7,
840 );
841 let mut waiter = ScriptedWaiter::new(vec![10, 20], Duration::from_millis(10));
842 let decision = table.route_causal_read(
843 BookmarkTarget::new(7, 100),
844 &CausalReadOptions::with_deadline(Duration::from_millis(15)),
845 &mut waiter,
846 );
847 assert_eq!(decision.kind, RouteKind::FallbackPrimary);
848 assert_eq!(decision.endpoint.addr, "primary:5050");
849 assert!(decision.kind.is_fallback());
850 }
851
852 #[test]
853 fn route_falls_back_to_primary_when_no_replica_is_healthy() {
854 let table = RoutingTable::from_membership(
855 membership(vec![replica("r-down:5050", "us-east-1", false, 500)]),
856 1,
857 );
858 let mut waiter = ScriptedWaiter::new(vec![], Duration::from_millis(10));
859 let decision = table.route_causal_read(
860 BookmarkTarget::new(1, 100),
861 &CausalReadOptions::with_deadline(Duration::from_millis(500)),
862 &mut waiter,
863 );
864 assert_eq!(decision.kind, RouteKind::FallbackPrimary);
866 assert_eq!(decision.endpoint.addr, "primary:5050");
867 assert!(waiter.polled_addrs.is_empty());
868 }
869
870 #[test]
871 fn route_falls_back_to_primary_when_no_replicas_advertised() {
872 let table = RoutingTable::from_membership(membership(vec![]), 1);
873 let mut waiter = ScriptedWaiter::new(vec![], Duration::from_millis(10));
874 let decision = table.route_causal_read(
875 BookmarkTarget::new(1, 100),
876 &CausalReadOptions::with_deadline(Duration::from_millis(500)),
877 &mut waiter,
878 );
879 assert_eq!(decision.kind, RouteKind::FallbackPrimary);
880 assert_eq!(decision.endpoint.addr, "primary:5050");
881 }
882
883 #[test]
886 fn lagging_replica_never_errors_always_resolves_an_endpoint() {
887 let shapes = vec![
890 vec![],
891 vec![replica("a:5050", "r1", true, 0)],
892 vec![replica("a:5050", "r1", false, 0)],
893 vec![
894 replica("a:5050", "r1", true, 1),
895 replica("b:5050", "r2", true, 999),
896 ],
897 ];
898 for shape in shapes {
899 let table = RoutingTable::from_membership(membership(shape), 1);
900 let mut waiter = ScriptedWaiter::new(vec![0], Duration::from_millis(10));
901 let decision = table.route_causal_read(
902 BookmarkTarget::new(1, 100),
903 &CausalReadOptions::with_deadline(Duration::from_millis(20)),
904 &mut waiter,
905 );
906 assert!(!decision.endpoint.addr.is_empty());
907 }
908 }
909
910 #[test]
911 fn route_read_strong_uses_primary_without_polling() {
912 let table = RoutingTable::from_membership(
913 membership(vec![replica("r-ok:5050", "us-east-1", true, 150)]),
914 1,
915 );
916 let mut waiter = ScriptedWaiter::new(vec![150], Duration::from_millis(10));
917 let decision = table.route_read(&QueryOptions::strong(), &mut waiter);
918 assert_eq!(decision.kind, RouteKind::StrongPrimary);
919 assert_eq!(decision.endpoint.addr, "primary:5050");
920 assert!(waiter.polled_addrs.is_empty());
921 }
922
923 #[test]
924 fn route_read_bounded_staleness_respects_declared_lag_bound() {
925 let table = RoutingTable::from_membership(
926 membership(vec![
927 lagging_replica("too-stale:5050", "us-east-1", true, 500, 250),
928 lagging_replica("fresh-enough:5050", "us-east-1", true, 90, 40),
929 ]),
930 1,
931 );
932 let mut waiter = ScriptedWaiter::new(vec![], Duration::from_millis(10));
933 let decision = table.route_read(
934 &QueryOptions::bounded_staleness(Duration::from_millis(50)),
935 &mut waiter,
936 );
937 assert_eq!(decision.kind, RouteKind::BoundedStalenessReplica);
938 assert_eq!(decision.endpoint.addr, "fresh-enough:5050");
939 assert!(waiter.polled_addrs.is_empty());
940
941 let fallback = table.route_read(
942 &QueryOptions::bounded_staleness(Duration::from_millis(10)),
943 &mut waiter,
944 );
945 assert_eq!(fallback.kind, RouteKind::FallbackPrimary);
946 assert_eq!(fallback.endpoint.addr, "primary:5050");
947 }
948
949 #[test]
950 fn route_read_local_never_blocks_on_freshness() {
951 let table = RoutingTable::from_membership(
952 membership(vec![lagging_replica(
953 "very-stale:5050",
954 "us-east-1",
955 true,
956 0,
957 u32::MAX,
958 )]),
959 1,
960 );
961 let mut waiter = ScriptedWaiter::new(vec![0, 0, 0], Duration::from_millis(10));
962 let decision = table.route_read(&QueryOptions::local(), &mut waiter);
963 assert_eq!(decision.kind, RouteKind::LocalReplica);
964 assert_eq!(decision.endpoint.addr, "very-stale:5050");
965 assert_eq!(decision.waited, Duration::ZERO);
966 assert!(waiter.polled_addrs.is_empty());
967 }
968}