node: Use `std::time` for reactor and wire

This reduces the exposure to the `localtime` crate, using
`std` instead.
This commit is contained in:
Lorenz Leutgeb 2025-10-17 22:34:41 +02:00 committed by Fintan Halpenny
parent 9bcdd353c9
commit e404f1038f
3 changed files with 81 additions and 52 deletions

View File

@ -10,11 +10,10 @@ use std::fmt::{Debug, Display, Formatter};
use std::io::ErrorKind; use std::io::ErrorKind;
use std::sync::Arc; use std::sync::Arc;
use std::thread::JoinHandle; use std::thread::JoinHandle;
use std::time::Duration; use std::time::{Duration, Instant};
use std::{io, thread}; use std::{io, thread};
use crossbeam_channel::{unbounded, Receiver, TryRecvError}; use crossbeam_channel::{unbounded, Receiver, TryRecvError};
use localtime::LocalTime;
use mio::event::{Event, Source}; use mio::event::{Event, Source};
use mio::{Events, Interest, Poll, Waker}; use mio::{Events, Interest, Poll, Waker};
use thiserror::Error; use thiserror::Error;
@ -212,7 +211,7 @@ pub trait ReactionHandler: Send + Iterator<Item = Action<Self::Listener, Self::T
type Transport: EventHandler + Source + Send + Debug + WriteAtomic; type Transport: EventHandler + Source + Send + Debug + WriteAtomic;
/// Method called by the reactor on the start of each event loop once the poll has returned. /// Method called by the reactor on the start of each event loop once the poll has returned.
fn tick(&mut self, time: localtime::LocalTime); fn tick(&mut self, instant: Instant);
/// Method called by the reactor when a previously set timeout is fired. /// Method called by the reactor when a previously set timeout is fired.
/// ///
@ -227,7 +226,7 @@ pub trait ReactionHandler: Send + Iterator<Item = Action<Self::Listener, Self::T
&mut self, &mut self,
token: Token, token: Token,
reaction: <Self::Listener as EventHandler>::Reaction, reaction: <Self::Listener as EventHandler>::Reaction,
time: localtime::LocalTime, instant: Instant,
); );
/// Method called by the reactor upon a reaction to an I/O event on a transport resource. /// Method called by the reactor upon a reaction to an I/O event on a transport resource.
@ -235,7 +234,7 @@ pub trait ReactionHandler: Send + Iterator<Item = Action<Self::Listener, Self::T
&mut self, &mut self,
token: Token, token: Token,
reaction: <Self::Transport as EventHandler>::Reaction, reaction: <Self::Transport as EventHandler>::Reaction,
time: localtime::LocalTime, instant: Instant,
); );
/// Method called by the reactor when a given resource was successfully registered /// Method called by the reactor when a given resource was successfully registered
@ -372,7 +371,7 @@ impl<H: ReactionHandler> Runtime<H> {
fn run(mut self) { fn run(mut self) {
loop { loop {
let before_poll = LocalTime::now(); let before_poll = Instant::now();
let timeout = self let timeout = self
.timeouts .timeouts
.next_expiring_from(before_poll) .next_expiring_from(before_poll)
@ -388,12 +387,12 @@ impl<H: ReactionHandler> Runtime<H> {
// Blocking // Blocking
let res = self.poll.poll(&mut events, Some(timeout)); let res = self.poll.poll(&mut events, Some(timeout));
let now = LocalTime::now(); let tick = Instant::now();
self.service.tick(now); self.service.tick(tick);
// The way this is currently used basically ignores which keys have // The way this is currently used basically ignores which keys have
// timed out. So as long as *something* timed out, we wake the service. // timed out. So as long as *something* timed out, we wake the service.
let timers_fired = self.timeouts.remove_expired_by(now); let timers_fired = self.timeouts.remove_expired_by(tick);
if timers_fired > 0 { if timers_fired > 0 {
log::trace!(target: "reactor", "Timer has fired"); log::trace!(target: "reactor", "Timer has fired");
self.service.timer_reacted(); self.service.timer_reacted();
@ -404,7 +403,9 @@ impl<H: ReactionHandler> Runtime<H> {
self.service.handle_error(Error::Poll(err)); self.service.handle_error(Error::Poll(err));
} }
let awoken = self.handle_events(now, events); let awoken = self.handle_events(tick, events);
log::trace!(target: "reactor", "Duration between tick and events handled: {:?}", Instant::now().duration_since(tick));
// Process the commands only if we awoken by the waker. // Process the commands only if we awoken by the waker.
if awoken { if awoken {
@ -420,14 +421,14 @@ impl<H: ReactionHandler> Runtime<H> {
} }
} }
self.handle_actions(now); self.handle_actions(tick);
} }
} }
/// # Returns /// # Returns
/// ///
/// Whether one of the events was originated from the waker. /// Whether one of the events was originated from the waker.
fn handle_events(&mut self, time: LocalTime, events: Events) -> bool { fn handle_events(&mut self, instant: Instant, events: Events) -> bool {
log::trace!(target: "reactor", "Handling events"); log::trace!(target: "reactor", "Handling events");
let mut awoken = false; let mut awoken = false;
let mut deregistered = Vec::new(); let mut deregistered = Vec::new();
@ -449,7 +450,7 @@ impl<H: ReactionHandler> Runtime<H> {
.handle(event) .handle(event)
.into_iter() .into_iter()
.for_each(|service_event| { .for_each(|service_event| {
self.service.listener_reacted(token, service_event, time); self.service.listener_reacted(token, service_event, instant);
}); });
} else { } else {
let listener = self.deregister_listener(token).unwrap_or_else(|| { let listener = self.deregister_listener(token).unwrap_or_else(|| {
@ -470,7 +471,8 @@ impl<H: ReactionHandler> Runtime<H> {
.handle(event) .handle(event)
.into_iter() .into_iter()
.for_each(|service_event| { .for_each(|service_event| {
self.service.transport_reacted(token, service_event, time); self.service
.transport_reacted(token, service_event, instant);
}); });
} else { } else {
let transport = self.deregister_transport(token).unwrap_or_else(|| { let transport = self.deregister_transport(token).unwrap_or_else(|| {
@ -488,13 +490,13 @@ impl<H: ReactionHandler> Runtime<H> {
awoken awoken
} }
fn handle_actions(&mut self, time: LocalTime) { fn handle_actions(&mut self, instant: Instant) {
while let Some(action) = self.service.next() { while let Some(action) = self.service.next() {
log::trace!(target: "reactor", "Handling action {action} from the service"); log::trace!(target: "reactor", "Handling action {action} from the service");
// Deadlock may happen here if the service will generate events over and over // Deadlock may happen here if the service will generate events over and over
// in the handle_* calls we may never get out of this loop // in the handle_* calls we may never get out of this loop
if let Err(err) = self.handle_action(action, time) { if let Err(err) = self.handle_action(action, instant) {
log::error!(target: "reactor", "Error: {err}"); log::error!(target: "reactor", "Error: {err}");
self.service.handle_error(err); self.service.handle_error(err);
} }
@ -504,7 +506,7 @@ impl<H: ReactionHandler> Runtime<H> {
fn handle_action( fn handle_action(
&mut self, &mut self,
action: Action<H::Listener, H::Transport>, action: Action<H::Listener, H::Transport>,
time: LocalTime, instant: Instant,
) -> Result<(), Error<H::Listener, H::Transport>> { ) -> Result<(), Error<H::Listener, H::Transport>> {
match action { match action {
Action::RegisterListener(token, mut listener) => { Action::RegisterListener(token, mut listener) => {
@ -562,7 +564,7 @@ impl<H: ReactionHandler> Runtime<H> {
Action::SetTimer(duration) => { Action::SetTimer(duration) => {
log::trace!(target: "reactor", "Adding timer {duration:?} from now"); log::trace!(target: "reactor", "Adding timer {duration:?} from now");
self.timeouts.set_timeout(duration, time); self.timeouts.set_timeout(duration, instant);
} }
} }
Ok(()) Ok(())

View File

@ -1,13 +1,11 @@
use std::collections::BTreeSet;
use std::time::Duration; use std::time::Duration;
use std::{collections::BTreeSet, time::Instant};
use localtime::{LocalDuration, LocalTime};
/// Manages timers and triggers timeouts. /// Manages timers and triggers timeouts.
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Timer { pub struct Timer {
/// Timeouts are durations since the UNIX epoch. /// Timeouts are durations since the UNIX epoch.
timeouts: BTreeSet<localtime::LocalTime>, timeouts: BTreeSet<Instant>,
} }
impl Timer { impl Timer {
@ -31,32 +29,32 @@ impl Timer {
} }
/// Register a new timeout relative to a certain point in time. /// Register a new timeout relative to a certain point in time.
pub fn set_timeout(&mut self, timeout: Duration, after: LocalTime) { pub fn set_timeout(&mut self, timeout: Duration, after: Instant) {
let time = after + LocalDuration::from_millis(timeout.as_millis()); let time = after + timeout;
self.timeouts.insert(time); self.timeouts.insert(time);
} }
/// Get the first timeout expiring right at or after certain moment of time. /// Get the first timeout expiring right at or after certain moment of time.
/// Returns [`None`] if there are no timeouts. /// Returns [`None`] if there are no timeouts.
pub fn next_expiring_from(&self, time: impl Into<LocalTime>) -> Option<Duration> { pub fn next_expiring_from(&self, time: impl Into<Instant>) -> Option<Duration> {
let time = time.into(); let time = time.into();
let last = *self.timeouts.first()?; let last = *self.timeouts.first()?;
Some(if last >= time { Some(if last >= time {
Duration::from_millis(last.as_millis() - time.as_millis()) last - time
} else { } else {
Duration::from_secs(0) Duration::default()
}) })
} }
/// Removes timeouts which expire by a certain moment of time (inclusive), /// Removes timeouts which expire by a certain moment of time (inclusive),
/// returning total number of timeouts which were removed. /// returning total number of timeouts which were removed.
pub fn remove_expired_by(&mut self, time: LocalTime) -> usize { pub fn remove_expired_by(&mut self, instant: Instant) -> usize {
// Since `split_off` returns everything *after* the given key, including the key, // Since `split_off` returns everything *after* the given key, including the key,
// if a timer is set for exactly the given time, it would remain in the "after" // if a timer is set for exactly the given time, it would remain in the "after"
// set of unexpired keys. This isn't what we want, therefore we add `1` to the // set of unexpired keys. This isn't what we want, therefore we add `1` to the
// given time value so that it is put in the "before" set that gets expired // given time value so that it is put in the "before" set that gets expired
// and overwritten. // and overwritten.
let at = time + LocalDuration::from_millis(1); let at = instant + Duration::from_millis(1);
let unexpired = self.timeouts.split_off(&at); let unexpired = self.timeouts.split_off(&at);
let fired = self.timeouts.len(); let fired = self.timeouts.len();
self.timeouts = unexpired; self.timeouts = unexpired;
@ -72,12 +70,12 @@ mod tests {
fn test_wake_exact() { fn test_wake_exact() {
let mut tm = Timer::new(); let mut tm = Timer::new();
let now = LocalTime::now(); let now = Instant::now();
tm.set_timeout(Duration::from_secs(8), now); tm.set_timeout(Duration::from_secs(8), now);
tm.set_timeout(Duration::from_secs(9), now); tm.set_timeout(Duration::from_secs(9), now);
tm.set_timeout(Duration::from_secs(10), now); tm.set_timeout(Duration::from_secs(10), now);
assert_eq!(tm.remove_expired_by(now + LocalDuration::from_secs(9)), 2); assert_eq!(tm.remove_expired_by(now + Duration::from_secs(9)), 2);
assert_eq!(tm.count(), 1); assert_eq!(tm.count(), 1);
} }
@ -85,7 +83,7 @@ mod tests {
fn test_wake() { fn test_wake() {
let mut tm = Timer::new(); let mut tm = Timer::new();
let now = LocalTime::now(); let now = Instant::now();
tm.set_timeout(Duration::from_secs(8), now); tm.set_timeout(Duration::from_secs(8), now);
tm.set_timeout(Duration::from_secs(16), now); tm.set_timeout(Duration::from_secs(16), now);
tm.set_timeout(Duration::from_secs(64), now); tm.set_timeout(Duration::from_secs(64), now);
@ -94,13 +92,13 @@ mod tests {
assert_eq!(tm.remove_expired_by(now), 0); assert_eq!(tm.remove_expired_by(now), 0);
assert_eq!(tm.count(), 4); assert_eq!(tm.count(), 4);
assert_eq!(tm.remove_expired_by(now + LocalDuration::from_secs(9)), 1); assert_eq!(tm.remove_expired_by(now + Duration::from_secs(9)), 1);
assert_eq!(tm.count(), 3, "one timeout has expired"); assert_eq!(tm.count(), 3, "one timeout has expired");
assert_eq!(tm.remove_expired_by(now + LocalDuration::from_secs(66)), 2); assert_eq!(tm.remove_expired_by(now + Duration::from_secs(66)), 2);
assert_eq!(tm.count(), 1, "another two timeouts have expired"); assert_eq!(tm.count(), 1, "another two timeouts have expired");
assert_eq!(tm.remove_expired_by(now + LocalDuration::from_secs(96)), 1); assert_eq!(tm.remove_expired_by(now + Duration::from_secs(96)), 1);
assert!(!tm.has_timeouts(), "all timeouts have expired"); assert!(!tm.has_timeouts(), "all timeouts have expired");
} }
@ -108,17 +106,17 @@ mod tests {
fn test_next() { fn test_next() {
let mut tm = Timer::new(); let mut tm = Timer::new();
let mut now = LocalTime::now(); let mut now = Instant::now();
tm.set_timeout(Duration::from_secs(3), now); tm.set_timeout(Duration::from_secs(3), now);
assert_eq!(tm.next_expiring_from(now), Some(Duration::from_secs(3))); assert_eq!(tm.next_expiring_from(now), Some(Duration::from_secs(3)));
now = now + LocalDuration::from_secs(2); now += Duration::from_secs(2);
assert_eq!(tm.next_expiring_from(now), Some(Duration::from_secs(1))); assert_eq!(tm.next_expiring_from(now), Some(Duration::from_secs(1)));
now = now + LocalDuration::from_secs(1); now += Duration::from_secs(1);
assert_eq!(tm.next_expiring_from(now), Some(Duration::from_secs(0))); assert_eq!(tm.next_expiring_from(now), Some(Duration::from_secs(0)));
now = now + LocalDuration::from_secs(1); now += Duration::from_secs(1);
assert_eq!(tm.next_expiring_from(now), Some(Duration::from_secs(0))); assert_eq!(tm.next_expiring_from(now), Some(Duration::from_secs(0)));
assert_eq!(tm.remove_expired_by(now), 1); assert_eq!(tm.remove_expired_by(now), 1);

View File

@ -5,6 +5,7 @@ use std::collections::hash_map::Entry;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::fmt::Debug; use std::fmt::Debug;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Instant, SystemTime};
use std::{io, net, time}; use std::{io, net, time};
use crossbeam_channel as chan; use crossbeam_channel as chan;
@ -12,7 +13,6 @@ use cyphernet::addr::{HostName, InetHost, NetAddr};
use cyphernet::encrypt::noise::{HandshakePattern, Keyset, NoiseState}; use cyphernet::encrypt::noise::{HandshakePattern, Keyset, NoiseState};
use cyphernet::proxy::socks5; use cyphernet::proxy::socks5;
use cyphernet::{Digest, EcSk, Ecdh, Sha256}; use cyphernet::{Digest, EcSk, Ecdh, Sha256};
use localtime::LocalTime;
use mio::net::TcpStream; use mio::net::TcpStream;
use radicle::node::device::Device; use radicle::node::device::Device;
@ -291,6 +291,35 @@ impl Peers {
} }
} }
/// The epoch time of when the node started.
struct Epoch {
/// The system time when the node started.
started_time: SystemTime,
/// The instant when the node started.
started_at: Instant,
}
impl Epoch {
/// Construct a new [`Epoch`].
fn new(started_time: SystemTime, started_at: Instant) -> Self {
Self {
started_time,
started_at,
}
}
/// Construct an [`Epoch`] where both values are recorded using their
/// equivalent `now` constructors.
fn now() -> Self {
Self::new(SystemTime::now(), Instant::now())
}
/// Get the elapsed [`SystemTime`] given a later [`Instant`].
fn elapsed_time(&self, later: Instant) -> SystemTime {
self.started_time + (later - self.started_at)
}
}
/// Wire protocol implementation for a set of peers. /// Wire protocol implementation for a set of peers.
pub(crate) struct Wire<D, S, G: crypto::signature::Signer<crypto::Signature> + Ecdh> { pub(crate) struct Wire<D, S, G: crypto::signature::Signer<crypto::Signature> + Ecdh> {
/// Backing service instance. /// Backing service instance.
@ -313,6 +342,8 @@ pub(crate) struct Wire<D, S, G: crypto::signature::Signer<crypto::Signature> + E
peers: Peers, peers: Peers,
/// A (practically) infinite source of tokens to identify transports and listeners. /// A (practically) infinite source of tokens to identify transports and listeners.
tokens: Tokens, tokens: Tokens,
/// Record of system time and instant when the node started.
epoch: Epoch,
} }
impl<D, S, G> Wire<D, S, G> impl<D, S, G> Wire<D, S, G>
@ -335,9 +366,14 @@ where
listening: RandomMap::default(), listening: RandomMap::default(),
peers: Peers(RandomMap::default()), peers: Peers(RandomMap::default()),
tokens: Tokens::default(), tokens: Tokens::default(),
epoch: Epoch::now(),
} }
} }
fn time(&self, instant: Instant) -> SystemTime {
self.epoch.elapsed_time(instant)
}
pub fn listen(&mut self, socket: Listener) { pub fn listen(&mut self, socket: Listener) {
let token = self.tokens.advance(); let token = self.tokens.advance();
self.listening.insert(token, socket.local_addr()); self.listening.insert(token, socket.local_addr());
@ -496,7 +532,7 @@ where
type Listener = Listener; type Listener = Listener;
type Transport = Transport<WireSession<G>>; type Transport = Transport<WireSession<G>>;
fn tick(&mut self, time: LocalTime) { fn tick(&mut self, time: Instant) {
self.metrics.open_channels = self self.metrics.open_channels = self
.peers .peers
.iter() .iter()
@ -509,10 +545,8 @@ where
}) })
.sum(); .sum();
self.metrics.worker_queue_size = self.worker.len(); self.metrics.worker_queue_size = self.worker.len();
self.service.tick(
LocalTime::from_millis(time.as_millis() as u128), self.service.tick(self.time(time).into(), &self.metrics);
&self.metrics,
);
} }
fn timer_reacted(&mut self) { fn timer_reacted(&mut self) {
@ -523,7 +557,7 @@ where
&mut self, &mut self,
_: Token, // Note that this is the token of the listener socket. _: Token, // Note that this is the token of the listener socket.
event: io::Result<(TcpStream, std::net::SocketAddr)>, event: io::Result<(TcpStream, std::net::SocketAddr)>,
_: LocalTime, _: Instant,
) { ) {
match event { match event {
Ok((connection, peer)) => { Ok((connection, peer)) => {
@ -587,12 +621,7 @@ where
} }
} }
fn transport_reacted( fn transport_reacted(&mut self, token: Token, event: SessionEvent<WireSession<G>>, _: Instant) {
&mut self,
token: Token,
event: SessionEvent<WireSession<G>>,
_: LocalTime,
) {
match event { match event {
SessionEvent::Established(ProtocolArtifact { state, session }) => { SessionEvent::Established(ProtocolArtifact { state, session }) => {
// SAFETY: With the NoiseXK protocol, there is always a remote static key. // SAFETY: With the NoiseXK protocol, there is always a remote static key.