Skip to content
Open
Show file tree
Hide file tree
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
49 changes: 48 additions & 1 deletion src/node/core.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::core::model::direction::Direction;
use crate::core::{
IdSearchReq, IdSearchRes, Identifier, LookupTable, LookupTableLevel, MembershipVector,
IdSearchReq, IdSearchRes, Identifier, Identity, LinkOutcome, LookupTable, LookupTableLevel,
MembershipVector, RelinkOutcome,
};
use anyhow::anyhow;
use tracing::Span;
Expand Down Expand Up @@ -60,6 +61,34 @@ pub trait Core: Send + Sync {
/// * `level` - the minimum required common-prefix length, in bits.
fn prefix_match(&self, candidate: MembershipVector, level: LookupTableLevel) -> bool;

/// Decides whether `candidate` becomes this node's neighbor at `(level, direction)`. See
/// [`LookupTable::try_link`] for the decision rule.
///
/// # Errors
///
/// **CRITICAL, INTERNAL** — propagated from a failed decision on the local
/// lookup table: a broken local invariant, not evidence of anything a
/// peer sent.
Comment on lines +69 to +71
fn try_link(
&self,
level: LookupTableLevel,
direction: Direction,
candidate: Identity,
) -> anyhow::Result<LinkOutcome>;

/// Decides whether `claimant` should become, or already is, this node's neighbor at
/// `(level, direction)`. See [`LookupTable::try_relink`] for the decision rule.
///
/// # Errors
///
/// Same failure mode as [`Self::try_link`].
fn try_relink(
&self,
level: LookupTableLevel,
direction: Direction,
claimant: Identity,
) -> anyhow::Result<RelinkOutcome>;

/// Shallow-clones this core. Cloned instances share the same underlying
/// state (lookup table, etc.) via Arc.
fn clone_box(&self) -> Box<dyn Core>;
Expand Down Expand Up @@ -211,6 +240,24 @@ impl Core for BaseCore {
self.mem_vec.common_prefix_bit(candidate) >= level
}

fn try_link(
&self,
level: LookupTableLevel,
direction: Direction,
candidate: Identity,
) -> anyhow::Result<LinkOutcome> {
self.lt.try_link(level, direction, candidate)
}

fn try_relink(
&self,
level: LookupTableLevel,
direction: Direction,
claimant: Identity,
) -> anyhow::Result<RelinkOutcome> {
self.lt.try_relink(level, direction, claimant)
}

fn clone_box(&self) -> Box<dyn Core> {
Box::new(self.clone())
}
Expand Down
172 changes: 154 additions & 18 deletions src/node/core_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,26 @@ use crate::core::testutil::fixtures::{
random_membership_vector, span_fixture,
};
use crate::core::{
ArrayLookupTable, IdSearchReq, Identifier, LookupTable, LookupTableMock, MembershipVector,
LOOKUP_TABLE_LEVELS,
ArrayLookupTable, IdSearchReq, Identifier, LinkOutcome, LookupTable, LookupTableMock,
MembershipVector, RelinkOutcome, LOOKUP_TABLE_LEVELS,
};
use crate::node::core::{BaseCore, Core};
use anyhow::anyhow;
use rand::Rng;
use std::sync::Arc;
use unimock::*;

fn make_core(id: Identifier, lt: Box<dyn LookupTable>) -> BaseCore {
BaseCore::new(span_fixture(), id, random_membership_vector(), lt)
}

/// Verifies `search_by_id` returns the core's own identifier when the lookup
/// table is empty.
#[test]
fn test_search_by_id_singleton_fallback() {
let origin_id = Identifier::from_bytes(&[10u8]).unwrap();
let core = make_core(origin_id, Box::new(ArrayLookupTable::new()));
let core = BaseCore::new(
span_fixture(),
origin_id,
random_membership_vector(),
Box::new(ArrayLookupTable::new()),
);
Comment on lines +24 to +29

let cases = [
(Identifier::from_bytes(&[5u8]).unwrap(), Direction::Left),
Expand Down Expand Up @@ -63,7 +64,12 @@ fn test_search_by_id_found_left_direction() {
)
.expect("failed to update entry in lookup table");

let core = make_core(random_identifier(), Box::new(lt.clone()));
let core = BaseCore::new(
span_fixture(),
random_identifier(),
random_membership_vector(),
Box::new(lt.clone()),
);
let req = IdSearchReq {
nonce: Nonce::random(),
origin: core.id(),
Expand Down Expand Up @@ -100,7 +106,12 @@ fn test_search_by_id_found_right_direction() {
)
.expect("failed to update entry in lookup table");

let core = make_core(random_identifier(), Box::new(lt.clone()));
let core = BaseCore::new(
span_fixture(),
random_identifier(),
random_membership_vector(),
Box::new(lt.clone()),
);
let req = IdSearchReq {
nonce: Nonce::random(),
origin: core.id(),
Expand Down Expand Up @@ -143,7 +154,12 @@ fn test_search_by_id_not_found_left_direction() {
.expect("failed to update entry in lookup table");
}

let core = make_core(random_identifier(), Box::new(lt.clone()));
let core = BaseCore::new(
span_fixture(),
random_identifier(),
random_membership_vector(),
Box::new(lt.clone()),
);
let req = IdSearchReq {
nonce: Nonce::random(),
origin: core.id(),
Expand Down Expand Up @@ -179,7 +195,12 @@ fn test_search_by_id_not_found_right_direction() {
.expect("failed to update entry in lookup table");
}

let core = make_core(random_identifier(), Box::new(lt.clone()));
let core = BaseCore::new(
span_fixture(),
random_identifier(),
random_membership_vector(),
Box::new(lt.clone()),
);
let req = IdSearchReq {
nonce: Nonce::random(),
origin: core.id(),
Expand All @@ -199,7 +220,12 @@ fn test_search_by_id_not_found_right_direction() {
#[test]
fn test_search_by_id_exact_result() {
let lt = random_lookup_table_with_extremes(LOOKUP_TABLE_LEVELS);
let core = make_core(random_identifier(), Box::new(lt.clone()));
let core = BaseCore::new(
span_fixture(),
random_identifier(),
random_membership_vector(),
Box::new(lt.clone()),
);

for lvl in 0..LOOKUP_TABLE_LEVELS {
for direction in [Direction::Left, Direction::Right] {
Expand All @@ -226,7 +252,12 @@ fn test_search_by_id_exact_result() {
fn test_search_by_id_concurrent_found_left_direction() {
let lt = random_lookup_table_with_extremes(LOOKUP_TABLE_LEVELS);
let target = random_identifier();
let core: Box<dyn Core> = Box::new(make_core(random_identifier(), Box::new(lt.clone())));
let core: Box<dyn Core> = Box::new(BaseCore::new(
span_fixture(),
random_identifier(),
random_membership_vector(),
Box::new(lt.clone()),
));

assert_ne!(target, core.id());

Expand Down Expand Up @@ -280,7 +311,12 @@ fn test_search_by_id_concurrent_found_left_direction() {
fn test_search_by_id_concurrent_right_direction() {
let lt = random_lookup_table_with_extremes(LOOKUP_TABLE_LEVELS);
let target = random_identifier();
let core: Box<dyn Core> = Box::new(make_core(random_identifier(), Box::new(lt.clone())));
let core: Box<dyn Core> = Box::new(BaseCore::new(
span_fixture(),
random_identifier(),
random_membership_vector(),
Box::new(lt.clone()),
));

assert_ne!(target, core.id());

Expand Down Expand Up @@ -337,7 +373,12 @@ fn test_search_by_id_error_propagation() {
.answers(&|_, _, _| Err(anyhow!("simulated lookup table error"))),
);

let core = make_core(random_identifier(), Box::new(lt));
let core = BaseCore::new(
span_fixture(),
random_identifier(),
random_membership_vector(),
Box::new(lt),
);
let req = IdSearchReq {
nonce: Nonce::random(),
origin: core.id(),
Expand Down Expand Up @@ -366,7 +407,12 @@ fn test_search_by_id_error_propagation() {
/// Verifies `max_level` returns 0 when the lookup table has no populated entries.
#[test]
fn test_max_level_empty_table() {
let core = make_core(random_identifier(), Box::new(ArrayLookupTable::new()));
let core = BaseCore::new(
span_fixture(),
random_identifier(),
random_membership_vector(),
Box::new(ArrayLookupTable::new()),
);

assert_eq!(core.max_level().unwrap(), 0);
}
Expand All @@ -381,7 +427,12 @@ fn test_max_level_one_side_populated() {
lt.update_entry(random_identity(), 5, Direction::Left)
.expect("failed to update entry in lookup table");

let core = make_core(random_identifier(), Box::new(lt));
let core = BaseCore::new(
span_fixture(),
random_identifier(),
random_membership_vector(),
Box::new(lt),
);

assert_eq!(core.max_level().unwrap(), 5);
}
Expand All @@ -396,7 +447,12 @@ fn test_max_level_both_sides_populated_different_levels() {
lt.update_entry(random_identity(), 7, Direction::Right)
.expect("failed to update entry in lookup table");

let core = make_core(random_identifier(), Box::new(lt));
let core = BaseCore::new(
span_fixture(),
random_identifier(),
random_membership_vector(),
Box::new(lt),
);

assert_eq!(core.max_level().unwrap(), 7);
}
Expand Down Expand Up @@ -425,3 +481,83 @@ fn test_prefix_match() {
// false case: required level exceeds the actual common-prefix length.
assert!(!core.prefix_match(mv_ones, common + 1));
}

/// Verifies `Core::try_link` delegates to the lookup table: an empty slot is
/// linked directly and the write is visible through the table.
#[test]
fn test_try_link_empty_slot() {
let lt = ArrayLookupTable::new();
let core = BaseCore::new(
span_fixture(),
random_identifier(),
random_membership_vector(),
Box::new(lt.clone()),
);
let candidate = random_identity();

let outcome = core
.try_link(0, Direction::Left, candidate)
.expect("try_link failed");

assert_eq!(outcome, LinkOutcome::LinkedDirectly);
assert_eq!(lt.get_entry(0, Direction::Left).unwrap(), Some(candidate));
}

/// Verifies `Core::try_relink` delegates to the lookup table: a slot already
/// holding the claimant is reported as consistent and left untouched.
#[test]
fn test_try_relink_already_consistent() {
let lt = ArrayLookupTable::new();
let claimant = random_identity();
lt.update_entry(claimant, 0, Direction::Right)
.expect("failed to update entry in lookup table");
let core = BaseCore::new(
span_fixture(),
random_identifier(),
random_membership_vector(),
Box::new(lt.clone()),
);

let outcome = core
.try_relink(0, Direction::Right, claimant)
.expect("try_relink failed");

assert_eq!(outcome, RelinkOutcome::AlreadyConsistent);
assert_eq!(lt.get_entry(0, Direction::Right).unwrap(), Some(claimant));
}

/// Verifies `Core::try_link` propagates the lookup table's out-of-range-level error.
#[test]
fn test_try_link_out_of_range_level() {
let core = BaseCore::new(
span_fixture(),
random_identifier(),
random_membership_vector(),
Box::new(ArrayLookupTable::new()),
);

let result = core.try_link(LOOKUP_TABLE_LEVELS, Direction::Left, random_identity());

assert!(
result.is_err(),
"expected an error but got a success result"
);
}

/// Verifies `Core::try_relink` propagates the lookup table's out-of-range-level error.
#[test]
fn test_try_relink_out_of_range_level() {
let core = BaseCore::new(
span_fixture(),
random_identifier(),
random_membership_vector(),
Box::new(ArrayLookupTable::new()),
);

let result = core.try_relink(LOOKUP_TABLE_LEVELS, Direction::Left, random_identity());

assert!(
result.is_err(),
"expected an error but got a success result"
);
}
Loading