Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
protocol: port Service::wake to sans-IO
Fintan Halpenny committed 10 months ago
commit ca9e9a27961c8d55024877b54c24abfd8c105f43
parent a8dac56ca645fdd98955766f07abe7cdd8980e56
2 files changed +840 -0
modified crates/radicle-protocol/src/lib.rs
@@ -4,5 +4,7 @@ pub mod service;
pub mod wire;
pub mod worker;

+
pub mod tasks;
+

/// Peer-to-peer protocol version.
pub const PROTOCOL_VERSION: u8 = 1;
added crates/radicle-protocol/src/tasks.rs
@@ -0,0 +1,838 @@
+
use localtime::{LocalDuration, LocalTime};
+
use nonempty::{nonempty, NonEmpty};
+

+
/// How often to run the "idle" task.
+
pub const IDLE_INTERVAL: LocalDuration = LocalDuration::from_secs(30);
+
/// How often to run the "gossip" task.
+
pub const GOSSIP_INTERVAL: LocalDuration = LocalDuration::from_secs(6);
+
/// How often to run the "announce" task.
+
pub const ANNOUNCE_INTERVAL: LocalDuration = LocalDuration::from_mins(60);
+
/// How often to run the "sync" task.
+
pub const SYNC_INTERVAL: LocalDuration = LocalDuration::from_secs(60);
+
/// How often to run the "prune" task.
+
pub const PRUNE_INTERVAL: LocalDuration = LocalDuration::from_mins(30);
+

+
/// The [`TaskTimers`] keeps track of the state of different timers and duration
+
/// intervals for when to emit a set of tasks.
+
///
+
/// [`TaskTimers::tick`] checks which intervals have elapsed and produces a
+
/// [`TaskTimerCommand`].
+
///
+
/// The [`TaskTimerCommand`] will provide a set of [`TimerEvent`]s which can be
+
/// used to [`TaskTimers::advance`] the clocks forward.
+
///
+
/// It also provides a set [`TaskEvent`]s which are tasks that the rest of the
+
/// system should execute.
+
pub struct TaskTimers {
+
    last_idle: LocalTime,
+
    last_gossip: LocalTime,
+
    last_sync: LocalTime,
+
    last_announce: LocalTime,
+
    last_prune: LocalTime,
+
    intervals: Intervals,
+
}
+

+
/// Interval durations used in [`TaskTimers`] for deciding when to issue
+
/// [`TaskEvent`]s and [`TimerEvent`]s.
+
///
+
/// The default values for each interval are given by:
+
///   - Idle: 30s
+
///   - Gossip: 6s
+
///   - Sync: 60s
+
///   - Announce: 60m
+
///   - Prune: 30m
+
pub struct Intervals {
+
    pub idle: LocalDuration,
+
    pub gossip: LocalDuration,
+
    pub sync: LocalDuration,
+
    pub announce: LocalDuration,
+
    pub prune: LocalDuration,
+
}
+

+
impl Default for Intervals {
+
    fn default() -> Self {
+
        Self {
+
            idle: IDLE_INTERVAL,
+
            gossip: GOSSIP_INTERVAL,
+
            sync: SYNC_INTERVAL,
+
            announce: ANNOUNCE_INTERVAL,
+
            prune: PRUNE_INTERVAL,
+
        }
+
    }
+
}
+

