195 lines
6.8 KiB
Rust
195 lines
6.8 KiB
Rust
//! Addressed (relayed) delivery scenarios for the `Sim` harness.
|
|
//!
|
|
//! Unlike the broadcast convergence scenarios in `convergence.rs`, addressed
|
|
//! observations diverge by design: the recipient holds a tasking that the
|
|
//! carriers no longer need once they have forwarded it. These tests exercise
|
|
//! the relay path — an [`Sim::observe_to`] observation handed off hop-by-hop
|
|
//! along a contact chain — without asserting whole-graph convergence.
|
|
//!
|
|
//! * [`custody_chain_delivers_without_carrier_failure`] — an addressed task
|
|
//! relayed hop-by-hop along an A→B→C chain reaches the recipient, and the
|
|
//! custody chain releases at every hop: a carrier that gets an ack stops
|
|
//! forwarding the id.
|
|
//! * [`released_duplicate_not_acked`] — a carrier that released custody on the
|
|
//! recipient's ack reconnects with the recipient without pulling the
|
|
//! delivered task back out of custody, so the message never vanishes.
|
|
|
|
use tines_core::{NodeId, Store};
|
|
use tines_sim::{Contact, ContactTrace, Sim, WorldConfig};
|
|
|
|
/// The recipient id. `Sim` assigns node `i` the id `[i as u8; 16]`, so node 2's
|
|
/// id is `[2; 16]` and the task in both tests below is addressed to it.
|
|
const RECIPIENT: NodeId = [2; 16];
|
|
|
|
/// The relay contact trace shared by both tests: `(0,1)` during `[0, 5000)`,
|
|
/// then `(1,2)` during `[10_000, 15_000)`. Node 0 and node 2 never share a
|
|
/// contact, so a task must be relayed through node 1 to reach node 2.
|
|
fn relay_trace() -> ContactTrace {
|
|
let mut trace = ContactTrace::default();
|
|
trace.contacts.push(Contact {
|
|
start_ms: 0,
|
|
end_ms: 5_000,
|
|
a: 0,
|
|
b: 1,
|
|
});
|
|
trace.contacts.push(Contact {
|
|
start_ms: 10_000,
|
|
end_ms: 15_000,
|
|
a: 1,
|
|
b: 2,
|
|
});
|
|
trace
|
|
}
|
|
|
|
/// The task observed at `t = 0` reaches the far end of the chain even though
|
|
/// node 0 and node 2 never met; it is relayed through node 1.
|
|
#[test]
|
|
fn relayed_task_reaches_recipient() {
|
|
let trace = relay_trace();
|
|
let end = trace.end();
|
|
let cfg = WorldConfig {
|
|
nodes: 3,
|
|
mtu: 1500,
|
|
loss: 0.0,
|
|
dup: 0.0,
|
|
seed: 0,
|
|
};
|
|
let mut sim = Sim::new(cfg, trace);
|
|
let id = sim.observe_to(0, RECIPIENT, 60_000, b"task", b"investigate");
|
|
sim.run_until(end);
|
|
|
|
assert!(
|
|
sim.nodes[2].store().contains(&id),
|
|
"node 2 must hold the relayed task"
|
|
);
|
|
}
|
|
|
|
/// An addressed task with a short lifetime never reaches the recipient: the
|
|
/// carrier drops it once its age passes the lifetime, so node 2 never stores
|
|
/// it after running well past expiry.
|
|
#[test]
|
|
fn carrier_drops_expired() {
|
|
let trace = relay_trace();
|
|
let end = trace.end();
|
|
let cfg = WorldConfig {
|
|
nodes: 3,
|
|
mtu: 1500,
|
|
loss: 0.0,
|
|
dup: 0.0,
|
|
seed: 0,
|
|
};
|
|
let mut sim = Sim::new(cfg, trace);
|
|
let id = sim.observe_to(0, RECIPIENT, 10, b"task", b"investigate");
|
|
sim.run_until(end + 10_000);
|
|
|
|
assert!(
|
|
!sim.nodes[2].store().contains(&id),
|
|
"an addressed task past its lifetime must not survive at the recipient"
|
|
);
|
|
}
|
|
|
|
/// An addressed task relayed hop-by-hop along a connected A→B→C contact chain
|
|
/// reaches the far end, and the custody chain releases at every hop: a carrier
|
|
/// that receives an ack stops forwarding the id. Neither hop fails — the
|
|
/// message lands with the recipient and no carrier keeps holding a copy it has
|
|
/// already handed off (the "carrier failure" the custody chain prevents).
|
|
///
|
|
/// The trace is the shared relay trace: `(0,1)` during `[0, 5000)`, then
|
|
/// `(1,2)` during `[10_000, 15_000)`, so the task must hop through node 1.
|
|
#[test]
|
|
fn custody_chain_delivers_without_carrier_failure() {
|
|
let trace = relay_trace();
|
|
let end = trace.end();
|
|
let cfg = WorldConfig {
|
|
nodes: 3,
|
|
mtu: 1500,
|
|
loss: 0.0,
|
|
dup: 0.0,
|
|
seed: 0,
|
|
};
|
|
let mut sim = Sim::new(cfg, trace);
|
|
// Node 0 addresses the task to node 2, the far end of the chain.
|
|
let id = sim.observe_to(0, RECIPIENT, 600_000, b"task", b"investigate");
|
|
// Relay it down the chain (0→1 in the first window, 1→2 in the second)
|
|
// and run a little past the end so node 1 can collect node 2's ack and
|
|
// release before the last contact closes.
|
|
sim.run_until(end + 2_000);
|
|
|
|
// The recipient holds the delivered task, un-released.
|
|
let c = sim.nodes[2].store();
|
|
assert!(
|
|
c.contains(&id) && !c.is_released(&id),
|
|
"node 2 must hold the addressed task after the relay"
|
|
);
|
|
|
|
// Every carrier that relayed it still holds the copy but has released it
|
|
// on the ack, so it no longer forwards the id (a released store is not
|
|
// re-offered).
|
|
for carrier in &sim.nodes[0..2] {
|
|
let s = carrier.store();
|
|
assert!(
|
|
s.contains(&id) && s.is_released(&id),
|
|
"carrier node {} must still hold the task but released",
|
|
carrier.id()[0]
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A carrier and its recipient both hold an addressed task mid-path; the
|
|
/// carrier releases custody once the recipient acks (the custody chain). When
|
|
/// the pair reconnects, the delivered task must not be lost: the recipient
|
|
/// keeps it, so custody never vanishes ("released-with-nobody-holding").
|
|
///
|
|
/// Two A↔B windows let the same pair reconnect after the first exchange: the
|
|
/// release happened over the first contact, the reconnect over the second.
|
|
#[test]
|
|
fn released_duplicate_not_acked() {
|
|
let mut trace = ContactTrace::default();
|
|
trace.contacts.push(Contact {
|
|
start_ms: 0,
|
|
end_ms: 5_000,
|
|
a: 0,
|
|
b: 1,
|
|
});
|
|
trace.contacts.push(Contact {
|
|
start_ms: 10_000,
|
|
end_ms: 15_000,
|
|
a: 0,
|
|
b: 1,
|
|
});
|
|
let end = trace.end();
|
|
let cfg = WorldConfig {
|
|
nodes: 2,
|
|
mtu: 1500,
|
|
loss: 0.0,
|
|
dup: 0.0,
|
|
seed: 0,
|
|
};
|
|
let mut sim = Sim::new(cfg, trace);
|
|
// Node 0 addresses a task to node 1.
|
|
let id = sim.observe_to(0, RECIPIENT, 600_000, b"task", b"investigate");
|
|
// First window: node 1 receives it and acks, so node 0 releases custody on
|
|
// the ack and node 1 is left holding the delivered task.
|
|
sim.run_until(5_000);
|
|
assert!(
|
|
sim.nodes[0].store().is_released(&id),
|
|
"the carrier must release custody on the recipient's ack"
|
|
);
|
|
assert!(
|
|
sim.nodes[1].store().contains(&id),
|
|
"the recipient must hold the task after delivery"
|
|
);
|
|
|
|
// Reconnect the pair and run the second window.
|
|
sim.run_until(end + 2_000);
|
|
|
|
// The task must stay with the recipient: reconnecting a released carrier
|
|
// must not pull the delivered copy back out of custody, so at least one
|
|
// node still holds it.
|
|
let r = sim.nodes[1].store();
|
|
assert!(
|
|
r.contains(&id) && !r.is_released(&id),
|
|
"the recipient must keep the delivered task after reconnecting"
|
|
);
|
|
}
|