refactor(core): narrow the 1.0 public API
This commit is contained in:
Generated
+1
@@ -1465,6 +1465,7 @@ name = "tines"
|
|||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heapless 0.8.0",
|
"heapless 0.8.0",
|
||||||
|
"postcard",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tines-bt",
|
"tines-bt",
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ use bluer::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use tines_core::consts::{MAX_FRAME, MAX_PEERS};
|
use tines_core::consts::{MAX_FRAME, MAX_PEERS};
|
||||||
|
use tines_core::frame_sender;
|
||||||
use tines_core::observation::NodeId;
|
use tines_core::observation::NodeId;
|
||||||
use tines_core::transport::{Event, FrameBuf, SendError, Transport};
|
use tines_core::transport::{Event, FrameBuf, SendError, Transport};
|
||||||
use tines_core::wire::frame_sender;
|
|
||||||
|
|
||||||
/// The radio I/O seam for [`BtTransport`].
|
/// The radio I/O seam for [`BtTransport`].
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ use crate::observation::ObservationId;
|
|||||||
|
|
||||||
/// A fixed-size salted Bloom filter keyed by `ObservationId`.
|
/// A fixed-size salted Bloom filter keyed by `ObservationId`.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct Bloom {
|
pub(crate) struct Bloom {
|
||||||
pub salt: u32,
|
pub(crate) salt: u32,
|
||||||
bits: [u8; consts::SUMMARY_BYTES],
|
bits: [u8; consts::SUMMARY_BYTES],
|
||||||
/// Bytes in use: a power of two in `1..=SUMMARY_BYTES`.
|
/// Bytes in use: a power of two in `1..=SUMMARY_BYTES`.
|
||||||
len: usize,
|
len: usize,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
/// Wire envelope version byte prepended to every serialized `Frame`.
|
/// Wire envelope version byte prepended to every serialized `Frame`.
|
||||||
///
|
///
|
||||||
/// The first byte of every frame on the wire; [`crate::wire::Frame::decode`]
|
/// The first byte of every frame on the wire; `crate::wire::Frame::decode`
|
||||||
/// rejects any frame whose leading byte differs from this
|
/// rejects any frame whose leading byte differs from this
|
||||||
/// (`WireError::VersionMismatch`), and [`crate::wire::frame_sender`] skips it.
|
/// (`WireError::VersionMismatch`), and [`crate::wire::frame_sender`] skips it.
|
||||||
/// `4` is the first value in the `4..=0x7F` range that fits a single-byte
|
/// `4` is the first value in the `4..=0x7F` range that fits a single-byte
|
||||||
@@ -42,7 +42,7 @@ pub const REASSEMBLY_TIMEOUT_MS: u64 = 5000;
|
|||||||
pub const SEND_QUEUE_CAP: usize = 256;
|
pub const SEND_QUEUE_CAP: usize = 256;
|
||||||
|
|
||||||
/// Minimum usable UDP frame size in bytes (the BLE floor below which a
|
/// Minimum usable UDP frame size in bytes (the BLE floor below which a
|
||||||
/// fragment header leaves no room for payload). [`crate::frag::fragment`] gives
|
/// fragment header leaves no room for payload). `crate::frag::fragment` gives
|
||||||
/// up cleanly with `WireError::TooLarge` for any `mtu` below this rather than
|
/// up cleanly with `WireError::TooLarge` for any `mtu` below this rather than
|
||||||
/// emitting a truncated frame; the MTU sweep runs from here to [`MAX_FRAME`].
|
/// emitting a truncated frame; the MTU sweep runs from here to [`MAX_FRAME`].
|
||||||
pub const MIN_MTU: usize = 23;
|
pub const MIN_MTU: usize = 23;
|
||||||
|
|||||||
+17
-13
@@ -1,6 +1,6 @@
|
|||||||
//! Exchange state machine for tines-core.
|
//! Exchange state machine for tines-core.
|
||||||
//!
|
//!
|
||||||
//! [`Exchange`] tracks the peers this node knows about and drives the
|
//! `Exchange` tracks the peers this node knows about and drives the
|
||||||
//! content-defined gossip protocol: acknowledging received observations,
|
//! content-defined gossip protocol: acknowledging received observations,
|
||||||
//! summarizing what we hold, requesting what peers are missing, and delivering
|
//! summarizing what we hold, requesting what peers are missing, and delivering
|
||||||
//! observations (fragmenting them across the wire when they exceed the MTU).
|
//! observations (fragmenting them across the wire when they exceed the MTU).
|
||||||
@@ -13,8 +13,10 @@
|
|||||||
|
|
||||||
use crate::consts;
|
use crate::consts;
|
||||||
use crate::{
|
use crate::{
|
||||||
Bloom, Event, Frame, NodeId, Observation, ObservationId, Reassembler, SendError, Store, Tick,
|
bloom::Bloom,
|
||||||
WireError,
|
frag::Reassembler,
|
||||||
|
wire::{Frame, WireError},
|
||||||
|
Event, NodeId, Observation, ObservationId, SendError, Store, Tick,
|
||||||
};
|
};
|
||||||
use heapless::Vec;
|
use heapless::Vec;
|
||||||
|
|
||||||
@@ -31,7 +33,7 @@ const SUMMARY_OVERHEAD: usize = 25;
|
|||||||
/// MTU changed). The stream is then abandoned and restarted rather than
|
/// MTU changed). The stream is then abandoned and restarted rather than
|
||||||
/// sending bytes the receiver cannot reassemble.
|
/// sending bytes the receiver cannot reassemble.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub struct InFlight {
|
pub(crate) struct InFlight {
|
||||||
/// The observation being fragmented.
|
/// The observation being fragmented.
|
||||||
pub id: ObservationId,
|
pub id: ObservationId,
|
||||||
/// Index of the next fragment to send.
|
/// Index of the next fragment to send.
|
||||||
@@ -45,8 +47,8 @@ pub struct InFlight {
|
|||||||
pub len: usize,
|
pub len: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Why the [`Exchange`] dropped an inbound frame it could not use; reported
|
/// Why the `Exchange` dropped an inbound frame it could not use; reported
|
||||||
/// via [`NodeEvent::Dropped`]. A peer's [`Exchange`] rejects expired addressed
|
/// via [`NodeEvent::Dropped`]. A peer's `Exchange` rejects expired addressed
|
||||||
/// observations by policy — those are *not* counted as drops.
|
/// observations by policy — those are *not* counted as drops.
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
@@ -57,7 +59,7 @@ pub enum DropReason {
|
|||||||
/// The version byte read off the wire.
|
/// The version byte read off the wire.
|
||||||
version: u8,
|
version: u8,
|
||||||
},
|
},
|
||||||
/// The frame was not a valid [`Frame`] (truncated, garbage, or empty).
|
/// The frame was not a valid `Frame` (truncated, garbage, or empty).
|
||||||
Malformed,
|
Malformed,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +93,7 @@ pub enum NodeEvent<A> {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The [`Exchange`]'s counted-drop table: up to [`consts::DROP_TABLE_CAP`]
|
/// The `Exchange`'s counted-drop table: up to [`consts::DROP_TABLE_CAP`]
|
||||||
/// distinct `(addr, DropReason)` pairs, each with a saturating count, plus a
|
/// distinct `(addr, DropReason)` pairs, each with a saturating count, plus a
|
||||||
/// single overflow counter. Counters are drained — and reset — through
|
/// single overflow counter. Counters are drained — and reset — through
|
||||||
/// [`Exchange::drain_events`].
|
/// [`Exchange::drain_events`].
|
||||||
@@ -163,8 +165,8 @@ impl<A: Clone + PartialEq> DropTable<A> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A single peer the [`Exchange`] is tracking.
|
/// A single peer the `Exchange` is tracking.
|
||||||
pub struct Peer<A> {
|
pub(crate) struct Peer<A> {
|
||||||
/// Wire address of the peer.
|
/// Wire address of the peer.
|
||||||
pub addr: A,
|
pub addr: A,
|
||||||
/// Node id learned from the first frame received from this peer.
|
/// Node id learned from the first frame received from this peer.
|
||||||
@@ -266,7 +268,7 @@ struct UndoAck<A> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The exchange state machine, tracking up to [`consts::MAX_PEERS`] peers.
|
/// The exchange state machine, tracking up to [`consts::MAX_PEERS`] peers.
|
||||||
pub struct Exchange<A> {
|
pub(crate) struct Exchange<A> {
|
||||||
me: NodeId,
|
me: NodeId,
|
||||||
salt: u32,
|
salt: u32,
|
||||||
peers: Vec<Peer<A>, { consts::MAX_PEERS }>,
|
peers: Vec<Peer<A>, { consts::MAX_PEERS }>,
|
||||||
@@ -763,12 +765,14 @@ impl<A: Clone + PartialEq + core::fmt::Debug> Exchange<A> {
|
|||||||
Some((addr, n))
|
Some((addr, n))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only reached from alloc-gated tests, so unused under --no-default-features.
|
||||||
|
#[allow(dead_code)]
|
||||||
/// Returns the current peers.
|
/// Returns the current peers.
|
||||||
pub fn peers(&self) -> &[Peer<A>] {
|
pub fn peers(&self) -> &[Peer<A>] {
|
||||||
&self.peers
|
&self.peers
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`Exchange`]s emit [`NodeEvent`]s — confirmed deliveries plus counted
|
/// `Exchange`s emit [`NodeEvent`]s — confirmed deliveries plus counted
|
||||||
/// drops — through `Exchange::drain_events`; `tines` surfaces them to its
|
/// drops — through `Exchange::drain_events`; `tines` surfaces them to its
|
||||||
/// transport, so the drain method is unused inside `tines-core` itself.
|
/// transport, so the drain method is unused inside `tines-core` itself.
|
||||||
///
|
///
|
||||||
@@ -3103,7 +3107,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A frame that passes the version gate (byte 0 is WIRE_VERSION) but whose
|
/// A frame that passes the version gate (byte 0 is WIRE_VERSION) but whose
|
||||||
/// body isn't a valid postcard [`Frame`] is a counted malformed drop.
|
/// body isn't a valid postcard `Frame` is a counted malformed drop.
|
||||||
#[test]
|
#[test]
|
||||||
fn malformed_is_counted() {
|
fn malformed_is_counted() {
|
||||||
let mut store = MemStore::new();
|
let mut store = MemStore::new();
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
//! Fragmentation and reassembly of `Observation`s over a byte-oriented link.
|
//! Fragmentation and reassembly of `Observation`s over a byte-oriented link.
|
||||||
|
|
||||||
use crate::consts;
|
use crate::consts;
|
||||||
use crate::{Frame, NodeId, Observation, ObservationId, Tick, WireError};
|
use crate::{
|
||||||
|
wire::{Frame, WireError},
|
||||||
|
NodeId, Observation, ObservationId, Tick,
|
||||||
|
};
|
||||||
use heapless::Vec;
|
use heapless::Vec;
|
||||||
|
|
||||||
/// Header allowance per fragment (bytes), pinned.
|
/// Header allowance per fragment (bytes), pinned.
|
||||||
pub const FRAG_OVERHEAD: usize = 65;
|
pub(crate) const FRAG_OVERHEAD: usize = 65;
|
||||||
|
|
||||||
/// The most fragments one observation may be split into: the reassembler
|
/// The most fragments one observation may be split into: the reassembler
|
||||||
/// tracks received indices in a `u64` bitset.
|
/// tracks received indices in a `u64` bitset.
|
||||||
@@ -27,9 +30,11 @@ pub(crate) fn plan(len: usize, mtu: usize) -> Option<(usize, usize)> {
|
|||||||
Some((chunk, count))
|
Some((chunk, count))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only reached from alloc-gated tests, so unused under --no-default-features.
|
||||||
|
#[allow(dead_code)]
|
||||||
/// Encodes `obs` as one Obs frame if it fits in `mtu`, else as N Frag frames.
|
/// Encodes `obs` as one Obs frame if it fits in `mtu`, else as N Frag frames.
|
||||||
/// Returns the number of frames; writes each via `emit(frame_bytes)`.
|
/// Returns the number of frames; writes each via `emit(frame_bytes)`.
|
||||||
pub fn fragment(
|
pub(crate) fn fragment(
|
||||||
from: NodeId,
|
from: NodeId,
|
||||||
obs: &Observation,
|
obs: &Observation,
|
||||||
mtu: usize,
|
mtu: usize,
|
||||||
@@ -74,7 +79,7 @@ pub fn fragment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A single-slot reassembler for inbound Frag streams.
|
/// A single-slot reassembler for inbound Frag streams.
|
||||||
pub struct Reassembler {
|
pub(crate) struct Reassembler {
|
||||||
id: Option<ObservationId>,
|
id: Option<ObservationId>,
|
||||||
count: u8,
|
count: u8,
|
||||||
received: u64,
|
received: u64,
|
||||||
|
|||||||
@@ -5,29 +5,27 @@
|
|||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
pub mod belief;
|
pub mod belief;
|
||||||
pub mod bloom;
|
mod bloom;
|
||||||
pub mod consts;
|
pub mod consts;
|
||||||
pub mod exchange;
|
mod exchange;
|
||||||
pub mod frag;
|
mod frag;
|
||||||
#[cfg(feature = "alloc")]
|
#[cfg(feature = "alloc")]
|
||||||
pub mod mem_store;
|
pub mod mem_store;
|
||||||
pub mod node;
|
pub mod node;
|
||||||
pub mod observation;
|
pub mod observation;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
pub mod transport;
|
pub mod transport;
|
||||||
pub mod wire;
|
mod wire;
|
||||||
|
|
||||||
pub use belief::{BeliefPolicy, LatestWins, SelfFirst};
|
pub use belief::{BeliefPolicy, LatestWins, SelfFirst};
|
||||||
pub use bloom::Bloom;
|
pub use exchange::{DropReason, NodeEvent};
|
||||||
pub use exchange::{DropReason, Exchange, NodeEvent, Peer};
|
|
||||||
pub use frag::{fragment, Reassembler};
|
|
||||||
pub use node::{Node, ObserveError};
|
pub use node::{Node, ObserveError};
|
||||||
pub use observation::{
|
pub use observation::{
|
||||||
Dest, NodeId, Observation, ObservationId, Payload, SigAlgo, Signature, Tick, Topic,
|
Dest, NodeId, Observation, ObservationId, Payload, SigAlgo, Signature, Tick, Topic,
|
||||||
};
|
};
|
||||||
pub use store::{Store, StoreError, TrimPolicy};
|
pub use store::{Store, StoreError, TrimPolicy};
|
||||||
pub use transport::{Event, FrameBuf, SendError, Transport};
|
pub use transport::{Event, FrameBuf, SendError, Transport};
|
||||||
pub use wire::{Frame, WireError};
|
pub use wire::frame_sender;
|
||||||
|
|
||||||
#[cfg(feature = "alloc")]
|
#[cfg(feature = "alloc")]
|
||||||
pub use mem_store::MemStore;
|
pub use mem_store::MemStore;
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
//! Node facade for tines-core.
|
//! Node facade for tines-core.
|
||||||
//!
|
//!
|
||||||
//! [`Node`] is the single entry point that ties the [`Store`], the
|
//! [`Node`] is the single entry point that ties the [`Store`], the
|
||||||
//! [`Exchange`] state machine, and a [`Transport`] together behind the two
|
//! `Exchange` state machine, and a [`Transport`] together behind the two
|
||||||
//! operations an application needs: [`Node::observe`] to record an observation,
|
//! operations an application needs: [`Node::observe`] to record an observation,
|
||||||
//! and [`Node::poll`] to drive the exchange.
|
//! and [`Node::poll`] to drive the exchange.
|
||||||
//!
|
//!
|
||||||
//! The facade never touches the heap: its only owned buffer, `out`, is a fixed
|
//! The facade never touches the heap: its only owned buffer, `out`, is a fixed
|
||||||
//! `[u8; MAX_FRAME]`. It also panics on nothing — oversized topics/payloads
|
//! `[u8; MAX_FRAME]`. It also panics on nothing — oversized topics/payloads
|
||||||
//! become [`ObserveError::TooLarge`] rather than truncating, and malformed
|
//! become [`ObserveError::TooLarge`] rather than truncating, and malformed
|
||||||
//! transport frames are dropped by the [`Exchange`].
|
//! transport frames are dropped by the `Exchange`.
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
consts, BeliefPolicy, Dest, Exchange, NodeEvent, NodeId, Observation, ObservationId, Payload,
|
consts, exchange::Exchange, BeliefPolicy, Dest, NodeEvent, NodeId, Observation, ObservationId,
|
||||||
Store, StoreError, Tick, Topic, Transport, TrimPolicy,
|
Payload, Store, StoreError, Tick, Topic, Transport, TrimPolicy,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Errors returned by [`Node::observe`].
|
/// Errors returned by [`Node::observe`].
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum ObserveError {
|
pub enum ObserveError {
|
||||||
/// The topic or payload exceeded `MAX_TOPIC`/`MAX_PAYLOAD`. The
|
/// The topic or payload exceeded `MAX_TOPIC`/`MAX_PAYLOAD`. The
|
||||||
@@ -105,7 +106,7 @@ impl<S: Store, T: Transport> Node<S, T> {
|
|||||||
///
|
///
|
||||||
/// The resulting observation has `dest = Some(Dest { to, lifetime_ms })`,
|
/// The resulting observation has `dest = Some(Dest { to, lifetime_ms })`,
|
||||||
/// so the exchange will deliver it only
|
/// so the exchange will deliver it only
|
||||||
/// to `to` (see [`Exchange`]). Returns the observation's content-derived
|
/// to `to` (see `Exchange`). Returns the observation's content-derived
|
||||||
/// id. An oversized topic or payload is rejected with
|
/// id. An oversized topic or payload is rejected with
|
||||||
/// [`ObserveError::TooLarge`] rather than truncated; a store rejection is
|
/// [`ObserveError::TooLarge`] rather than truncated; a store rejection is
|
||||||
/// surfaced as [`ObserveError::Store`]. Duplicate ids are accepted (the id
|
/// surfaced as [`ObserveError::Store`]. Duplicate ids are accepted (the id
|
||||||
@@ -143,7 +144,7 @@ impl<S: Store, T: Transport> Node<S, T> {
|
|||||||
/// Returns the number of *new* observations stored this round. If the
|
/// Returns the number of *new* observations stored this round. If the
|
||||||
/// transport refuses the frame with `Busy` or `Unreachable`, the exchange
|
/// transport refuses the frame with `Busy` or `Unreachable`, the exchange
|
||||||
/// rolls back and offers the same frame on a later poll; a `TooLarge`
|
/// rolls back and offers the same frame on a later poll; a `TooLarge`
|
||||||
/// frame is dropped (see [`Exchange::on_send_error`]).
|
/// frame is dropped (see `Exchange::on_send_error`).
|
||||||
pub fn poll(&mut self, now: Tick) -> usize {
|
pub fn poll(&mut self, now: Tick) -> usize {
|
||||||
let mut stored = 0usize;
|
let mut stored = 0usize;
|
||||||
while let Some(ev) = self.transport.poll() {
|
while let Some(ev) = self.transport.poll() {
|
||||||
@@ -165,7 +166,7 @@ impl<S: Store, T: Transport> Node<S, T> {
|
|||||||
stored
|
stored
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drains the [`NodeEvent`]s the [`Exchange`] has collected: confirmed
|
/// Drains the [`NodeEvent`]s the `Exchange` has collected: confirmed
|
||||||
/// deliveries first (oldest-first), then one [`NodeEvent::Dropped`] per
|
/// deliveries first (oldest-first), then one [`NodeEvent::Dropped`] per
|
||||||
/// counted drop.
|
/// counted drop.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ pub struct Dest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// An observation published by a node, with content-derived identity.
|
/// An observation published by a node, with content-derived identity.
|
||||||
|
///
|
||||||
|
/// `#[non_exhaustive]`: a future 1.x may add fields, so build it via the
|
||||||
|
/// constructors on [`Observation`] rather than a struct literal outside
|
||||||
|
/// `tines-core`.
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct Observation {
|
pub struct Observation {
|
||||||
pub observer: NodeId,
|
pub observer: NodeId,
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ use crate::observation::{Observation, ObservationId, Tick};
|
|||||||
/// Rules are applied in the order listed here: per-topic retention, then a
|
/// Rules are applied in the order listed here: per-topic retention, then a
|
||||||
/// global age cutoff, then a global count cap. Any field left as `None` is
|
/// global age cutoff, then a global count cap. Any field left as `None` is
|
||||||
/// ignored.
|
/// ignored.
|
||||||
|
///
|
||||||
|
/// `#[non_exhaustive]`: build via [`TrimPolicy::default`] and set fields, so a
|
||||||
|
/// future 1.x can add policy knobs without a breaking change.
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct TrimPolicy {
|
pub struct TrimPolicy {
|
||||||
/// Keep the `N` youngest (lowest current age) observations per exact topic.
|
/// Keep the `N` youngest (lowest current age) observations per exact topic.
|
||||||
@@ -24,6 +28,7 @@ pub struct TrimPolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Errors returned by [`Store`] operations.
|
/// Errors returned by [`Store`] operations.
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum StoreError {
|
pub enum StoreError {
|
||||||
/// The store is full and cannot accept new observations.
|
/// The store is full and cannot accept new observations.
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ pub type FrameBuf = heapless::Vec<u8, { consts::MAX_FRAME }>;
|
|||||||
/// `Contact`/`Lost`; the `large_enum_variant` lint is intentionally suppressed
|
/// `Contact`/`Lost`; the `large_enum_variant` lint is intentionally suppressed
|
||||||
/// to keep `FrameBuf` off the heap in the `no_std` core.
|
/// to keep `FrameBuf` off the heap in the `no_std` core.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum Event<A> {
|
pub enum Event<A> {
|
||||||
/// A peer became reachable.
|
/// A peer became reachable.
|
||||||
@@ -26,6 +27,7 @@ pub enum Event<A> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Reasons a `Transport::send` call can fail.
|
/// Reasons a `Transport::send` call can fail.
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum SendError {
|
pub enum SendError {
|
||||||
/// The transport is busy with an in-flight send and cannot accept another.
|
/// The transport is busy with an in-flight send and cannot accept another.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
/// large, so the informative `large_enum_variant` lint is suppressed here.
|
/// large, so the informative `large_enum_variant` lint is suppressed here.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum Frame {
|
pub(crate) enum Frame {
|
||||||
Summary {
|
Summary {
|
||||||
from: NodeId,
|
from: NodeId,
|
||||||
salt: u32,
|
salt: u32,
|
||||||
@@ -85,7 +85,7 @@ impl Frame {
|
|||||||
|
|
||||||
/// Peek the [`NodeId`] a wire frame was sent by, without fully decoding it.
|
/// Peek the [`NodeId`] a wire frame was sent by, without fully decoding it.
|
||||||
///
|
///
|
||||||
/// A [`Frame`] envelope is a `WIRE_VERSION` byte, then an externally-tagged
|
/// A `Frame` envelope is a `WIRE_VERSION` byte, then an externally-tagged
|
||||||
/// postcard enum: `bytes[0]` is the version, `bytes[1]` the variant tag, and
|
/// postcard enum: `bytes[0]` is the version, `bytes[1]` the variant tag, and
|
||||||
/// `from` (the first field) occupies the 16 bytes at `bytes[2..18]`. This skips
|
/// `from` (the first field) occupies the 16 bytes at `bytes[2..18]`. This skips
|
||||||
/// the two leading bytes, reads those 16 bytes, and returns `None` for anything
|
/// the two leading bytes, reads those 16 bytes, and returns `None` for anything
|
||||||
@@ -114,7 +114,7 @@ pub fn frame_sender(bytes: &[u8]) -> Option<NodeId> {
|
|||||||
|
|
||||||
/// Errors produced while (de)serializing frames on the wire.
|
/// Errors produced while (de)serializing frames on the wire.
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum WireError {
|
pub(crate) enum WireError {
|
||||||
/// The output buffer was too small to hold the serialized frame.
|
/// The output buffer was too small to hold the serialized frame.
|
||||||
TooLarge,
|
TooLarge,
|
||||||
/// The input bytes were malformed, truncated, or not a valid `Frame`.
|
/// The input bytes were malformed, truncated, or not a valid `Frame`.
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
//! Compile probe for the tines-core 1.0 public surface (Task 5).
|
||||||
|
//!
|
||||||
|
//! Every `use` path here must keep resolving against `tines_core`. A future
|
||||||
|
//! change that removes any of these items from the public API — or drops a
|
||||||
|
//! crate-root re-export that Task 5 established — fails to build here, which
|
||||||
|
//! is exactly what the probe guards against. CI compiles it on the workspace
|
||||||
|
//! both with and without the default (alloc) features.
|
||||||
|
#![allow(unused_imports)]
|
||||||
|
|
||||||
|
// Re-exported at the crate root by Task 5's `tines_core::lib`.
|
||||||
|
use tines_core::consts::WIRE_VERSION;
|
||||||
|
use tines_core::{
|
||||||
|
belief::{BeliefPolicy, LatestWins, SelfFirst},
|
||||||
|
node::{Node, ObserveError},
|
||||||
|
observation::{
|
||||||
|
Dest, NodeId, Observation, ObservationId, Payload, SigAlgo, Signature, Tick, Topic,
|
||||||
|
},
|
||||||
|
store::{Store, StoreError, TrimPolicy},
|
||||||
|
transport::{Event, FrameBuf, SendError, Transport},
|
||||||
|
DropReason, NodeEvent,
|
||||||
|
};
|
||||||
|
use tines_core::{consts, frame_sender};
|
||||||
|
|
||||||
|
// `MemStore` lives behind the `alloc` feature.
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
use tines_core::MemStore;
|
||||||
@@ -112,8 +112,8 @@ impl SimWorld {
|
|||||||
/// A test seam: it lets tests feed arbitrary or malformed byte strings
|
/// A test seam: it lets tests feed arbitrary or malformed byte strings
|
||||||
/// straight to a node's exchange, which is where the "never panic on
|
/// straight to a node's exchange, which is where the "never panic on
|
||||||
/// untrusted wire input" guarantee lives. The frame is not validated here —
|
/// untrusted wire input" guarantee lives. The frame is not validated here —
|
||||||
/// a malformed frame is dropped by the receiving node's
|
/// a malformed frame is dropped by the receiving
|
||||||
/// [`Exchange`](tines_core::Exchange) rather than stored.
|
/// [`Node`](tines_core::Node) rather than stored.
|
||||||
pub fn inject_frame(&mut self, node: usize, peer: usize, bytes: FrameBuf) {
|
pub fn inject_frame(&mut self, node: usize, peer: usize, bytes: FrameBuf) {
|
||||||
self.inboxes[node].push_back(Event::Frame(peer, bytes));
|
self.inboxes[node].push_back(Event::Frame(peer, bytes));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1016,15 +1016,13 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn obs(observer: [u8; 16], seq: u64, age: u64, at: Option<u64>) -> Observation {
|
fn obs(observer: [u8; 16], seq: u64, age: u64, at: Option<u64>) -> Observation {
|
||||||
Observation {
|
// `Observation` is `#[non_exhaustive]` outside `tines-core` (Task 5);
|
||||||
observer,
|
// build it via the public constructor instead of a struct literal.
|
||||||
seq,
|
let base = Observation::broadcast(observer, seq, b"door/3", b"open").aged(age);
|
||||||
age_ms: age,
|
if let Some(at) = at {
|
||||||
observed_at: at,
|
base.with_observed_at(at)
|
||||||
topic: tines_core::Topic::from_slice(b"door/3").unwrap(),
|
} else {
|
||||||
payload: tines_core::Payload::from_slice(b"open").unwrap(),
|
base
|
||||||
dest: None,
|
|
||||||
sig: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,21 +7,19 @@ use std::io::Write;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
use tines_core::{Observation, Payload, Store, Topic};
|
use tines_core::{Observation, Store};
|
||||||
use tines_store::LogStore;
|
use tines_store::LogStore;
|
||||||
|
|
||||||
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
fn obs(observer: [u8; 16], seq: u64, age: u64, at: Option<u64>) -> Observation {
|
fn obs(observer: [u8; 16], seq: u64, age: u64, at: Option<u64>) -> Observation {
|
||||||
Observation {
|
// `Observation` is `#[non_exhaustive]` outside `tines-core` (Task 5);
|
||||||
observer,
|
// build it via the public constructor instead of a struct literal.
|
||||||
seq,
|
let base = Observation::broadcast(observer, seq, b"door/3", b"open").aged(age);
|
||||||
age_ms: age,
|
if let Some(at) = at {
|
||||||
observed_at: at,
|
base.with_observed_at(at)
|
||||||
topic: Topic::from_slice(b"door/3").unwrap(),
|
} else {
|
||||||
payload: Payload::from_slice(b"open").unwrap(),
|
base
|
||||||
dest: None,
|
|
||||||
sig: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,20 +333,13 @@ mod fleet_fixes_f5 {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use std::os::unix::fs::MetadataExt;
|
use std::os::unix::fs::MetadataExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tines_core::{Observation, Payload, Store, Topic};
|
use tines_core::{Observation, Store};
|
||||||
use tines_store::LogStore;
|
use tines_store::LogStore;
|
||||||
|
|
||||||
fn a_obs(observer: [u8; 16], seq: u64) -> Observation {
|
fn a_obs(observer: [u8; 16], seq: u64) -> Observation {
|
||||||
Observation {
|
// `Observation` is `#[non_exhaustive]` outside `tines-core` (Task 5);
|
||||||
observer,
|
// build it via the public constructor instead of a struct literal.
|
||||||
seq,
|
Observation::broadcast(observer, seq, b"door/3", b"open").with_observed_at(10)
|
||||||
age_ms: 0,
|
|
||||||
observed_at: Some(10),
|
|
||||||
topic: Topic::from_slice(b"door/3").unwrap(),
|
|
||||||
payload: Payload::from_slice(b"open").unwrap(),
|
|
||||||
dest: None,
|
|
||||||
sig: None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Flip a byte in the body of the `n`-th record after the header (0 = Boot),
|
/// Flip a byte in the body of the `n`-th record after the header (0 = Boot),
|
||||||
|
|||||||
@@ -19,3 +19,7 @@ serde_json = "1"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
heapless = "0.8"
|
heapless = "0.8"
|
||||||
|
# Only for the ipc mock peer, which reconstructs wire frames from tines-core's
|
||||||
|
# public `Observation` to classify inbound and synthesize ack/summary frames —
|
||||||
|
# without reaching into the private `Frame` enum.
|
||||||
|
postcard = "1"
|
||||||
|
|||||||
+54
-23
@@ -16,7 +16,8 @@ use heapless::Vec as HeaplessVec;
|
|||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
use tines::{handle, serve, HostClock, Request, Response};
|
use tines::{handle, serve, HostClock, Request, Response};
|
||||||
use tines_core::{
|
use tines_core::{
|
||||||
consts, Event, Frame, FrameBuf, Node, NodeEvent, NodeId, ObservationId, SendError, Transport,
|
consts, Event, FrameBuf, Node, NodeEvent, NodeId, Observation, ObservationId, SendError,
|
||||||
|
Transport,
|
||||||
};
|
};
|
||||||
use tines_store::LogStore;
|
use tines_store::LogStore;
|
||||||
|
|
||||||
@@ -56,27 +57,27 @@ impl Transport for MockTransport {
|
|||||||
|
|
||||||
fn send(&mut self, to: &NodeId, frame: &[u8]) -> Result<(), SendError> {
|
fn send(&mut self, to: &NodeId, frame: &[u8]) -> Result<(), SendError> {
|
||||||
self.sent.push(frame.to_vec());
|
self.sent.push(frame.to_vec());
|
||||||
match Frame::decode(frame) {
|
match frame_kind(frame) {
|
||||||
Ok(Frame::Obs { obs, .. }) => {
|
Some(OBS_TAG) => {
|
||||||
// A well-behaved peer acks each distinct observed broadcast.
|
// A well-behaved peer acks each distinct observed broadcast.
|
||||||
if self.acked.insert(obs.id()) {
|
if let Ok((_, obs)) = postcard::from_bytes::<(NodeId, Observation)>(&frame[2..]) {
|
||||||
let ack = ack_frame(*to, obs.id());
|
if self.acked.insert(obs.id()) {
|
||||||
self.pending.push_back(frame_event(*to, &ack));
|
self.pending.push_back(ack_event(*to, obs.id()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Frame::Summary { salt, .. }) => {
|
Some(SUMMARY_TAG) => {
|
||||||
// Echo back an empty, full-size Summary so the exchange learns
|
// Echo back an empty, full-size Summary so the exchange learns
|
||||||
// the peer holds nothing and broadcasts its diff (which we then
|
// the peer holds nothing and broadcasts its diff (which we then
|
||||||
// ack, firing a NodeEvent::Delivered).
|
// ack, firing a NodeEvent::Delivered).
|
||||||
let raw: [u8; consts::SUMMARY_BYTES] = [0u8; consts::SUMMARY_BYTES];
|
if let Ok((from, salt, _)) = postcard::from_bytes::<(
|
||||||
let bits: HeaplessVec<u8, { consts::SUMMARY_BYTES }> =
|
NodeId,
|
||||||
HeaplessVec::from_slice(&raw).expect("512 fits SUMMARY_BYTES");
|
u32,
|
||||||
let reply = Frame::Summary {
|
HeaplessVec<u8, { consts::SUMMARY_BYTES }>,
|
||||||
from: *to,
|
)>(&frame[2..])
|
||||||
salt,
|
{
|
||||||
bits,
|
self.pending.push_back(summary_event(from, salt));
|
||||||
};
|
}
|
||||||
self.pending.push_back(frame_event(*to, &reply));
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -88,21 +89,51 @@ impl Transport for MockTransport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ack_frame(from: NodeId, id: ObservationId) -> Frame {
|
// Wire-variant tags, mirroring tines-core's private `Frame` enum order
|
||||||
let ids: HeaplessVec<ObservationId, { consts::ACK_IDS_CAP }> =
|
// (Summary=0, Obs=1, Frag=2, Ack=3). tines-core keeps `Frame` internal, so the
|
||||||
HeaplessVec::from_slice(&[id]).expect("one id fits ACK_IDS_CAP");
|
// mock peer re-derives these here to classify inbound frames and synthesize the
|
||||||
Frame::Ack { from, ids }
|
// acks/summaries the exchange expects from a well-behaved peer.
|
||||||
|
const SUMMARY_TAG: u8 = 0;
|
||||||
|
const OBS_TAG: u8 = 1;
|
||||||
|
const ACK_TAG: u8 = 3;
|
||||||
|
|
||||||
|
/// Returns a wire frame's postcard variant tag, or `None` if `frame` is not a
|
||||||
|
/// well-formed tines frame (too short, or a wrong leading version byte).
|
||||||
|
fn frame_kind(frame: &[u8]) -> Option<u8> {
|
||||||
|
(frame.len() >= 2 && frame[0] == consts::WIRE_VERSION).then_some(frame[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn frame_event(from: NodeId, frame: &Frame) -> Event<NodeId> {
|
/// Encode a `Frame`-variant-shaped tuple as wire bytes: the WIRE_VERSION byte,
|
||||||
|
/// the variant tag, then the postcard payload — mirroring tines-core's
|
||||||
|
/// internal `Frame::encode`, using only `tines_core`'s public types.
|
||||||
|
fn encode_frame<T: serde::Serialize>(from: NodeId, tag: u8, payload: &T) -> Event<NodeId> {
|
||||||
let mut buf = [0u8; consts::MAX_FRAME];
|
let mut buf = [0u8; consts::MAX_FRAME];
|
||||||
let n = frame.encode(&mut buf).expect("frame fits MAX_FRAME");
|
buf[0] = consts::WIRE_VERSION;
|
||||||
|
buf[1] = tag;
|
||||||
|
let n = postcard::to_slice(payload, &mut buf[2..])
|
||||||
|
.expect("frame fits MAX_FRAME")
|
||||||
|
.len();
|
||||||
Event::Frame(
|
Event::Frame(
|
||||||
from,
|
from,
|
||||||
FrameBuf::from_slice(&buf[..n]).expect("frame fits FrameBuf"),
|
FrameBuf::from_slice(&buf[..2 + n]).expect("frame fits FrameBuf"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Synthesize the Ack a well-behaved peer sends for one observed id.
|
||||||
|
fn ack_event(from: NodeId, id: ObservationId) -> Event<NodeId> {
|
||||||
|
let ids: HeaplessVec<ObservationId, { consts::ACK_IDS_CAP }> =
|
||||||
|
HeaplessVec::from_slice(&[id]).expect("one id fits ACK_IDS_CAP");
|
||||||
|
encode_frame(from, ACK_TAG, &(from, ids))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synthesize the full-size, all-zero Summary a well-behaved peer echoes for an
|
||||||
|
/// incoming Summary (telling the exchange the peer holds nothing).
|
||||||
|
fn summary_event(from: NodeId, salt: u32) -> Event<NodeId> {
|
||||||
|
let bits: HeaplessVec<u8, { consts::SUMMARY_BYTES }> =
|
||||||
|
HeaplessVec::from_slice(&[0u8; consts::SUMMARY_BYTES]).expect("512 fits SUMMARY_BYTES");
|
||||||
|
encode_frame(from, SUMMARY_TAG, &(from, salt, bits))
|
||||||
|
}
|
||||||
|
|
||||||
fn topic() -> Vec<u8> {
|
fn topic() -> Vec<u8> {
|
||||||
b"tines/test".to_vec()
|
b"tines/test".to_vec()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user