+
impl Intervals {
+
    /// The set of [`Intervals`] that never elapse.
+
    pub const NEVER: Intervals = Intervals {
+
        idle: LocalDuration::MAX,
+
        gossip: LocalDuration::MAX,
+
        sync: LocalDuration::MAX,
+
        announce: LocalDuration::MAX,
+
        prune: LocalDuration::MAX,
+
    };
+

+
    /// The set of [`Intervals`] that always elapse – as long as at least a
+
    /// millisecond of time passes.
+
    pub const ALWAYS: Intervals = Intervals {
+
        idle: LocalDuration::from_millis(0),
+
        gossip: LocalDuration::from_millis(0),
+
        sync: LocalDuration::from_millis(0),
+
        announce: LocalDuration::from_millis(0),
+
        prune: LocalDuration::from_millis(0),
+
    };
+

+
    /// Set the idle interval – issues tasks based on the node's idleness.
+
    pub fn with_idle(mut self, idle: LocalDuration) -> Self {
+
        self.idle = idle;
+
        self
+
    }
+

+
    /// Set the gossip interval – issues tasks for gossiping to the network.
+
    pub fn with_gossip(mut self, gossip: LocalDuration) -> Self {
+
        self.gossip = gossip;
+
        self
+
    }
+

+
    /// Set the sync interval – issues tasks for syncing with the network.
+
    pub fn with_sync(mut self, sync: LocalDuration) -> Self {
+
        self.sync = sync;
+
        self
+
    }
+

+
    /// Set the announce interval – issues tasks for announcing changes to the
+
    /// network.
+
    pub fn with_announce(mut self, announce: LocalDuration) -> Self {
+
        self.announce = announce;
+
        self
+
    }
+

+
    /// Set the prune interval – issues tasks for cleaning up stale data on the
+
    /// node.
+
    pub fn with_prune(mut self, prune: LocalDuration) -> Self {
+
        self.prune = prune;
+
        self
+
    }
+
}
+

+
/// Task events that are emitted by the [`TaskTimers`].
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+
pub enum TaskEvent {
+
    /// Maintain any configured persistent connections.
+
    MaintainPersistenConnections,
+
    /// Idle tasks.
+
    Idle(Idle),
+
    /// Gossip tasks.
+
    Gossip(Gossip),
+
    /// Synchronization tasks.
+
    Sync(Sync),
+
    /// Announcement tasks.
+
    Announce(Announce),
+
    /// Prune tasks.
+
    Prune(Prune),
+
}
+

+
impl TaskEvent {
+
    /// Produce all [`TaskEvent`]s.
+
    pub fn all() -> NonEmpty<Self> {
+
        let mut events = nonempty![TaskEvent::MaintainPersistenConnections];
+
        events.extend(Idle::all().map(Self::from));
+
        events.extend(Gossip::all().map(Self::from));
+
        events.extend(Sync::all().map(Self::from));
+
        events.extend(Announce::all().map(Self::from));
+
        events.extend(Prune::all().map(Self::from));
+
        events
+
    }
+
}
+

+
/// Events that describe how timer state should change.
+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+
pub enum TimerEvent {
+
    /// Advance the idle clock of [`TaskTimers`].
+
    AdvanceIdle(LocalTime),
+
    /// Advance the gossip clock of [`TaskTimers`].
+
    AdvanceGossip(LocalTime),
+
    /// Advance the sync clock of [`TaskTimers`].
+
    AdvanceSync(LocalTime),
+
    /// Advance the announce clock of [`TaskTimers`].
+
    AdvanceAnnounce(LocalTime),
+
    /// Advance the prune clock of [`TaskTimers`].
+
    AdvancePrune(LocalTime),
+
}
+

+
/// Tasks for the machine to perform while it is idling.
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+
pub enum Idle {
+
    /// Maintain any existing connections.
+
    MaintainConnections,
+
    /// Issue a keep alive message to connections.
+
    KeepAlive,
+
    /// Disconnect any unresponsive peers.
+
    DisconnectUnresponsivePeers,
+
    /// Dequeue any queued fetches.
+
    DequeueFetches,
+
}
+

+
impl Idle {
+
    /// Return all the [`Idle`] variants.
+
    pub fn all() -> NonEmpty<Self> {
+
        nonempty![
+
            Self::MaintainConnections,
+
            Self::KeepAlive,
+
            Self::DisconnectUnresponsivePeers,
+
            Self::DequeueFetches,
+
        ]
+
    }
+
}
+

+
impl From<Idle> for TaskEvent {
+
    fn from(value: Idle) -> Self {
+
        TaskEvent::Idle(value)
+
    }
+
}
+

+
/// Gossip tasks for the machine to execute.
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+
pub enum Gossip {
+
    /// Relay announcements to connections.
+
    RelayAnnouncements,
+
}
+

