rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwood53ca9970ac9d462332f053295be50f4a6cf278f8
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Created",
"patch": {
"id": "28703d219b029533f9f0708d8e0880d7778ecc31",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"title": "node: Use `std::time` for reactor and wire",
"state": {
"status": "open",
"conflicts": []
},
"before": "5741bafa3b9149a312cac686247972010e115dc2",
"after": "53ca9970ac9d462332f053295be50f4a6cf278f8",
"commits": [
"53ca9970ac9d462332f053295be50f4a6cf278f8"
],
"target": "5741bafa3b9149a312cac686247972010e115dc2",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "28703d219b029533f9f0708d8e0880d7778ecc31",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "This reduces the exposure to the `localtime` crate, using\n`std` instead.",
"base": "5741bafa3b9149a312cac686247972010e115dc2",
"oid": "53ca9970ac9d462332f053295be50f4a6cf278f8",
"timestamp": 1761599462
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "f97adac2-b4a2-46ef-8bf4-bf87465a07c0"
},
"info_url": "https://cci.rad.levitte.org//f97adac2-b4a2-46ef-8bf4-bf87465a07c0.html"
}
Started at: 2025-10-27 23:05:10.084225+01:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW added
✓ Remote-tracking branch cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW/master created for z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz added
✓ Remote-tracking branch erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz/master created for z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/f97adac2-b4a2-46ef-8bf4-bf87465a07c0/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 126 issues · 16 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 28703d219b029533f9f0708d8e0880d7778ecc31
✓ Switched to branch patch/28703d2 at revision 28703d2
✓ Branch patch/28703d2 setup to track rad/patches/28703d219b029533f9f0708d8e0880d7778ecc31
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout 53ca9970ac9d462332f053295be50f4a6cf278f8
HEAD is now at 53ca9970 node: Use `std::time` for reactor and wire
Exit code: 0
$ git show 53ca9970ac9d462332f053295be50f4a6cf278f8
commit 53ca9970ac9d462332f053295be50f4a6cf278f8
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Fri Oct 17 22:34:41 2025 +0200
node: Use `std::time` for reactor and wire
This reduces the exposure to the `localtime` crate, using
`std` instead.
diff --git a/crates/radicle-node/src/reactor.rs b/crates/radicle-node/src/reactor.rs
index 1e0802df..3273fd85 100644
--- a/crates/radicle-node/src/reactor.rs
+++ b/crates/radicle-node/src/reactor.rs
@@ -10,11 +10,10 @@ use std::fmt::{Debug, Display, Formatter};
use std::io::ErrorKind;
use std::sync::Arc;
use std::thread::JoinHandle;
-use std::time::Duration;
+use std::time::{Duration, Instant};
use std::{io, thread};
use crossbeam_channel::{unbounded, Receiver, TryRecvError};
-use localtime::LocalTime;
use mio::event::{Event, Source};
use mio::{Events, Interest, Poll, Waker};
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;
/// 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.
///
@@ -227,7 +226,7 @@ pub trait ReactionHandler: Send + Iterator<Item = Action<Self::Listener, Self::T
&mut self,
token: Token,
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.
@@ -235,7 +234,7 @@ pub trait ReactionHandler: Send + Iterator<Item = Action<Self::Listener, Self::T
&mut self,
token: Token,
reaction: <Self::Transport as EventHandler>::Reaction,
- time: localtime::LocalTime,
+ instant: Instant,
);
/// 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) {
loop {
- let before_poll = LocalTime::now();
+ let before_poll = Instant::now();
let timeout = self
.timeouts
.next_expiring_from(before_poll)
@@ -388,12 +387,12 @@ impl<H: ReactionHandler> Runtime<H> {
// Blocking
let res = self.poll.poll(&mut events, Some(timeout));
- let now = LocalTime::now();
- self.service.tick(now);
+ let tick = Instant::now();
+ self.service.tick(tick);
// The way this is currently used basically ignores which keys have
// 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 {
log::trace!(target: "reactor", "Timer has fired");
self.service.timer_reacted();
@@ -404,7 +403,9 @@ impl<H: ReactionHandler> Runtime<H> {
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.
if awoken {
@@ -420,14 +421,14 @@ impl<H: ReactionHandler> Runtime<H> {
}
}
- self.handle_actions(now);
+ self.handle_actions(tick);
}
}
/// # Returns
///
/// 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");
let mut awoken = false;
let mut deregistered = Vec::new();
@@ -449,7 +450,7 @@ impl<H: ReactionHandler> Runtime<H> {
.handle(event)
.into_iter()
.for_each(|service_event| {
- self.service.listener_reacted(token, service_event, time);
+ self.service.listener_reacted(token, service_event, instant);
});
} else {
let listener = self.deregister_listener(token).unwrap_or_else(|| {
@@ -470,7 +471,8 @@ impl<H: ReactionHandler> Runtime<H> {
.handle(event)
.into_iter()
.for_each(|service_event| {
- self.service.transport_reacted(token, service_event, time);
+ self.service
+ .transport_reacted(token, service_event, instant);
});
} else {
let transport = self.deregister_transport(token).unwrap_or_else(|| {
@@ -488,13 +490,13 @@ impl<H: ReactionHandler> Runtime<H> {
awoken
}
- fn handle_actions(&mut self, time: LocalTime) {
+ fn handle_actions(&mut self, instant: Instant) {
while let Some(action) = self.service.next() {
log::trace!(target: "reactor", "Handling action {action} from the service");
// 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
- if let Err(err) = self.handle_action(action, time) {
+ if let Err(err) = self.handle_action(action, instant) {
log::error!(target: "reactor", "Error: {err}");
self.service.handle_error(err);
}
@@ -504,7 +506,7 @@ impl<H: ReactionHandler> Runtime<H> {
fn handle_action(
&mut self,
action: Action<H::Listener, H::Transport>,
- time: LocalTime,
+ instant: Instant,
) -> Result<(), Error<H::Listener, H::Transport>> {
match action {
Action::RegisterListener(token, mut listener) => {
@@ -562,7 +564,7 @@ impl<H: ReactionHandler> Runtime<H> {
Action::SetTimer(duration) => {
log::trace!(target: "reactor", "Adding timer {duration:?} from now");
- self.timeouts.set_timeout(duration, time);
+ self.timeouts.set_timeout(duration, instant);
}
}
Ok(())
diff --git a/crates/radicle-node/src/reactor/timer.rs b/crates/radicle-node/src/reactor/timer.rs
index 9fb59444..680a6034 100644
--- a/crates/radicle-node/src/reactor/timer.rs
+++ b/crates/radicle-node/src/reactor/timer.rs
@@ -1,13 +1,12 @@
-use std::collections::BTreeSet;
use std::time::Duration;
+use std::{collections::BTreeSet, time::Instant};
-use localtime::{LocalDuration, LocalTime};
/// Manages timers and triggers timeouts.
#[derive(Debug, Default)]
pub struct Timer {
/// Timeouts are durations since the UNIX epoch.
- timeouts: BTreeSet<localtime::LocalTime>,
+ timeouts: BTreeSet<Instant>,
}
impl Timer {
@@ -31,32 +30,32 @@ impl Timer {
}
/// Register a new timeout relative to a certain point in time.
- pub fn set_timeout(&mut self, timeout: Duration, after: LocalTime) {
- let time = after + LocalDuration::from_millis(timeout.as_millis());
+ pub fn set_timeout(&mut self, timeout: Duration, after: Instant) {
+ let time = after + timeout;
self.timeouts.insert(time);
}
/// Get the first timeout expiring right at or after certain moment of time.
/// 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 last = *self.timeouts.first()?;
Some(if last >= time {
- Duration::from_millis(last.as_millis() - time.as_millis())
+ last - time
} else {
- Duration::from_secs(0)
+ Duration::default()
})
}
/// Removes timeouts which expire by a certain moment of time (inclusive),
/// 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,
// 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
// given time value so that it is put in the "before" set that gets expired
// and overwritten.
- let at = time + LocalDuration::from_millis(1);
+ let at = instant + Duration::from_millis(1);
let unexpired = self.timeouts.split_off(&at);
let fired = self.timeouts.len();
self.timeouts = unexpired;
@@ -72,12 +71,12 @@ mod tests {
fn test_wake_exact() {
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(9), 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);
}
@@ -85,7 +84,7 @@ mod tests {
fn test_wake() {
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(16), now);
tm.set_timeout(Duration::from_secs(64), now);
@@ -94,13 +93,13 @@ mod tests {
assert_eq!(tm.remove_expired_by(now), 0);
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.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.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");
}
@@ -108,17 +107,17 @@ mod tests {
fn test_next() {
let mut tm = Timer::new();
- let mut now = LocalTime::now();
+ let mut now = Instant::now();
tm.set_timeout(Duration::from_secs(3), now);
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)));
- now = now + LocalDuration::from_secs(1);
+ now += Duration::from_secs(1);
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.remove_expired_by(now), 1);
diff --git a/crates/radicle-node/src/wire.rs b/crates/radicle-node/src/wire.rs
index 4103cafd..e6588339 100644
--- a/crates/radicle-node/src/wire.rs
+++ b/crates/radicle-node/src/wire.rs
@@ -5,6 +5,7 @@ use std::collections::hash_map::Entry;
use std::collections::VecDeque;
use std::fmt::Debug;
use std::sync::Arc;
+use std::time::{Instant, SystemTime};
use std::{io, net, time};
use crossbeam_channel as chan;
@@ -12,7 +13,6 @@ use cyphernet::addr::{HostName, InetHost, NetAddr};
use cyphernet::encrypt::noise::{HandshakePattern, Keyset, NoiseState};
use cyphernet::proxy::socks5;
use cyphernet::{Digest, EcSk, Ecdh, Sha256};
-use localtime::LocalTime;
use mio::net::TcpStream;
use radicle::node::device::Device;
@@ -313,6 +313,8 @@ pub(crate) struct Wire<D, S, G: crypto::signature::Signer<crypto::Signature> + E
peers: Peers,
/// A (practically) infinite source of tokens to identify transports and listeners.
tokens: Tokens,
+ /// Record of system time and instant when the node started.
+ epoch: (SystemTime, Instant),
}
impl<D, S, G> Wire<D, S, G>
@@ -335,9 +337,14 @@ where
listening: RandomMap::default(),
peers: Peers(RandomMap::default()),
tokens: Tokens::default(),
+ epoch: (SystemTime::now(), Instant::now()),
}
}
+ fn time(&self, instant: Instant) -> SystemTime {
+ self.epoch.0 + (instant - self.epoch.1)
+ }
+
pub fn listen(&mut self, socket: Listener) {
let token = self.tokens.advance();
self.listening.insert(token, socket.local_addr());
@@ -496,7 +503,7 @@ where
type Listener = Listener;
type Transport = Transport<WireSession<G>>;
- fn tick(&mut self, time: LocalTime) {
+ fn tick(&mut self, time: Instant) {
self.metrics.open_channels = self
.peers
.iter()
@@ -509,10 +516,8 @@ where
})
.sum();
self.metrics.worker_queue_size = self.worker.len();
- self.service.tick(
- LocalTime::from_millis(time.as_millis() as u128),
- &self.metrics,
- );
+
+ self.service.tick(self.time(time).into(), &self.metrics);
}
fn timer_reacted(&mut self) {
@@ -523,7 +528,7 @@ where
&mut self,
_: Token, // Note that this is the token of the listener socket.
event: io::Result<(TcpStream, std::net::SocketAddr)>,
- _: LocalTime,
+ _: Instant,
) {
match event {
Ok((connection, peer)) => {
@@ -587,12 +592,7 @@ where
}
}
- fn transport_reacted(
- &mut self,
- token: Token,
- event: SessionEvent<WireSession<G>>,
- _: LocalTime,
- ) {
+ fn transport_reacted(&mut self, token: Token, event: SessionEvent<WireSession<G>>, _: Instant) {
match event {
SessionEvent::Established(ProtocolArtifact { state, session }) => {
// SAFETY: With the NoiseXK protocol, there is always a remote static key.
Exit code: 0
shell: 'export RUSTDOCFLAGS=''-D warnings'' cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny warnings cargo build --all-targets --workspace cargo doc --workspace --no-deps cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name f97adac2-b4a2-46ef-8bf4-bf87465a07c0 -v /opt/radcis/ci.rad.levitte.org/cci/state/f97adac2-b4a2-46ef-8bf4-bf87465a07c0/s:/f97adac2-b4a2-46ef-8bf4-bf87465a07c0/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/f97adac2-b4a2-46ef-8bf4-bf87465a07c0/w:/f97adac2-b4a2-46ef-8bf4-bf87465a07c0/w -w /f97adac2-b4a2-46ef-8bf4-bf87465a07c0/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:bookworm bash /f97adac2-b4a2-46ef-8bf4-bf87465a07c0/s/script.sh
+ export 'RUSTDOCFLAGS=-D warnings'
+ RUSTDOCFLAGS='-D warnings'
+ cargo --version
info: syncing channel updates for '1.90-x86_64-unknown-linux-gnu'
info: latest update on 2025-09-18, rust version 1.90.0 (1159e78c4 2025-09-14)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-src'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-src'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
cargo 1.90.0 (840b83a10 2025-07-30)
+ rustc --version
rustc 1.90.0 (1159e78c4 2025-09-14)
+ cargo fmt --check
Diff in /f97adac2-b4a2-46ef-8bf4-bf87465a07c0/w/crates/radicle-node/src/reactor/timer.rs:1:
use std::time::Duration;
use std::{collections::BTreeSet, time::Instant};
-
/// Manages timers and triggers timeouts.
#[derive(Debug, Default)]
pub struct Timer {
Exit code: 1
{
"response": "finished",
"result": "failure"
}