Files
fips/src/proto/lookup/tests/core.rs
T
Johnathan Corgan e5586cb333 fix(lookup): count a request of our own that came back apart from a duplicate
Dropping our own flooded request when a bloom false positive circulates
it back to us recorded the drop under `req_duplicate`, whose documented
meaning is that a peer resent a request. The two events are not the same,
and only one of them says anything about the peer that delivered the
frame.

A returning copy has a nonzero floor in healthy operation and rises with
the bloom fill ratio, so folding it into `req_duplicate` puts a permanent
number on a counter an operator reads as neighbour misbehaviour, and
leaves no way to tell a resending peer from this node's own fan-out
coming home. It now carries its own rejection reason and counter,
`req_own_loopback`, shown in fipstop as "Own Loopback", and its own log
line naming the target. The control socket's show routing fixture gains
the field.

Two comments record what the guard rests on. `request.origin` is the
obvious cheaper identity test and is unusable: it is unsigned and set by
whoever sends the frame, so a peer could put this node's address on any
request and make it refuse to transit that request. And the test reaches
only as far as the last MAX_RECORDED_IDS a target's ladder issued, so a
ladder configured with more rungs than that loses its earliest ids.

The request-side regression test now builds its request with this node's
own address as the origin, which is the shape the defect actually has. It
was using a helper whose origin is a third party, so it exercised the
guard without reproducing the case.
2026-08-31 20:26:46 +00:00

780 lines
26 KiB
Rust