+
impl Gossip {
+
    /// Return all the [`Gossip`] variants.
+
    pub fn all() -> NonEmpty<Self> {
+
        nonempty![Self::RelayAnnouncements]
+
    }
+
}
+

+
impl From<Gossip> for TaskEvent {
+
    fn from(value: Gossip) -> Self {
+
        TaskEvent::Gossip(value)
+
    }
+
}
+

+
/// Synchronization tasks for the machine to execute.
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+
pub enum Sync {
+
    /// Fetch missing repositories from connections. Missing repositories are
+
    /// repositories that are seeded, but not in storage.
+
    MissingRepositories,
+
}
+

+
impl Sync {
+
    /// Return all the [`Sync`] variants.
+
    pub fn all() -> NonEmpty<Self> {
+
        nonempty![Self::MissingRepositories]
+
    }
+
}
+

+
impl From<Sync> for TaskEvent {
+
    fn from(value: Sync) -> Self {
+
        TaskEvent::Sync(value)
+
    }
+
}
+

+
/// Announcement tasks for the machine to execute.
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+
pub enum Announce {
+
    /// Announce inventory to connections.
+
    Inventory,
+
}
+

+
impl Announce {
+
    /// Return all the [`Announce`] variants.
+
    pub fn all() -> NonEmpty<Self> {
+
        nonempty![Self::Inventory]
+
    }
+
}
+

+
impl From<Announce> for TaskEvent {
+
    fn from(value: Announce) -> Self {
+
        TaskEvent::Announce(value)
+
    }
+
}
+

+
/// Pruning tasks for the machine to execute.
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+
pub enum Prune {
+
    /// Prune any routing entries that are no longer required.
+
    RoutingEntries,
+
    /// Prune any announcements that are no longer required.
+
    Announcements,
+
}
+

+
impl Prune {
+
    /// Return all the [`Prune`] variants.
+
    pub fn all() -> NonEmpty<Self> {
+
        nonempty![Self::RoutingEntries, Self::Announcements]
+
    }
+
}
+

+
impl From<Prune> for TaskEvent {
+
    fn from(value: Prune) -> Self {
+
        TaskEvent::Prune(value)
+
    }
+
}
+

+
/// The result of performing a [`TaskTimers::tick`].
+
pub struct TaskTimerCommand {
+
    /// The set of tasks that should be performed next.
+
    pub tasks: NonEmpty<TaskEvent>,
+
    /// The set of timers that should be used to advance the [`TaskTimers`]
+
    /// ([`TaskTimers::advance`]).
+
    pub timers: Vec<TimerEvent>,
+
}
+

