feat(host): widen service IPC — addressed observe, List, contact/drop events
New public names: ContactOut, DropOut, ObservationOut, LIST_CAP.
This commit is contained in:
+167
-24
@@ -3,8 +3,9 @@
|
|||||||
//! The [`serve`] fn hosts a tines [`Node`] and speaks a tiny
|
//! The [`serve`] fn hosts a tines [`Node`] and speaks a tiny
|
||||||
//! request/response protocol to each connecting peer. A single instance is the
|
//! request/response protocol to each connecting peer. A single instance is the
|
||||||
//! control plane for its device; external tools (or another tines Node, in
|
//! control plane for its device; external tools (or another tines Node, in
|
||||||
//! later tasks) drive the node through four operations: `observe`, `belief`,
|
//! later tasks) drive the node through six operations: `observe`, `belief`,
|
||||||
//! `events`, and `status`.
|
//! `events`, `status`, and `list` (the fleet console adds addressed `observe`
|
||||||
|
//! and contact/drop reporting to the `events` response).
|
||||||
//!
|
//!
|
||||||
//! # Wire format
|
//! # Wire format
|
||||||
//!
|
//!
|
||||||
@@ -39,7 +40,7 @@ use std::os::unix::net::{UnixListener, UnixStream};
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use tines_core::{Node, NodeEvent, Store, Transport, TrimPolicy};
|
use tines_core::{Node, NodeEvent, NodeId, Store, Transport, TrimPolicy};
|
||||||
use tines_store::LogStore;
|
use tines_store::LogStore;
|
||||||
|
|
||||||
use crate::trust::{adopt_wall_clock, PeerTimeWindow};
|
use crate::trust::{adopt_wall_clock, PeerTimeWindow};
|
||||||
@@ -52,6 +53,9 @@ const MAX_FRAME: usize = 1 << 20; // 1 MiB cap on a single JSON frame
|
|||||||
/// of its accept loop.
|
/// of its accept loop.
|
||||||
const POLL_CAP: u64 = 64;
|
const POLL_CAP: u64 = 64;
|
||||||
|
|
||||||
|
/// Max entries a `List` response returns; `truncated` reports when more exist.
|
||||||
|
pub const LIST_CAP: usize = 1024;
|
||||||
|
|
||||||
/// A confirmed delivery handed out by the IPC `events` request.
|
/// A confirmed delivery handed out by the IPC `events` request.
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||||
pub struct EventOut {
|
pub struct EventOut {
|
||||||
@@ -61,6 +65,50 @@ pub struct EventOut {
|
|||||||
pub peer: String,
|
pub peer: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A contact change handed out by the IPC `events` request: a transport
|
||||||
|
/// `Contact` (`joined: true`) or `Lost` (`joined: false`) for one peer.
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||||
|
pub struct ContactOut {
|
||||||
|
/// Hex of the peer's `NodeId`.
|
||||||
|
pub peer: String,
|
||||||
|
/// `true` for a new contact, `false` for a loss.
|
||||||
|
pub joined: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A counted drop handed out by the IPC `events` request.
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||||
|
pub struct DropOut {
|
||||||
|
/// Hex of the peer the dropped frame came from, or `None` for the
|
||||||
|
/// overflow slot (which has no address to name).
|
||||||
|
pub peer: Option<String>,
|
||||||
|
/// The drop reason, debug-formatted (e.g. `"VersionMismatch { version: 255 }"`).
|
||||||
|
pub reason: String,
|
||||||
|
/// The number of frames folded into this counter since the last drain.
|
||||||
|
pub count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One stored observation handed out by the IPC `list` request.
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||||
|
pub struct ObservationOut {
|
||||||
|
/// Hex of the `ObservationId`.
|
||||||
|
pub id: String,
|
||||||
|
/// Hex of the observer's `NodeId`.
|
||||||
|
pub observer: String,
|
||||||
|
/// Hex of the topic.
|
||||||
|
pub topic: String,
|
||||||
|
/// Hex of the payload.
|
||||||
|
pub payload: String,
|
||||||
|
/// Hex of the destination, or `None` for a broadcast.
|
||||||
|
pub to: Option<String>,
|
||||||
|
/// The addressed lifetime in ms, or `None` for a broadcast.
|
||||||
|
pub lifetime_ms: Option<u64>,
|
||||||
|
/// How old the observation is, in ms (see `Observation::age_ms`).
|
||||||
|
pub age_ms: u64,
|
||||||
|
/// The observer's wall-clock time, ms since the Unix epoch, or `None`
|
||||||
|
/// when the observer had no trusted clock.
|
||||||
|
pub observed_at: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
/// A belief returned by the IPC `belief` request.
|
/// A belief returned by the IPC `belief` request.
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||||
pub struct BeliefOut {
|
pub struct BeliefOut {
|
||||||
@@ -82,16 +130,26 @@ pub enum Request {
|
|||||||
topic: String,
|
topic: String,
|
||||||
/// Hex of the observation payload.
|
/// Hex of the observation payload.
|
||||||
payload: String,
|
payload: String,
|
||||||
|
/// Hex of the destination `NodeId` for an addressed observation.
|
||||||
|
/// Must be set together with `lifetime_ms`.
|
||||||
|
#[serde(default)]
|
||||||
|
to: Option<String>,
|
||||||
|
/// The addressed lifetime in ms. Must be set together with `to`.
|
||||||
|
#[serde(default)]
|
||||||
|
lifetime_ms: Option<u64>,
|
||||||
},
|
},
|
||||||
/// Read the latest belief for `topic`.
|
/// Read the latest belief for `topic`.
|
||||||
Belief {
|
Belief {
|
||||||
/// Hex of the topic bytes to query.
|
/// Hex of the topic bytes to query.
|
||||||
topic: String,
|
topic: String,
|
||||||
},
|
},
|
||||||
/// Drain outstanding confirmed deliveries and drop counters.
|
/// Drain outstanding confirmed deliveries, contact changes, and counted
|
||||||
|
/// drops.
|
||||||
Events,
|
Events,
|
||||||
/// Report the node's identity and log depth.
|
/// Report the node's identity and log depth.
|
||||||
Status,
|
Status,
|
||||||
|
/// List every stored observation (capped at [`LIST_CAP`]).
|
||||||
|
List,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An IPC response. See the module docs for the wire format.
|
/// An IPC response. See the module docs for the wire format.
|
||||||
@@ -108,10 +166,17 @@ pub enum Response {
|
|||||||
/// The stored belief, or `None` if the topic is unknown.
|
/// The stored belief, or `None` if the topic is unknown.
|
||||||
obs: Option<BeliefOut>,
|
obs: Option<BeliefOut>,
|
||||||
},
|
},
|
||||||
/// Confirmed deliveries drained from the exchange.
|
/// Confirmed deliveries, contact changes, and counted drops drained from
|
||||||
|
/// the exchange.
|
||||||
Events {
|
Events {
|
||||||
/// The drained confirmed deliveries.
|
/// The drained confirmed deliveries.
|
||||||
events: Vec<EventOut>,
|
events: Vec<EventOut>,
|
||||||
|
/// The drained contact changes (joined or lost), in order.
|
||||||
|
#[serde(default)]
|
||||||
|
contacts: Vec<ContactOut>,
|
||||||
|
/// The drained counted drops.
|
||||||
|
#[serde(default)]
|
||||||
|
drops: Vec<DropOut>,
|
||||||
},
|
},
|
||||||
/// The node's `id`, next tines `next_seq`, and observations `stored`.
|
/// The node's `id`, next tines `next_seq`, and observations `stored`.
|
||||||
Status {
|
Status {
|
||||||
@@ -122,6 +187,14 @@ pub enum Response {
|
|||||||
/// Number of observations in the store.
|
/// Number of observations in the store.
|
||||||
stored: u32,
|
stored: u32,
|
||||||
},
|
},
|
||||||
|
/// Every stored observation, up to [`LIST_CAP`] entries.
|
||||||
|
List {
|
||||||
|
/// The stored observations.
|
||||||
|
observations: Vec<ObservationOut>,
|
||||||
|
/// `true` when more observations exist than fit in
|
||||||
|
/// [`LIST_CAP`] entries.
|
||||||
|
truncated: bool,
|
||||||
|
},
|
||||||
/// The request was malformed or rejected.
|
/// The request was malformed or rejected.
|
||||||
Error {
|
Error {
|
||||||
/// A human-readable explanation.
|
/// A human-readable explanation.
|
||||||
@@ -147,10 +220,15 @@ pub fn handle<S, T>(
|
|||||||
) -> Response
|
) -> Response
|
||||||
where
|
where
|
||||||
S: Store + 'static,
|
S: Store + 'static,
|
||||||
T: Transport,
|
T: Transport<Addr = NodeId>,
|
||||||
{
|
{
|
||||||
match req {
|
match req {
|
||||||
Request::Observe { topic, payload } => {
|
Request::Observe {
|
||||||
|
topic,
|
||||||
|
payload,
|
||||||
|
to,
|
||||||
|
lifetime_ms,
|
||||||
|
} => {
|
||||||
let topic = match decode_hex(&topic) {
|
let topic = match decode_hex(&topic) {
|
||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
Err(e) => return error(e),
|
Err(e) => return error(e),
|
||||||
@@ -159,7 +237,26 @@ where
|
|||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
Err(e) => return error(e),
|
Err(e) => return error(e),
|
||||||
};
|
};
|
||||||
match node.observe(&topic, &payload, observed_at, now) {
|
let result = match (to, lifetime_ms) {
|
||||||
|
(None, None) => node.observe(&topic, &payload, observed_at, now),
|
||||||
|
(Some(to), Some(lifetime_ms)) => {
|
||||||
|
let to = match decode_hex(&to) {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => return error(e),
|
||||||
|
};
|
||||||
|
let to: NodeId = match to.try_into() {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(_) => {
|
||||||
|
return error("to must be a 16-byte NodeId (32 hex chars)");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
node.observe_to(to, lifetime_ms, &topic, &payload, observed_at, now)
|
||||||
|
}
|
||||||
|
// An addressed observation needs both a destination and a
|
||||||
|
// lifetime; half-addressed is almost certainly a client bug.
|
||||||
|
_ => return error("to and lifetime_ms must be set together"),
|
||||||
|
};
|
||||||
|
match result {
|
||||||
Ok(id) => Response::Observe {
|
Ok(id) => Response::Observe {
|
||||||
id: encode_hex(&id),
|
id: encode_hex(&id),
|
||||||
},
|
},
|
||||||
@@ -183,30 +280,76 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Request::Events => {
|
Request::Events => {
|
||||||
let events = node
|
let mut events = Vec::new();
|
||||||
.drain_events()
|
let mut contacts = Vec::new();
|
||||||
.into_iter()
|
let mut drops = Vec::new();
|
||||||
.filter_map(|ev| match ev {
|
for ev in node.drain_events() {
|
||||||
NodeEvent::Delivered { peer, id } => Some(EventOut {
|
match ev {
|
||||||
|
NodeEvent::Delivered { peer, id } => events.push(EventOut {
|
||||||
id: encode_hex(&id),
|
id: encode_hex(&id),
|
||||||
peer: encode_hex(&peer),
|
peer: encode_hex(&peer),
|
||||||
}),
|
}),
|
||||||
// Dropped frames are surfaced to the operator via stderr
|
NodeEvent::Contact { addr } => contacts.push(ContactOut {
|
||||||
// (they are the whole point of D3), not the `events`
|
peer: encode_hex(&addr),
|
||||||
// response, so they filter out here.
|
joined: true,
|
||||||
|
}),
|
||||||
|
NodeEvent::Lost { addr } => contacts.push(ContactOut {
|
||||||
|
peer: encode_hex(&addr),
|
||||||
|
joined: false,
|
||||||
|
}),
|
||||||
|
// Drops surface here AND on stderr (they are the whole
|
||||||
|
// point of D3; the stderr line stays for `tines` logs).
|
||||||
NodeEvent::Dropped {
|
NodeEvent::Dropped {
|
||||||
addr,
|
addr,
|
||||||
reason,
|
reason,
|
||||||
count,
|
count,
|
||||||
} => {
|
} => {
|
||||||
eprintln!("tines: drop: {reason:?} (count={count}, addr={addr:?})");
|
eprintln!("tines: drop: {reason:?} (count={count}, addr={addr:?})");
|
||||||
None
|
drops.push(DropOut {
|
||||||
|
peer: addr.map(|a| encode_hex(&a)),
|
||||||
|
reason: format!("{reason:?}"),
|
||||||
|
count,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// `NodeEvent` is non-exhaustive: ignore any future variant.
|
// `NodeEvent` is non-exhaustive: ignore any future variant.
|
||||||
_ => None,
|
_ => {}
|
||||||
})
|
}
|
||||||
.collect();
|
}
|
||||||
Response::Events { events }
|
Response::Events {
|
||||||
|
events,
|
||||||
|
contacts,
|
||||||
|
drops,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Request::List => {
|
||||||
|
let mut ids = Vec::with_capacity(LIST_CAP.min(node.store().len()));
|
||||||
|
let mut truncated = false;
|
||||||
|
node.store().for_each_id(&mut |id| {
|
||||||
|
if ids.len() < LIST_CAP {
|
||||||
|
ids.push(*id);
|
||||||
|
} else {
|
||||||
|
truncated = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut observations = Vec::with_capacity(ids.len());
|
||||||
|
for id in &ids {
|
||||||
|
if let Some(obs) = node.store().get(id, now) {
|
||||||
|
observations.push(ObservationOut {
|
||||||
|
id: encode_hex(&obs.id()),
|
||||||
|
observer: encode_hex(&obs.observer),
|
||||||
|
topic: encode_hex(&obs.topic),
|
||||||
|
payload: encode_hex(&obs.payload),
|
||||||
|
to: obs.dest.as_ref().map(|d| encode_hex(&d.to)),
|
||||||
|
lifetime_ms: obs.dest.as_ref().map(|d| d.lifetime_ms),
|
||||||
|
age_ms: obs.age_ms,
|
||||||
|
observed_at: obs.observed_at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Response::List {
|
||||||
|
observations,
|
||||||
|
truncated,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Request::Status => Response::Status {
|
Request::Status => Response::Status {
|
||||||
id: encode_hex(&node.id()),
|
id: encode_hex(&node.id()),
|
||||||
@@ -232,7 +375,7 @@ struct HostState {
|
|||||||
// [`serve`], fed by the socket read timeouts.
|
// [`serve`], fed by the socket read timeouts.
|
||||||
fn poll_exchange<T>(node: &mut Node<LogStore, T>, host: &mut HostState)
|
fn poll_exchange<T>(node: &mut Node<LogStore, T>, host: &mut HostState)
|
||||||
where
|
where
|
||||||
T: Transport + 'static,
|
T: Transport<Addr = NodeId> + 'static,
|
||||||
{
|
{
|
||||||
while node.poll(host.clock.now()) > 0 && host.rounds < POLL_CAP {
|
while node.poll(host.clock.now()) > 0 && host.rounds < POLL_CAP {
|
||||||
host.rounds += 1;
|
host.rounds += 1;
|
||||||
@@ -308,7 +451,7 @@ pub fn serve<T>(
|
|||||||
peer_time_window_ms: u64,
|
peer_time_window_ms: u64,
|
||||||
) -> std::io::Result<()>
|
) -> std::io::Result<()>
|
||||||
where
|
where
|
||||||
T: Transport + 'static,
|
T: Transport<Addr = NodeId> + 'static,
|
||||||
{
|
{
|
||||||
let socket_path = socket_path.as_ref().to_path_buf();
|
let socket_path = socket_path.as_ref().to_path_buf();
|
||||||
// Clear a stale socket file left by a previous run so bind() can't fail.
|
// Clear a stale socket file left by a previous run so bind() can't fail.
|
||||||
@@ -370,7 +513,7 @@ fn serve_stream<T>(
|
|||||||
host: &mut HostState,
|
host: &mut HostState,
|
||||||
) -> io::Result<()>
|
) -> io::Result<()>
|
||||||
where
|
where
|
||||||
T: Transport + 'static,
|
T: Transport<Addr = NodeId> + 'static,
|
||||||
{
|
{
|
||||||
loop {
|
loop {
|
||||||
match read_request(stream) {
|
match read_request(stream) {
|
||||||
|
|||||||
+4
-1
@@ -15,4 +15,7 @@ pub mod ipc;
|
|||||||
pub mod trust;
|
pub mod trust;
|
||||||
|
|
||||||
pub use clock::HostClock;
|
pub use clock::HostClock;
|
||||||
pub use ipc::{handle, serve, BeliefOut, EventOut, Request, Response};
|
pub use ipc::{
|
||||||
|
handle, serve, BeliefOut, ContactOut, DropOut, EventOut, ObservationOut, Request, Response,
|
||||||
|
LIST_CAP,
|
||||||
|
};
|
||||||
|
|||||||
+288
-2
@@ -266,6 +266,8 @@ fn observe_and_belief_over_socket() {
|
|||||||
&Request::Observe {
|
&Request::Observe {
|
||||||
topic: hex_encode(&topic()),
|
topic: hex_encode(&topic()),
|
||||||
payload: hex_encode(&payload()),
|
payload: hex_encode(&payload()),
|
||||||
|
to: None,
|
||||||
|
lifetime_ms: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let id = match read_response(&mut stream) {
|
let id = match read_response(&mut stream) {
|
||||||
@@ -311,7 +313,7 @@ fn events_over_socket() {
|
|||||||
|
|
||||||
rpc(&mut stream, &Request::Events);
|
rpc(&mut stream, &Request::Events);
|
||||||
match read_response(&mut stream) {
|
match read_response(&mut stream) {
|
||||||
Response::Events { events } => {
|
Response::Events { events, .. } => {
|
||||||
assert_eq!(events.len(), 1, "expected one confirmed delivery");
|
assert_eq!(events.len(), 1, "expected one confirmed delivery");
|
||||||
assert_eq!(events[0].id, hex_encode(&x), "event id matches observed id");
|
assert_eq!(events[0].id, hex_encode(&x), "event id matches observed id");
|
||||||
assert_eq!(events[0].peer, hex_encode(&peer), "event came from peer");
|
assert_eq!(events[0].peer, hex_encode(&peer), "event came from peer");
|
||||||
@@ -329,6 +331,8 @@ fn handle_bad_hex_is_error() {
|
|||||||
Request::Observe {
|
Request::Observe {
|
||||||
topic: "z".into(),
|
topic: "z".into(),
|
||||||
payload: "ff".into(),
|
payload: "ff".into(),
|
||||||
|
to: None,
|
||||||
|
lifetime_ms: None,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
0,
|
0,
|
||||||
@@ -348,8 +352,14 @@ fn confirmed_delivery_direct_probe() {
|
|||||||
node.poll(0);
|
node.poll(0);
|
||||||
node.poll(0);
|
node.poll(0);
|
||||||
let evs = node.drain_events();
|
let evs = node.drain_events();
|
||||||
assert_eq!(evs.len(), 1, "one confirmed delivery after observe+ack");
|
// The mock's first poll surfaces a Contact (now a NodeEvent) ahead of the
|
||||||
|
// confirmed delivery.
|
||||||
|
assert_eq!(evs.len(), 2, "contact event then confirmed delivery");
|
||||||
match &evs[0] {
|
match &evs[0] {
|
||||||
|
NodeEvent::Contact { addr } => assert_eq!(*addr, [7; 16]),
|
||||||
|
other => panic!("expected a Contact event, got {other:?}"),
|
||||||
|
}
|
||||||
|
match &evs[1] {
|
||||||
NodeEvent::Delivered { peer, id } => {
|
NodeEvent::Delivered { peer, id } => {
|
||||||
assert_eq!(*id, x);
|
assert_eq!(*id, x);
|
||||||
assert_eq!(*peer, [7; 16]);
|
assert_eq!(*peer, [7; 16]);
|
||||||
@@ -589,6 +599,8 @@ fn f2_propagation_between_instances() {
|
|||||||
&Request::Observe {
|
&Request::Observe {
|
||||||
topic: hex_encode(&topic()),
|
topic: hex_encode(&topic()),
|
||||||
payload: hex_encode(&payload()),
|
payload: hex_encode(&payload()),
|
||||||
|
to: None,
|
||||||
|
lifetime_ms: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let obs = match read_response(&mut stream_a) {
|
let obs = match read_response(&mut stream_a) {
|
||||||
@@ -694,6 +706,8 @@ fn b_stamp(
|
|||||||
&Request::Observe {
|
&Request::Observe {
|
||||||
topic: hex_encode(&topic()),
|
topic: hex_encode(&topic()),
|
||||||
payload: hex_encode(format!("probe-{n}").as_bytes()),
|
payload: hex_encode(format!("probe-{n}").as_bytes()),
|
||||||
|
to: None,
|
||||||
|
lifetime_ms: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let id = match read_response(&mut stream) {
|
let id = match read_response(&mut stream) {
|
||||||
@@ -895,3 +909,275 @@ fn forged_estimate_does_not_hide_an_honest_one() {
|
|||||||
fn below_floor_observed_at_is_not_adopted() {
|
fn below_floor_observed_at_is_not_adopted() {
|
||||||
assert_not_adopted("floor", 1_000_000_000_000);
|
assert_not_adopted("floor", 1_000_000_000_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Widened IPC (fleet console): addressed observe, List, contact/drop events
|
||||||
|
|
||||||
|
/// An addressed `Observe` (both `to` and `lifetime_ms` set) returns the id and
|
||||||
|
/// the observation shows up in `List` with matching addressing.
|
||||||
|
#[test]
|
||||||
|
fn observe_addressed_roundtrip() {
|
||||||
|
let dir = unique_dir("addressed");
|
||||||
|
let mut node = build_node([3; 16], &dir);
|
||||||
|
let to = [4u8; 16];
|
||||||
|
|
||||||
|
let resp = handle(
|
||||||
|
&mut node,
|
||||||
|
Request::Observe {
|
||||||
|
topic: hex_encode(&topic()),
|
||||||
|
payload: hex_encode(&payload()),
|
||||||
|
to: Some(hex_encode(&to)),
|
||||||
|
lifetime_ms: Some(60_000),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
let id = match resp {
|
||||||
|
Response::Observe { id } => id,
|
||||||
|
other => panic!("expected Observe, got {other:?}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
match handle(&mut node, Request::List, None, 0) {
|
||||||
|
Response::List {
|
||||||
|
observations,
|
||||||
|
truncated,
|
||||||
|
} => {
|
||||||
|
assert!(!truncated);
|
||||||
|
assert_eq!(observations.len(), 1);
|
||||||
|
let o = &observations[0];
|
||||||
|
assert_eq!(o.id, id);
|
||||||
|
assert_eq!(o.observer, hex_encode(&[3u8; 16]));
|
||||||
|
assert_eq!(o.topic, hex_encode(&topic()));
|
||||||
|
assert_eq!(o.payload, hex_encode(&payload()));
|
||||||
|
assert_eq!(o.to.as_deref(), Some(hex_encode(&to).as_str()));
|
||||||
|
assert_eq!(o.lifetime_ms, Some(60_000));
|
||||||
|
}
|
||||||
|
other => panic!("expected List, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exactly one of `to` / `lifetime_ms` set is an error, in either direction.
|
||||||
|
#[test]
|
||||||
|
fn observe_half_addressed_is_error() {
|
||||||
|
let dir = unique_dir("half");
|
||||||
|
let mut node = build_node([3; 16], &dir);
|
||||||
|
|
||||||
|
for (to, lifetime) in [(Some(hex_encode(&[4u8; 16])), None), (None, Some(1u64))] {
|
||||||
|
let resp = handle(
|
||||||
|
&mut node,
|
||||||
|
Request::Observe {
|
||||||
|
topic: hex_encode(&topic()),
|
||||||
|
payload: hex_encode(&payload()),
|
||||||
|
to,
|
||||||
|
lifetime_ms: lifetime,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
match resp {
|
||||||
|
Response::Error { message } => {
|
||||||
|
assert!(
|
||||||
|
message.contains("must be set together"),
|
||||||
|
"unexpected message: {message}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
other => panic!("expected Error, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `List` returns every stored observation (broadcast and addressed) with
|
||||||
|
/// correct fields.
|
||||||
|
#[test]
|
||||||
|
fn list_returns_all_observations() {
|
||||||
|
let dir = unique_dir("listall");
|
||||||
|
let mut node = build_node([3; 16], &dir);
|
||||||
|
|
||||||
|
let _ = handle(
|
||||||
|
&mut node,
|
||||||
|
Request::Observe {
|
||||||
|
topic: hex_encode(&topic()),
|
||||||
|
payload: hex_encode(b"broadcast"),
|
||||||
|
to: None,
|
||||||
|
lifetime_ms: None,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
let _ = handle(
|
||||||
|
&mut node,
|
||||||
|
Request::Observe {
|
||||||
|
topic: hex_encode(&topic()),
|
||||||
|
payload: hex_encode(b"addressed"),
|
||||||
|
to: Some(hex_encode(&[4u8; 16])),
|
||||||
|
lifetime_ms: Some(1_000),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
match handle(&mut node, Request::List, None, 0) {
|
||||||
|
Response::List {
|
||||||
|
observations,
|
||||||
|
truncated,
|
||||||
|
} => {
|
||||||
|
assert!(!truncated);
|
||||||
|
assert_eq!(observations.len(), 2, "both observations listed");
|
||||||
|
let broadcast = observations
|
||||||
|
.iter()
|
||||||
|
.find(|o| o.payload == hex_encode(b"broadcast"))
|
||||||
|
.expect("broadcast present");
|
||||||
|
assert_eq!(broadcast.to, None);
|
||||||
|
assert_eq!(broadcast.lifetime_ms, None);
|
||||||
|
assert_eq!(broadcast.topic, hex_encode(&topic()));
|
||||||
|
assert_eq!(broadcast.observer, hex_encode(&[3u8; 16]));
|
||||||
|
let addressed = observations
|
||||||
|
.iter()
|
||||||
|
.find(|o| o.payload == hex_encode(b"addressed"))
|
||||||
|
.expect("addressed present");
|
||||||
|
assert_eq!(
|
||||||
|
addressed.to.as_deref(),
|
||||||
|
Some(hex_encode(&[4u8; 16]).as_str())
|
||||||
|
);
|
||||||
|
assert_eq!(addressed.lifetime_ms, Some(1_000));
|
||||||
|
}
|
||||||
|
other => panic!("expected List, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `List` caps at `LIST_CAP` entries and reports truncation when more exist.
|
||||||
|
#[test]
|
||||||
|
fn list_caps_at_list_cap() {
|
||||||
|
let dir = unique_dir("listcap");
|
||||||
|
let mut node = build_node([3; 16], &dir);
|
||||||
|
|
||||||
|
for _ in 0..(tines::LIST_CAP + 1) {
|
||||||
|
let resp = handle(
|
||||||
|
&mut node,
|
||||||
|
Request::Observe {
|
||||||
|
topic: hex_encode(&topic()),
|
||||||
|
payload: hex_encode(b"x"),
|
||||||
|
to: None,
|
||||||
|
lifetime_ms: None,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
assert!(matches!(resp, Response::Observe { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
match handle(&mut node, Request::List, None, 0) {
|
||||||
|
Response::List {
|
||||||
|
observations,
|
||||||
|
truncated,
|
||||||
|
} => {
|
||||||
|
assert_eq!(observations.len(), tines::LIST_CAP);
|
||||||
|
assert!(truncated, "more observations exist than fit");
|
||||||
|
}
|
||||||
|
other => panic!("expected List, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The transport's first `Contact` surfaces in `Events.contacts` as
|
||||||
|
/// `joined: true`, alongside an empty `events` (no deliveries yet).
|
||||||
|
#[test]
|
||||||
|
fn events_include_contacts() {
|
||||||
|
let dir = unique_dir("contacts");
|
||||||
|
let socket = dir.join("ipc.sock");
|
||||||
|
let peer = [7; 16];
|
||||||
|
let mut node = build_node(peer, &dir);
|
||||||
|
node.poll(0); // the mock's first poll surfaces the Contact
|
||||||
|
|
||||||
|
let serve_path = socket.clone();
|
||||||
|
let _handle =
|
||||||
|
thread::spawn(move || serve(node, serve_path, HostClock::start(), 20, WINDOW_MS).unwrap());
|
||||||
|
let mut stream = connect(&socket);
|
||||||
|
|
||||||
|
rpc(&mut stream, &Request::Events);
|
||||||
|
match read_response(&mut stream) {
|
||||||
|
Response::Events {
|
||||||
|
events,
|
||||||
|
contacts,
|
||||||
|
drops,
|
||||||
|
} => {
|
||||||
|
assert!(events.is_empty(), "no deliveries before any ack");
|
||||||
|
assert_eq!(drops.len(), 0, "nothing was dropped");
|
||||||
|
assert_eq!(contacts.len(), 1, "one contact surfaced");
|
||||||
|
assert_eq!(contacts[0].peer, hex_encode(&peer));
|
||||||
|
assert!(contacts[0].joined);
|
||||||
|
}
|
||||||
|
other => panic!("expected Events, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Lost` surfaces as `joined: false`, and a garbage frame surfaces in
|
||||||
|
/// `drops` with its reason (while the stderr line stays in the service).
|
||||||
|
#[test]
|
||||||
|
fn events_include_lost_and_drops() {
|
||||||
|
let dir = unique_dir("lost");
|
||||||
|
let peer = [7; 16];
|
||||||
|
let mut node = build_node(peer, &dir);
|
||||||
|
|
||||||
|
node.poll(0); // Contact -> NodeEvent::Contact
|
||||||
|
node.transport_mut().pending.push_back(Event::Lost(peer));
|
||||||
|
node.poll(0); // Lost -> NodeEvent::Lost
|
||||||
|
node.transport_mut().pending.push_back(Event::Frame(
|
||||||
|
peer,
|
||||||
|
FrameBuf::from_slice(&[0xff; 8]).expect("small garbage frame fits"),
|
||||||
|
));
|
||||||
|
node.poll(0); // garbage -> counted drop
|
||||||
|
|
||||||
|
match handle(&mut node, Request::Events, None, 0) {
|
||||||
|
Response::Events {
|
||||||
|
events,
|
||||||
|
contacts,
|
||||||
|
drops,
|
||||||
|
} => {
|
||||||
|
assert!(events.is_empty());
|
||||||
|
assert_eq!(contacts.len(), 2, "contact then loss");
|
||||||
|
assert!(contacts[0].joined);
|
||||||
|
assert_eq!(contacts[0].peer, hex_encode(&peer));
|
||||||
|
assert!(!contacts[1].joined, "the lost contact is closed");
|
||||||
|
assert_eq!(contacts[1].peer, hex_encode(&peer));
|
||||||
|
|
||||||
|
assert_eq!(drops.len(), 1, "one counted drop");
|
||||||
|
assert_eq!(drops[0].peer.as_deref(), Some(hex_encode(&peer).as_str()));
|
||||||
|
assert!(
|
||||||
|
drops[0].reason.contains("VersionMismatch"),
|
||||||
|
"the drop names its reason: {}",
|
||||||
|
drops[0].reason
|
||||||
|
);
|
||||||
|
assert_eq!(drops[0].count, 1);
|
||||||
|
}
|
||||||
|
other => panic!("expected Events, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The existing `events` field still reports confirmed deliveries exactly as
|
||||||
|
/// before the widening (the mock auto-ack path).
|
||||||
|
#[test]
|
||||||
|
fn existing_events_field_unchanged() {
|
||||||
|
let dir = unique_dir("unchanged");
|
||||||
|
let socket = dir.join("ipc.sock");
|
||||||
|
let peer = [7; 16];
|
||||||
|
let mut node = build_node(peer, &dir);
|
||||||
|
|
||||||
|
let x = node.observe(&topic(), &payload(), None, 0).unwrap();
|
||||||
|
node.poll(0);
|
||||||
|
node.poll(0);
|
||||||
|
node.poll(0);
|
||||||
|
|
||||||
|
let serve_path = socket.clone();
|
||||||
|
let _handle =
|
||||||
|
thread::spawn(move || serve(node, serve_path, HostClock::start(), 20, WINDOW_MS).unwrap());
|
||||||
|
let mut stream = connect(&socket);
|
||||||
|
|
||||||
|
rpc(&mut stream, &Request::Events);
|
||||||
|
match read_response(&mut stream) {
|
||||||
|
Response::Events { events, .. } => {
|
||||||
|
assert_eq!(events.len(), 1, "one confirmed delivery");
|
||||||
|
assert_eq!(events[0].id, hex_encode(&x));
|
||||||
|
assert_eq!(events[0].peer, hex_encode(&peer));
|
||||||
|
}
|
||||||
|
other => panic!("expected Events, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user