//! Tests for the sans-IO lookup decision core.
use super::util::{
MockRoutingView, action_peers, empty_lookup, make_request, make_request_id, suppressing_lookup,
};
use crate::TreeCoordinate;
use crate::proto::lookup::*;
use crate::testutil::make_node_addr;
#[test]
fn picks_only_tree_peers_when_a_tree_peer_matches() {
let tree_peer = make_node_addr(1);
let non_tree_peer = make_node_addr(2);
let rv = MockRoutingView {
peers: vec![(tree_peer, true, true), (non_tree_peer, false, true)],
};
let mut request = make_request(3);
match plan_forward(&mut request, &rv) {
ForwardOutcome::Forward {
actions,
used_fallback,
} => {
assert!(!used_fallback, "tree match must not use fallback");
assert_eq!(action_peers(&actions), vec![tree_peer]);
}
_ => panic!("expected Forward"),
}
}
#[test]
fn falls_back_to_non_tree_peers_when_no_tree_peer_matches() {
let non_tree_a = make_node_addr(3);
let non_tree_b = make_node_addr(4);
// A tree peer exists but does not reach the target.
let tree_no_reach = make_node_addr(5);
let rv = MockRoutingView {
peers: vec![
(tree_no_reach, true, false),
(non_tree_a, false, true),
(non_tree_b, false, true),
],
};
let mut request = make_request(3);
match plan_forward(&mut request, &rv) {
ForwardOutcome::Forward {
actions,
used_fallback,
} => {
assert!(used_fallback, "no tree match must use fallback");
assert_eq!(action_peers(&actions), vec![non_tree_a, non_tree_b]);
}
_ => panic!("expected Forward via fallback"),
}
}
#[test]
fn returns_no_peers_when_nothing_reaches_target() {
let tree_peer = make_node_addr(6);
let non_tree_peer = make_node_addr(7);
let rv = MockRoutingView {
peers: vec![(tree_peer, true, false), (non_tree_peer, false, false)],
};
let mut request = make_request(3);
assert!(matches!(
plan_forward(&mut request, &rv),
ForwardOutcome::NoPeers
));
}
#[test]
fn returns_ttl_exhausted_when_ttl_is_zero() {
let tree_peer = make_node_addr(8);
let rv = MockRoutingView {
peers: vec![(tree_peer, true, true)],
};
let mut request = make_request(0);
assert!(matches!(
plan_forward(&mut request, &rv),
ForwardOutcome::TtlExhausted
));
}
#[test]
fn initiate_picks_only_tree_peers_and_never_falls_back() {
let tree_peer = make_node_addr(1);
let non_tree_peer = make_node_addr(2);
let rv = MockRoutingView {
peers: vec![(tree_peer, true, true), (non_tree_peer, false, true)],
};
let request = make_request(3);
let actions = plan_initiate(&request, &rv);
assert_eq!(action_peers(&actions), vec![tree_peer]);
}
#[test]
fn initiate_returns_empty_when_only_non_tree_peers_reach() {
// A tree peer exists but cannot reach the target; only cross-links reach.
// plan_forward would fall back to the cross-links here; plan_initiate does
// NOT — the tree-only origination gap preserved for behavior-neutrality.
let tree_no_reach = make_node_addr(5);
let non_tree_a = make_node_addr(3);
let non_tree_b = make_node_addr(4);
let rv = MockRoutingView {
peers: vec![
(tree_no_reach, true, false),
(non_tree_a, false, true),
(non_tree_b, false, true),
],
};
let request = make_request(3);
assert!(plan_initiate(&request, &rv).is_empty());
}
#[test]
fn initiate_returns_empty_when_nothing_reaches_target() {
let tree_peer = make_node_addr(6);
let non_tree_peer = make_node_addr(7);
let rv = MockRoutingView {
peers: vec![(tree_peer, true, false), (non_tree_peer, false, false)],
};
let request = make_request(3);
assert!(plan_initiate(&request, &rv).is_empty());
}
#[test]
fn response_route_uses_recorded_reverse_path() {
let mut lookup = empty_lookup();
let from = make_node_addr(9);
lookup
.recent_requests
.insert(42, RecentRequest::new(from, 0));
match plan_response_route(&lookup, 42) {
ResponseRouteDecision::ReversePath(peer) => assert_eq!(peer, from),
ResponseRouteDecision::NeedsTreeRoute => panic!("expected ReversePath"),
}
}
#[test]
fn response_route_needs_tree_route_when_no_record() {
let lookup = empty_lookup();
assert!(matches!(
plan_response_route(&lookup, 42),
ResponseRouteDecision::NeedsTreeRoute
));
}
#[test]
fn classify_response_transit_on_fresh_forwarded_request() {
let from_peer = make_node_addr(0x11);
let mut lookup = empty_lookup();
lookup
.recent_requests
.insert(42, RecentRequest::new(from_peer, 1000));
// With no lookup of ours outstanding for the target, transit is decided by
// the dedup record — pass a target no pending lookup mentions.
match classify_response(&mut lookup, 42, &make_node_addr(0xF1)) {
ResponseRoute::Transit { from_peer: peer } => assert_eq!(peer, from_peer),
_ => panic!("expected Transit"),
}
// The dedup flag must flip after the first transit.
assert!(lookup.recent_requests.get(&42).unwrap().response_forwarded);
}
#[test]
fn classify_response_already_forwarded_on_second_call() {
let from_peer = make_node_addr(0x22);
let mut lookup = empty_lookup();
lookup
.recent_requests
.insert(7, RecentRequest::new(from_peer, 1000));
let target = make_node_addr(0xF2);
assert!(matches!(
classify_response(&mut lookup, 7, &target),
ResponseRoute::Transit { .. }
));
assert!(matches!(
classify_response(&mut lookup, 7, &target),
ResponseRoute::AlreadyForwarded
));
}
#[test]
fn classify_response_originator_wins_over_a_transit_dedup_record() {
// Regression. A copy of a request we originated can be flooded back to us
// and filed in `recent_requests` as ordinary transit, keyed on our own
// request_id. Consulting the dedup record first then classified the answer
// as Transit and relayed it to the peer that looped the request, so the
// pending lookup was never satisfied and discovery failed with "no reply"
// while the responses were in fact arriving. Originator-ness wins.
let target = make_node_addr(0x61);
let looping_peer = make_node_addr(0x62);
let mut lookup = empty_lookup();
lookup
.recent_requests
.insert(4242, RecentRequest::new(looping_peer, 1000));
let mut pending = PendingLookup::new(1000);
pending.record(4242);
lookup.pending_lookups.insert(target, pending);
assert!(matches!(
classify_response(&mut lookup, 4242, &target),
ResponseRoute::Originator
));
// And nothing was reverse-path forwarded on our behalf.
assert!(
!lookup
.recent_requests
.get(&4242)
.unwrap()
.response_forwarded
);
}
#[test]
fn classify_response_originator_when_a_pending_lookup_issued_the_id() {
// No dedup record, so this is not a response we transit. It counts as ours
// only because the target has a lookup outstanding and that lookup issued
// the very request_id the response carries.
let target = make_node_addr(0x51);
let mut lookup = empty_lookup();
let mut pending = PendingLookup::new(1000);
pending.record(999);
lookup.pending_lookups.insert(target, pending);
assert!(matches!(
classify_response(&mut lookup, 999, &target),
ResponseRoute::Originator
));
}
#[test]
fn classify_response_unsolicited_when_nothing_correlates_the_id() {
// The three ways a response can fail to correlate, each of which must be
// dropped before the identity resolve and the signature verify rather than
// being treated as an answer to something we asked for.
let target = make_node_addr(0x52);
let other = make_node_addr(0x53);
let mut lookup = empty_lookup();
// 1. Nothing outstanding at all.
assert!(matches!(
classify_response(&mut lookup, 999, &target),
ResponseRoute::Unsolicited
));
// 2. A lookup is outstanding for the target, but it never issued this id:
// a harvested response cannot be replayed against a live lookup.
let mut pending = PendingLookup::new(1000);
pending.record(1);
lookup.pending_lookups.insert(target, pending);
assert!(matches!(
classify_response(&mut lookup, 999, &target),
ResponseRoute::Unsolicited
));
// 3. The id was issued, but for a different target: the id is bound to the
// exchange it was drawn for and cannot be redirected onto another name.
assert!(matches!(
classify_response(&mut lookup, 1, &other),
ResponseRoute::Unsolicited
));
// The matching pair still classifies as ours, so the checks above are
// discriminating rather than rejecting everything.
assert!(matches!(
classify_response(&mut lookup, 1, &target),
ResponseRoute::Originator
));
}
#[test]
fn on_response_accepted_clears_state_and_emits_effects() {
let target = make_node_addr(0x5A);
let mut lookup = empty_lookup();
// Seed a backoff entry and a pending lookup for the target.
lookup.backoff.record_failure(&target, 1000);
assert!(!lookup.backoff.is_empty(), "precondition: backoff seeded");
lookup
.pending_lookups
.insert(target, PendingLookup::new(1000));
assert!(lookup.pending_lookups.contains_key(&target));
let coords = TreeCoordinate::root(target);
let now_ms = 12_345u64;
let path_mtu = 1400u16;
let actions = on_response_accepted(&mut lookup, 7, &target, coords, now_ms, path_mtu);
// Success state must be cleared.
assert!(
lookup.backoff.is_empty(),
"backoff entry must clear on success"
);
assert!(
!lookup.pending_lookups.contains_key(&target),
"pending lookup must be dropped"
);
// Exactly the four effect actions, in order.
assert_eq!(actions.len(), 4, "expected four effect actions");
match &actions[0] {
LookupAction::CacheCoords {
target: t,
now_ms: n,
path_mtu: p,
..
} => {
assert_eq!(*t, target);
assert_eq!(*n, now_ms);
assert_eq!(*p, path_mtu);
}
_ => panic!("action[0] must be CacheCoords"),
}
match &actions[1] {
LookupAction::WritePathMtu {
target: t,
now_ms: n,
path_mtu: p,
} => {
assert_eq!(*t, target);
assert_eq!(
*n, now_ms,
"the path-MTU entry must be stamped with the same instant the \
coordinates are cached at, or the clamp outlives its route"
);
assert_eq!(*p, path_mtu);
}
_ => panic!("action[1] must be WritePathMtu"),
}
match &actions[2] {
LookupAction::ResetWarmupIfEstablished { target: t } => assert_eq!(*t, target),
_ => panic!("action[2] must be ResetWarmupIfEstablished"),
}
match &actions[3] {
LookupAction::RetryQueuedPackets { target: t } => assert_eq!(*t, target),
_ => panic!("action[3] must be RetryQueuedPackets"),
}
}
#[test]
fn cache_coords_action_carries_the_request_id_the_response_arrived_with() {
let target = make_node_addr(0x5A);
let mut lookup = empty_lookup();
// Distinct from now_ms and path_mtu below, so substituting either for the
// correlator fails rather than coincidentally matching.
let request_id = 0xDEAD_BEEF_0000_0001u64;
let now_ms = 12_345u64;
let path_mtu = 1400u16;
let actions = on_response_accepted(
&mut lookup,
request_id,
&target,
TreeCoordinate::root(target),
now_ms,
path_mtu,
);
match &actions[0] {
LookupAction::CacheCoords { request_id: id, .. } => assert_eq!(
*id, request_id,
"the cache-coords action must carry the correlator of the response it \
was planned from, or the shell's sub-floor path-MTU warning cannot \
name the exchange its sibling warnings do"
),
_ => panic!("action[0] must be CacheCoords"),
}
}
#[test]
fn poll_pending_no_action_before_first_deadline() {
let target = make_node_addr(0x30);
let mut lookup = empty_lookup();
let t0 = 10_000u64;
lookup
.pending_lookups
.insert(target, PendingLookup::new(t0));
// Just before the attempt-1 deadline (1s): nothing fires.
let outcome = poll_pending(&mut lookup, t0 + 999, &[1, 2, 4, 8]);
assert!(outcome.retries.is_empty(), "no retry before deadline");
assert!(outcome.timeouts.is_empty(), "no timeout before deadline");
// Entry unchanged.
let entry = lookup.pending_lookups.get(&target).unwrap();
assert_eq!(entry.attempt, 1);
assert_eq!(entry.last_sent_ms, t0);
}
#[test]
fn poll_pending_retries_at_first_deadline() {
let target = make_node_addr(0x31);
let mut lookup = empty_lookup();
let t0 = 10_000u64;
lookup
.pending_lookups
.insert(target, PendingLookup::new(t0));
// At the attempt-1 deadline (t0 + 1000): one retry to attempt 2.
let outcome = poll_pending(&mut lookup, t0 + 1000, &[1, 2, 4, 8]);
assert_eq!(outcome.retries, vec![(target, 2)]);
assert!(outcome.timeouts.is_empty());
// Entry mutated: attempt bumped, last_sent refreshed.
let entry = lookup.pending_lookups.get(&target).unwrap();
assert_eq!(entry.attempt, 2);
assert_eq!(entry.last_sent_ms, t0 + 1000);
}
#[test]
fn poll_pending_final_timeout_at_max_attempt() {
let target = make_node_addr(0x32);
let mut lookup = empty_lookup();
// Drive the entry to attempt == max (4) with a known last_sent.
let tn = 50_000u64;
let mut entry = PendingLookup::new(tn);
entry.attempt = 4;
entry.last_sent_ms = tn;
lookup.pending_lookups.insert(target, entry);
// attempt_timeouts_secs[3] == 8 → deadline at tn + 8000.
let outcome = poll_pending(&mut lookup, tn + 8000, &[1, 2, 4, 8]);
assert!(outcome.retries.is_empty(), "max attempt cannot retry");
assert_eq!(
outcome.timeouts,
vec![(target, 1)],
"one timeout, failure #1"
);
// Entry removed and a backoff failure recorded.
assert!(
!lookup.pending_lookups.contains_key(&target),
"timed-out entry must be removed"
);
assert_eq!(lookup.backoff.failure_count(&target), 1);
}
// --- classify_request tests ---
#[test]
fn classify_request_forwards_fresh_and_records_it() {
let mut lookup = empty_lookup();
let from = make_node_addr(0x01);
let my_addr = make_node_addr(0x99);
let target = make_node_addr(0xAA);
let request = make_request_id(1, target, 3);
let outcome =
classify_request(&mut lookup, &request, &from, &my_addr, 1000, 5000, 4096, 1).outcome;
assert!(matches!(outcome, RequestOutcome::Forward));
// Recorded for reverse-path forwarding.
assert!(lookup.recent_requests.contains_key(&1));
assert_eq!(lookup.recent_requests.get(&1).unwrap().from_peer, from);
}
#[test]
fn classify_request_duplicate_on_second_call() {
let mut lookup = empty_lookup();
let from = make_node_addr(0x01);
let my_addr = make_node_addr(0x99);
let target = make_node_addr(0xAA);
let request = make_request_id(1, target, 3);
assert!(matches!(
classify_request(&mut lookup, &request, &from, &my_addr, 1000, 5000, 4096, 1).outcome,
RequestOutcome::Forward
));
assert!(matches!(
classify_request(&mut lookup, &request, &from, &my_addr, 1000, 5000, 4096, 1).outcome,
RequestOutcome::Duplicate
));
}
#[test]
fn classify_request_drops_our_own_request_looped_back_to_us() {
// Regression, request side of the same defect. Our flood reaches a peer
// that circulates it back; the only identity test is `target == my_addr`,
// which a lookup we originated never satisfies. Recording it would file
// our own request_id as a transit entry and hand the eventual answer to
// the looping peer, so the copy is dropped as the duplicate it is.
let mut lookup = empty_lookup();
let looping_peer = make_node_addr(0x01);
let my_addr = make_node_addr(0x99);
let target = make_node_addr(0xAA);
let mut pending = PendingLookup::new(1000);
pending.record(77);
lookup.pending_lookups.insert(target, pending);
// Built here rather than through `make_request_id`, whose origin is a
// third party: the defect is our *own* request returning, so the request
// under test has to carry our address as its origin. The guard keys on
// the id and ignores the origin, so this asserts the same behaviour the
// helper would; it asserts it on the shape the defect actually has.
let request = LookupRequest::new(77, target, my_addr, TreeCoordinate::root(my_addr), 3, 0);
let classification = classify_request(
&mut lookup,
&request,
&looping_peer,
&my_addr,
1000,
5000,
4096,
1,
);
assert!(matches!(
classification.outcome,
RequestOutcome::OwnRequestLooped
));
assert!(classification.evicted.is_none());
// Crucially, our own id must not enter the transit dedup cache.
assert!(!lookup.recent_requests.contains_key(&77));
assert!(lookup.recent_by_peer.is_empty());
// A response for it is then ours to accept.
assert!(matches!(
classify_response(&mut lookup, 77, &target),
ResponseRoute::Originator
));
}
#[test]
fn classify_request_still_transits_a_foreign_id_for_a_target_we_are_looking_up() {
// The guard keys on the id, not the target: another node's lookup for the
// same target must still be transited normally while ours is outstanding.
let mut lookup = empty_lookup();
let from = make_node_addr(0x01);
let my_addr = make_node_addr(0x99);
let target = make_node_addr(0xAA);
let mut pending = PendingLookup::new(1000);
pending.record(77);
lookup.pending_lookups.insert(target, pending);
let request = make_request_id(88, target, 3);
let outcome =
classify_request(&mut lookup, &request, &from, &my_addr, 1000, 5000, 4096, 1).outcome;
assert!(matches!(outcome, RequestOutcome::Forward));
assert_eq!(lookup.recent_requests.get(&88).unwrap().from_peer, from);
}
#[test]
fn classify_request_evicts_rather_than_refusing_a_full_dedup_cache() {
// Regression. The cache-full path used to drop the arriving request, so
// one peer emitting fresh request_ids
// could stop this node forwarding anyone's lookups and answering
// lookups for itself. A full cache now evicts instead, charged to the
// peer holding the most entries.
let mut lookup = empty_lookup();
let from = make_node_addr(0x01);
let my_addr = make_node_addr(0x99);
let target = make_node_addr(0xAA);
// Fill the cache to max_recent with distinct request_ids, through
// `record_recent` so the per-peer index stays level with the cache. A
// direct `recent_requests.insert` would leave the index short and turn
// the eviction policy into a no-op, so the test would pass without
// exercising it.
let max_recent = 3usize;
for id in 100..(100 + max_recent as u64) {
lookup.record_recent(id, from, 1000);
}
assert_eq!(lookup.recent_requests.len(), max_recent);
let request = make_request_id(1, target, 3);
let classification = classify_request(
&mut lookup,
&request,
&from,
&my_addr,
1000,
5000,
max_recent,
1,
);
assert!(
matches!(classification.outcome, RequestOutcome::Forward),
"a full cache must not stop the node forwarding"
);
let evicted = classification
.evicted
.expect("admitting into a full cache must evict something");
assert_eq!(evicted.request_id, 100, "the oldest entry is what pays");
assert_eq!(
evicted.peer, from,
"and the peer that filled the cache is what pays"
);
// The arriving request is admitted, the evicted one is gone, and the
// cache has not grown past its bound.
assert!(lookup.recent_requests.contains_key(&1));
assert!(!lookup.recent_requests.contains_key(&100));
assert_eq!(lookup.recent_requests.len(), max_recent);
}
#[test]
fn classify_request_respond_as_target() {
let mut lookup = empty_lookup();
let from = make_node_addr(0x01);
let my_addr = make_node_addr(0xAA);
// target == my_addr
let request = make_request_id(1, my_addr, 3);
assert!(matches!(
classify_request(&mut lookup, &request, &from, &my_addr, 1000, 5000, 4096, 1).outcome,
RequestOutcome::RespondAsTarget
));
// Recorded before the target decision.
assert!(lookup.recent_requests.contains_key(&1));
}
#[test]
fn classify_request_ttl_exhausted_for_non_target() {
let mut lookup = empty_lookup();
let from = make_node_addr(0x01);
let my_addr = make_node_addr(0x99);
let target = make_node_addr(0xAA);
// ttl 0 → not forwardable, and not the target.
let request = make_request_id(1, target, 0);
assert!(matches!(
classify_request(&mut lookup, &request, &from, &my_addr, 1000, 5000, 4096, 1).outcome,
RequestOutcome::TtlExhausted
));
}
#[test]
fn classify_request_forward_rate_limited() {
let mut lookup = empty_lookup();
let from = make_node_addr(0x01);
let my_addr = make_node_addr(0x99);
let target = make_node_addr(0xAA);
// Pre-seed the forward limiter so should_forward(target) returns false
// on the next call within the (default 2s) min interval.
assert!(lookup.forward_limiter.should_forward(&target, 1000));
let request = make_request_id(1, target, 3);
assert!(matches!(
classify_request(&mut lookup, &request, &from, &my_addr, 1000, 5000, 4096, 1).outcome,
RequestOutcome::ForwardRateLimited
));
}
#[test]
fn classify_request_purges_expired_entries() {
let mut lookup = empty_lookup();
let from = make_node_addr(0x01);
let my_addr = make_node_addr(0x99);
let target = make_node_addr(0xAA);
// Seed an entry that is expired at now_ms with the given expiry window.
// is_expired: now - timestamp > expiry_ms → expired.
lookup.record_recent(55, from, 1000);
// now_ms = 10_000, expiry_ms = 5000 → 9000 > 5000 → expired.
let request = make_request_id(1, target, 3);
let outcome = classify_request(
&mut lookup,
&request,
&from,
&my_addr,
10_000,
5000,
4096,
1,
)
.outcome;
assert!(matches!(outcome, RequestOutcome::Forward));
// The expired entry (55) must have been purged.
assert!(!lookup.recent_requests.contains_key(&55));
// The fresh request is recorded.
assert!(lookup.recent_requests.contains_key(&1));
}
#[test]
fn poll_pending_full_ladder_end_to_end() {
let target = make_node_addr(0x33);
let mut lookup = empty_lookup();
let t0 = 0u64;
lookup
.pending_lookups
.insert(target, PendingLookup::new(t0));
let ladder = [1u64, 2, 4, 8];
// attempt 1 → 2 at deadline 1s
let o = poll_pending(&mut lookup, t0 + 1000, &ladder);
assert_eq!(o.retries, vec![(target, 2)]);
// attempt 2 → 3 at deadline 2s after last send
let o = poll_pending(&mut lookup, t0 + 1000 + 2000, &ladder);
assert_eq!(o.retries, vec![(target, 3)]);
// attempt 3 → 4 at deadline 4s after last send
let o = poll_pending(&mut lookup, t0 + 1000 + 2000 + 4000, &ladder);
assert_eq!(o.retries, vec![(target, 4)]);
// attempt 4 is max → final timeout at deadline 8s after last send
let last = t0 + 1000 + 2000 + 4000;
let o = poll_pending(&mut lookup, last + 8000, &ladder);
assert!(o.retries.is_empty());
assert_eq!(o.timeouts, vec![(target, 1)]);
assert!(!lookup.pending_lookups.contains_key(&target));
}
// --- initiate_gate / initiate_failed tests ---
#[test]
fn initiate_gate_deduplicated_when_pending() {
let dest = make_node_addr(0x40);
let mut lookup = empty_lookup();
lookup.pending_lookups.insert(dest, PendingLookup::new(500));
// reachable=true would otherwise Proceed, but the pending entry wins.
assert!(matches!(
initiate_gate(&mut lookup, &dest, 1000, true),
InitiateDecision::Deduplicated
));
}
#[test]
fn initiate_gate_suppressed_by_backoff() {
let dest = make_node_addr(0x41);
let mut lookup = suppressing_lookup();
// One failure arms suppression under with_params(30, 300).
lookup.backoff.record_failure(&dest, 1000);
assert!(
lookup.backoff.is_suppressed(&dest, 1000),
"precondition: suppressed"
);
match initiate_gate(&mut lookup, &dest, 1000, true) {
InitiateDecision::Suppressed { failures } => assert_eq!(failures, 1),
_ => panic!("expected Suppressed"),
}
// No pending entry was inserted on the suppress path.
assert!(!lookup.pending_lookups.contains_key(&dest));
}
#[test]
fn initiate_gate_bloom_miss_records_failure() {
let dest = make_node_addr(0x42);
let mut lookup = empty_lookup();
assert!(matches!(
initiate_gate(&mut lookup, &dest, 1000, false),
InitiateDecision::BloomMiss
));
// A backoff failure was recorded, and no pending entry created.
assert_eq!(lookup.backoff.failure_count(&dest), 1);
assert!(!lookup.pending_lookups.contains_key(&dest));
}
#[test]
fn initiate_gate_proceed_inserts_pending() {
let dest = make_node_addr(0x43);
let mut lookup = empty_lookup();
let now_ms = 7_777u64;
assert!(matches!(
initiate_gate(&mut lookup, &dest, now_ms, true),
InitiateDecision::Proceed
));
// The pending entry now exists, stamped with now_ms.
let entry = lookup
.pending_lookups
.get(&dest)
.expect("Proceed must insert a pending lookup");
assert_eq!(entry.last_sent_ms, now_ms);
assert_eq!(entry.attempt, 1);
}
#[test]
fn initiate_failed_drops_pending_and_records_failure() {
let dest = make_node_addr(0x44);
let mut lookup = empty_lookup();
lookup
.pending_lookups
.insert(dest, PendingLookup::new(1000));
initiate_failed(&mut lookup, &dest, 1000);
assert!(
!lookup.pending_lookups.contains_key(&dest),
"pending entry must be dropped"
);
assert_eq!(lookup.backoff.failure_count(&dest), 1);
}