+
impl TaskTimers {
+
    /// Construct the [`Timers`] starting all timers at `now`.
+
    pub fn new(now: LocalTime) -> Self {
+
        Self {
+
            last_idle: now,
+
            last_gossip: now,
+
            last_sync: now,
+
            last_announce: now,
+
            last_prune: now,
+
            intervals: Intervals::default(),
+
        }
+
    }
+

+
    /// Set the [`Intervals`] for the [`Timers`].
+
    pub fn with_intervals(mut self, intervals: Intervals) -> Self {
+
        self.intervals = intervals;
+
        self
+
    }
+

+
    /// Issue [`TaskEvent`]s and [`TimerEvent`]s based on the [`Intervals`] of
+
    /// the timers, and the time passed in `now`.
+
    ///
+
    /// The [`TimerEvent`]s can be used to advance the timers, using
+
    /// [`Timers::advance`].
+
    pub fn tick(&self, now: LocalTime) -> TaskTimerCommand {
+
        let mut tasks = NonEmpty::new(TaskEvent::MaintainPersistenConnections);
+
        let mut timers = Vec::with_capacity(5);
+

+
        if self.has_idle_elapsed(now) {
+
            tasks.extend(Idle::all().map(TaskEvent::from));
+
            timers.push(TimerEvent::AdvanceIdle(now));
+
        }
+

+
        if self.has_gossip_elapsed(now) {
+
            tasks.extend(Gossip::all().map(TaskEvent::from));
+
            timers.push(TimerEvent::AdvanceGossip(now));
+
        }
+

+
        if self.has_sync_elapsed(now) {
+
            tasks.extend(Sync::all().map(TaskEvent::from));
+
            timers.push(TimerEvent::AdvanceSync(now));
+
        }
+

+
        if self.has_announce_elapsed(now) {
+
            tasks.extend(Announce::all().map(TaskEvent::from));
+
            timers.push(TimerEvent::AdvanceAnnounce(now));
+
        }
+

+
        if self.has_prune_elapsed(now) {
+
            tasks.extend(Prune::all().map(TaskEvent::from));
+
            timers.push(TimerEvent::AdvancePrune(now));
+
        }
+

+
        TaskTimerCommand { tasks, timers }
+
    }
+

+
    /// Apply [`TimerEvent`]s to advance their clocks.
+
    pub fn advance(&mut self, events: &[TimerEvent]) {
+
        for event in events {
+
            self.advance_timer(event);
+
        }
+
    }
+

+
    fn advance_timer(&mut self, event: &TimerEvent) {
+
        match event {
+
            TimerEvent::AdvanceIdle(time) => self.last_idle = *time,
+
            TimerEvent::AdvanceGossip(time) => self.last_gossip = *time,
+
            TimerEvent::AdvanceSync(time) => self.last_sync = *time,
+
            TimerEvent::AdvanceAnnounce(time) => self.last_announce = *time,
+
            TimerEvent::AdvancePrune(time) => self.last_prune = *time,
+
        }
+
    }
+

+
    fn has_idle_elapsed(&self, now: LocalTime) -> bool {
+
        now - self.last_idle >= self.intervals.idle
+
    }
+

+
    fn has_gossip_elapsed(&self, now: LocalTime) -> bool {
+
        now - self.last_gossip >= self.intervals.gossip
+
    }
+

+
    fn has_sync_elapsed(&self, now: LocalTime) -> bool {
+
        now - self.last_sync >= self.intervals.sync
+
    }
+

+
    fn has_announce_elapsed(&self, now: LocalTime) -> bool {
+
        now - self.last_announce >= self.intervals.announce
+
    }
+

+
    fn has_prune_elapsed(&self, now: LocalTime) -> bool {
+
        now - self.last_prune >= self.intervals.prune
+
    }
+
}
+

