Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions src/node/base_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2792,4 +2792,164 @@ mod tests {
"z's reply must land on this node's own right slot"
);
}

/// The search returns `s`, the closest node below this node's id, and `s` has no right
/// neighbor `z`. The join must send only one link request, to `s`, and finish once `s`
/// replies. This node's left entry then holds `s`, and its right entry stays empty.
#[tokio::test]
async fn test_join_stage1_link_level0_no_right_neighbor_sends_single_link_request() {
let node_id = random_identifier();
let mem_vec = random_membership_vector();
let address = random_address();
let span = span_fixture();
let introducer = random_identifier_less_than(&node_id);
// the reply is validated against this node's id, so `s` must sit below it.
let s_identity = Identity::new(
random_identifier_less_than(&node_id),
random_membership_vector(),
random_address(),
);
let s_id = s_identity.id();

let search_nonce_cell: Arc<Mutex<Option<Nonce>>> = Arc::new(Mutex::new(None));
let neighbor_nonce_cell: Arc<Mutex<Option<Nonce>>> = Arc::new(Mutex::new(None));
let s_link_nonce_cell: Arc<Mutex<Option<Nonce>>> = Arc::new(Mutex::new(None));
let link_request_count: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
let (search_mock, neighbor_mock, s_link_mock, link_count_mock) = (
search_nonce_cell.clone(),
neighbor_nonce_cell.clone(),
s_link_nonce_cell.clone(),
link_request_count.clone(),
);

let mock_net = Unimock::new((
NetworkMock::register_processor
.each_call(matching!(_))
.answers(&|_, _| Ok(())),
NetworkMock::clone_box
.each_call(matching!())
.answers(&|mock| Box::new(mock.clone())),
NetworkMock::address
.each_call(matching!())
.answers_arc(Arc::new(move |_| address)),
NetworkMock::send_event
.each_call(matching!(_))
.answers_arc(Arc::new(move |_, dest: Identifier, event: Event| {
match event {
SearchByIdRequest(req) => {
assert_eq!(dest, introducer, "search must go to the introducer");
assert_eq!(
req.direction,
Direction::Right,
"introducer.id() < u.id() must search Direction::Right"
);
*search_mock.lock().expect("mutex poisoned") = Some(req.nonce);
}
GetNeighborOp(req) => {
assert_eq!(dest, s_id, "neighbor query must go to s");
assert_eq!(
req.direction,
Direction::Right,
"neighbor query must reuse the search's own direction"
);
*neighbor_mock.lock().expect("mutex poisoned") = Some(req.nonce);
}
GetLinkOp(req) => {
assert_eq!(
dest,
s_id,
"no z was ever known, so no second link request should be sent"
);
*link_count_mock.lock().expect("mutex poisoned") += 1;
*s_link_mock.lock().expect("mutex poisoned") = Some(req.nonce);
}
_ => panic!("unexpected event to {:?}: {:?}", dest, event),
}
Ok(())
})),
));

let lt = ArrayLookupTable::new();
let core = Box::new(BaseCore::new(
span.clone(),
node_id,
mem_vec,
Box::new(lt.clone()),
));
let node = BaseNode::new(span, core, Box::new(mock_net)).expect("failed to create node");
let node_reply = node.clone();

let deliver = async {
let search_nonce =
poll_until_some(|| *search_nonce_cell.lock().expect("mutex poisoned")).await;
node_reply
.process_incoming_event(
introducer,
SearchByIdResponse(IdSearchRes {
nonce: search_nonce,
target: node_id,
termination_level: 0,
result: s_id,
}),
)
.expect("failed to process search reply");

let neighbor_nonce =
poll_until_some(|| *neighbor_nonce_cell.lock().expect("mutex poisoned")).await;
node_reply
.process_incoming_event(
s_id,
RetNeighborOp(NeighborRes {
nonce: neighbor_nonce,
level: 0,
direction: Direction::Right,
neighbor: None,
}),
)
.expect("failed to process neighbor reply");

let s_link_nonce =
poll_until_some(|| *s_link_nonce_cell.lock().expect("mutex poisoned")).await;
node_reply
.process_incoming_event(
s_id,
SetLinkOp(LinkRes {
nonce: s_link_nonce,
dir: Direction::Left,
level: 0,
linked: Some(s_identity),
}),
)
.expect("failed to process s link reply");
};

let (join_result, ()) = tokio::time::timeout(Duration::from_secs(2), async {
tokio::join!(
node.join_stage1_link_level0(introducer, 0, Duration::from_secs(1)),
deliver
)
})
.await
.expect("test timed out");

join_result.expect("join_stage1_link_level0 should resolve");

assert_eq!(
*link_request_count.lock().expect("mutex poisoned"),
1,
"exactly one GetLinkOp should ever be sent when z is None"
);
assert_eq!(
lt.get_entry(0, Direction::Left)
.expect("get_entry should not error")
.map(|identity| identity.id()),
Some(s_id)
);
assert_eq!(
lt.get_entry(0, Direction::Right)
.expect("get_entry should not error"),
None,
"right side must remain unset when z was None at query time"
);
}
}
Loading