+
#[cfg(test)]
+
mod tests {
+
    use super::*;
+
    use localtime::{LocalDuration, LocalTime};
+
    use nonempty::nonempty;
+

+
    // Helper function to create a test time
+
    fn test_time(secs: u64) -> LocalTime {
+
        LocalTime::from_secs(secs)
+
    }
+

+
    // Helper function to create test duration
+
    fn test_duration(secs: u64) -> LocalDuration {
+
        LocalDuration::from_secs(secs)
+
    }
+

+
    fn assert_tasks(got: &NonEmpty<TaskEvent>, expected: NonEmpty<TaskEvent>) {
+
        assert_eq!(
+
            got.len(),
+
            expected.len(),
+
            "extra task(s) emitted: {:?}",
+
            got
+
        );
+
        for t in expected {
+
            assert!(got.contains(&t), "missing expected task: {t:?}")
+
        }
+
    }
+

+
    fn assert_timers(got: &Vec<TimerEvent>, expected: Vec<TimerEvent>) {
+
        assert_eq!(
+
            got.len(),
+
            expected.len(),
+
            "extra timer(s) emitted: {:?}",
+
            got
+
        );
+
        for t in expected {
+
            assert!(got.contains(&t), "missing expected timer: {t:?}")
+
        }
+
    }
+

+
    fn gen_duration(seed: usize) -> LocalDuration {
+
        LocalDuration::from_secs(radicle::test::arbitrary::gen(seed))
+
    }
+

+
    #[test]
+
    fn test_tick_at_start_time_only_persistent_connections() {
+
        let start_time = test_time(1000);
+
        let timers = TaskTimers::new(start_time).with_intervals(Intervals::NEVER);
+

+
        let command = timers.tick(start_time);
+

+
        assert_eq!(
+
            command.tasks,
+
            nonempty![TaskEvent::MaintainPersistenConnections]
+
        );
+
        assert_eq!(command.timers, vec![]);
+
    }
+

+
    #[test]
+
    fn test_tick_idle_interval_elapsed() {
+
        let start_time = test_time(1000);
+
        let timers = TaskTimers::new(start_time).with_intervals(Intervals {
+
            idle: LocalDuration::from_secs(1),
+
            ..Intervals::NEVER
+
        });
+

+
        // Advance time by exactly the idle interval
+
        let tick_time = start_time + test_duration(2);
+
        let command = timers.tick(tick_time);
+

+
        let mut expected = nonempty![TaskEvent::MaintainPersistenConnections];
+
        expected.append(&mut Idle::all().map(TaskEvent::from).into());
+
        assert_tasks(&command.tasks, expected);
+
        assert_timers(&command.timers, vec![TimerEvent::AdvanceIdle(tick_time)]);
+
    }
+

+
    #[test]
+
    fn test_tick_gossip_only() {
+
        let start_time = test_time(1000);
+
        let timers = TaskTimers::new(start_time).with_intervals(Intervals {
+
            gossip: LocalDuration::from_secs(1),
+
            ..Intervals::NEVER
+
        });
+

+
        let tick_time = start_time + test_duration(2);
+
        let command = timers.tick(tick_time);
+

+
        let mut expected = nonempty![TaskEvent::MaintainPersistenConnections];
+
        expected.append(&mut Gossip::all().map(TaskEvent::from).into());
+
        assert_tasks(&command.tasks, expected);
+
        assert_timers(&command.timers, vec![TimerEvent::AdvanceGossip(tick_time)]);
+
    }
+

+
    #[test]
+
    fn test_tick_sync_interval_elapsed() {
+
        let start_time = test_time(1000);
+
        let timers = TaskTimers::new(start_time).with_intervals(Intervals {
+
            sync: LocalDuration::from_secs(1),
+
            ..Intervals::NEVER
+
        });
+

+
        let tick_time = start_time + test_duration(2);
+
        let command = timers.tick(tick_time);
+

+
        let mut expected = nonempty![TaskEvent::MaintainPersistenConnections];
+
        expected.append(&mut Sync::all().map(TaskEvent::from).into());
+
        assert_tasks(&command.tasks, expected);
+
        assert_timers(&command.timers, vec![TimerEvent::AdvanceSync(tick_time)]);
+
    }
+

+
    #[test]
+
    fn test_tick_announce_interval_elapsed() {
+
        let start_time = test_time(1000);
+
        let timers = TaskTimers::new(start_time).with_intervals(Intervals {
+
            announce: LocalDuration::from_secs(1),
+
            ..Intervals::NEVER
+
        });
+

+
        let tick_time = start_time + test_duration(2);
+
        let command = timers.tick(tick_time);
+

+
        let mut expected = nonempty![TaskEvent::MaintainPersistenConnections];
+
        expected.append(&mut Announce::all().map(TaskEvent::from).into());
+
        assert_tasks(&command.tasks, expected);
+
        assert_timers(
+
            &command.timers,
+
            vec![TimerEvent::AdvanceAnnounce(tick_time)],
+
        );
+
    }
+

+
    #[test]
+
    fn test_tick_prune_interval_elapsed() {
+
        let start_time = test_time(1000);
+
        let timers = TaskTimers::new(start_time).with_intervals(Intervals {
+
            prune: LocalDuration::from_secs(1),
+
            ..Intervals::NEVER
+
        });
+

+
        let tick_time = start_time + test_duration(2);
+
        let command = timers.tick(tick_time);
+

+
        let mut expected = nonempty![TaskEvent::MaintainPersistenConnections];
+
        expected.append(&mut Prune::all().map(TaskEvent::from).into());
+
        assert_tasks(&command.tasks, expected);
+
        assert_timers(&command.timers, vec![TimerEvent::AdvancePrune(tick_time)]);
+
    }
+

+
    #[test]
+
    fn test_tick_multiple_intervals_elapsed() {
+
        let start_time = test_time(1000);
+
        let timers = TaskTimers::new(start_time).with_intervals(Intervals {
+
            idle: LocalDuration::from_secs(1),
+
            gossip: LocalDuration::from_secs(1),
+
            sync: LocalDuration::from_secs(1),
+
            ..Intervals::NEVER
+
        });
+

+
        let tick_time = start_time + test_duration(2);
+
        let command = timers.tick(tick_time);
+

+
        let mut expected = nonempty![TaskEvent::MaintainPersistenConnections];
+
        expected.append(&mut Idle::all().map(TaskEvent::from).into());
+
        expected.append(&mut Gossip::all().map(TaskEvent::from).into());
+
        expected.append(&mut Sync::all().map(TaskEvent::from).into());
+
        assert_tasks(&command.tasks, expected);
+
        assert_timers(
+
            &command.timers,
+
            vec![
+
                TimerEvent::AdvanceIdle(tick_time),
+
                TimerEvent::AdvanceGossip(tick_time),
+
                TimerEvent::AdvanceSync(tick_time),
+
            ],
+
        );
+
    }
+

+
    #[test]
+
    fn test_tick_all_intervals_elapsed() {
+
        let start_time = test_time(1000);
+
        let timers = TaskTimers::new(start_time).with_intervals(Intervals::ALWAYS);
+

+
        let tick_time = start_time + test_duration(1);
+
        let command = timers.tick(tick_time);
+

+
        assert_tasks(&command.tasks, TaskEvent::all());
+
        assert_timers(
+
            &command.timers,
+
            vec![
+
                TimerEvent::AdvanceIdle(tick_time),
+
                TimerEvent::AdvanceGossip(tick_time),
+
                TimerEvent::AdvanceSync(tick_time),
+
                TimerEvent::AdvanceAnnounce(tick_time),
+
                TimerEvent::AdvancePrune(tick_time),
+
            ],
+
        );
+
    }
+

+
    #[test]
+
    fn test_advance_single_timer() {
+
        let start_time = test_time(1000);
+
        let mut timers = TaskTimers::new(start_time);
+

+
        let new_time = test_time(2000);
+
        let events = vec![TimerEvent::AdvanceIdle(new_time)];
+

+
        timers.advance(&events);
+

+
        assert_eq!(timers.last_idle, new_time);
+
        assert_eq!(timers.last_gossip, start_time);
+
        assert_eq!(timers.last_sync, start_time);
+
        assert_eq!(timers.last_announce, start_time);
+
        assert_eq!(timers.last_prune, start_time);
+
    }
+

+
    #[test]
+
    fn test_advance_multiple_timers() {
+
        let start_time = test_time(1000);
+
        let mut timers = TaskTimers::new(start_time);
+

+
        let new_time = test_time(2000);
+
        let events = vec![
+
            TimerEvent::AdvanceIdle(new_time),
+
            TimerEvent::AdvanceGossip(new_time),
+
            TimerEvent::AdvanceSync(new_time),
+
        ];
+

+
        timers.advance(&events);
+

+
        assert_eq!(timers.last_idle, new_time);
+
        assert_eq!(timers.last_gossip, new_time);
+
        assert_eq!(timers.last_sync, new_time);
+
        assert_eq!(timers.last_announce, start_time);
+
        assert_eq!(timers.last_prune, start_time);
+
    }
+

+
    #[test]
+
    fn test_advance_all_timers() {
+
        let start_time = test_time(1000);
+
        let mut timers = TaskTimers::new(start_time);
+

+
        let new_time = test_time(2000);
+
        let events = vec![
+
            TimerEvent::AdvanceIdle(new_time),
+
            TimerEvent::AdvanceGossip(new_time),
+
            TimerEvent::AdvanceSync(new_time),
+
            TimerEvent::AdvanceAnnounce(new_time),
+
            TimerEvent::AdvancePrune(new_time),
+
        ];
+

+
        timers.advance(&events);
+

+
        assert_eq!(timers.last_idle, new_time);
+
        assert_eq!(timers.last_gossip, new_time);
+
        assert_eq!(timers.last_sync, new_time);
+
        assert_eq!(timers.last_announce, new_time);
+
        assert_eq!(timers.last_prune, new_time);
+
    }
+

+
    #[test]
+
    fn test_advance_empty_events() {
+
        let start_time = test_time(1000);
+
        let mut timers = TaskTimers::new(start_time);
+

+
        timers.advance(&[]);
+

+
        // All timers should remain unchanged
+
        assert_eq!(timers.last_idle, start_time);
+
        assert_eq!(timers.last_gossip, start_time);
+
        assert_eq!(timers.last_sync, start_time);
+
        assert_eq!(timers.last_announce, start_time);
+
        assert_eq!(timers.last_prune, start_time);
+
    }
+

+
    #[test]
+
    fn test_exactly_at_interval_boundary() {
+
        let start_time = test_time(1000);
+
        let timers = TaskTimers::new(start_time).with_intervals(Intervals {
+
            idle: LocalDuration::from_secs(30),
+
            ..Intervals::NEVER
+
        });
+

+
        // Test exactly at the idle boundary
+
        let boundary_time = start_time + test_duration(30);
+
        let command = timers.tick(boundary_time);
+

+
        assert!(command.tasks.contains(&TaskEvent::Idle(Idle::KeepAlive)));
+
    }
+

+
    #[test]
+
    fn test_just_before_interval_boundary() {
+
        let start_time = test_time(1000);
+
        // Use different intervals to avoid gossip triggering when testing idle boundary
+
        let intervals = Intervals {
+
            idle: test_duration(30),
+
            ..Intervals::NEVER
+
        };
+
        let timers = TaskTimers::new(start_time).with_intervals(intervals);
+

+
        // Test just before the idle boundary
+
        let almost_boundary = start_time + test_duration(29);
+
        let command = timers.tick(almost_boundary);
+

+
        // Should not trigger idle tasks, only persistent connections
+
        assert_tasks(
+
            &command.tasks,
+
            nonempty![TaskEvent::MaintainPersistenConnections],
+
        );
+
    }
+

+
    #[test]
+
    fn test_tick_after_advance() {
+
        let start_time = test_time(1000);
+
        let mut timers = TaskTimers::new(start_time).with_intervals(Intervals {
+
            idle: LocalDuration::from_secs(30),
+
            gossip: LocalDuration::from_secs(30),
+
            ..Intervals::NEVER
+
        });
+

+
        // First tick - should trigger idle and gossip
+
        let first_tick = start_time + test_duration(30);
+
        let command1 = timers.tick(first_tick);
+

+
        // Advance the timers
+
        timers.advance(&command1.timers);
+

+
        // Second tick at same time - should not trigger anything again
+
        let command2 = timers.tick(first_tick);
+

+
        assert!(!command2.tasks.contains(&TaskEvent::Idle(Idle::KeepAlive)));
+
        assert!(!command2
+
            .tasks
+
            .contains(&TaskEvent::Gossip(Gossip::RelayAnnouncements)));
+
        assert_eq!(
+
            command2.tasks,
+
            nonempty![TaskEvent::MaintainPersistenConnections]
+
        );
+
    }
+

+
    #[test]
+
    fn test_tick_advance_tick_cycle() {
+
        let start_time = test_time(1000);
+
        let mut timers = TaskTimers::new(start_time).with_intervals(Intervals {
+
            idle: LocalDuration::from_secs(30),
+
            ..Intervals::NEVER
+
        });
+

+
        // First cycle
+
        let tick1 = start_time + test_duration(30);
+
        let command1 = timers.tick(tick1);
+
        timers.advance(&command1.timers);
+

+
        // Second cycle - advance by another idle interval
+
        let tick2 = tick1 + test_duration(30);
+
        let command2 = timers.tick(tick2);
+

+
        // Should trigger idle tasks again
+
        assert!(command2.tasks.contains(&TaskEvent::Idle(Idle::KeepAlive)));
+
    }
+

+
    #[test]
+
    fn test_custom_intervals_behavior() {
+
        let start_time = test_time(1000);
+
        let custom_intervals = Intervals {
+
            idle: test_duration(1),   // Very short idle interval
+
            gossip: test_duration(2), // Short gossip interval
+
            ..Intervals::NEVER
+
        };
+
        let timers = TaskTimers::new(start_time).with_intervals(custom_intervals);
+

+
        // Test that custom intervals are respected
+
        let tick_time = start_time + test_duration(1);
+
        let command = timers.tick(tick_time);
+

+
        assert!(command.tasks.contains(&TaskEvent::Idle(Idle::KeepAlive)));
+
        // Gossip should not trigger yet (interval is 2s)
+
        assert!(!command
+
            .tasks
+
            .contains(&TaskEvent::Gossip(Gossip::RelayAnnouncements)));
+

+
        // Test gossip triggers at its interval
+
        let gossip_time = start_time + test_duration(2);
+
        let command2 = timers.tick(gossip_time);
+

+
        assert!(command2
+
            .tasks
+
            .contains(&TaskEvent::Gossip(Gossip::RelayAnnouncements)));
+
    }
+

+
    #[test]
+
    fn test_zero_duration_intervals() {
+
        let start_time = test_time(1000);
+
        let zero_intervals = Intervals {
+
            idle: LocalDuration::from_secs(0),
+
            gossip: LocalDuration::from_secs(0),
+
            ..Intervals::NEVER
+
        };
+
        let timers = TaskTimers::new(start_time).with_intervals(zero_intervals);
+

+
        // Even with zero duration, should trigger immediately
+
        let command = timers.tick(start_time);
+

+
        assert!(command.tasks.contains(&TaskEvent::Idle(Idle::KeepAlive)));
+
        assert!(command
+
            .tasks
+
            .contains(&TaskEvent::Gossip(Gossip::RelayAnnouncements)));
+
    }
+

+
    #[test]
+
    fn test_partial_timer_advance() {
+
        let start_time = test_time(1000);
+
        let mut timers = TaskTimers::new(start_time);
+

+
        let new_time = test_time(2000);
+

+
        // Only advance some timers (not all that might have been generated)
+
        let partial_events = vec![
+
            TimerEvent::AdvanceIdle(new_time),
+
            TimerEvent::AdvanceGossip(new_time),
+
            // Intentionally omit sync, announce, and prune
+
        ];
+

+
        timers.advance(&partial_events);
+

+
        // Only idle and gossip should be advanced
+
        assert_eq!(timers.last_idle, new_time);
+
        assert_eq!(timers.last_gossip, new_time);
+
        assert_eq!(timers.last_sync, start_time);
+
        assert_eq!(timers.last_announce, start_time);
+
        assert_eq!(timers.last_prune, start_time);
+
    }
+

+
    #[test]
+
    fn test_property_timer_monotonicity() {
+
        let start_time = test_time(1000);
+
        let intervals = Intervals {
+
            idle: gen_duration(0),
+
            gossip: gen_duration(1),
+
            sync: gen_duration(2),
+
            announce: gen_duration(3),
+
            prune: gen_duration(4),
+
        };
+
        let mut timers = TaskTimers::new(start_time).with_intervals(intervals);
+

+
        let mut current_time = start_time;
+

+
        for _ in 0..100 {
+
            current_time = current_time + test_duration(10);
+
            let command = timers.tick(current_time);
+

+
            // Advance all timers
+
            timers.advance(&command.timers);
+

+
            // Verify all timers are monotonically increasing
+
            assert!(timers.last_idle >= start_time);
+
            assert!(timers.last_gossip >= start_time);
+
            assert!(timers.last_sync >= start_time);
+
            assert!(timers.last_announce >= start_time);
+
            assert!(timers.last_prune >= start_time);
+
        }
+
    }
+
}