rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwood39b90878ffca0197c86d78ea3a0de82472dc4ce3
{
"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": "Updated",
"patch": {
"id": "80ae361a0b13980a10d50febaca13090842791fd",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"title": "node/reactor: Correctly handle error events",
"state": {
"status": "open",
"conflicts": []
},
"before": "532e5a0de03e0d0251bf1b30fdd6ba7432c5e4d6",
"after": "39b90878ffca0197c86d78ea3a0de82472dc4ce3",
"commits": [
"39b90878ffca0197c86d78ea3a0de82472dc4ce3"
],
"target": "532e5a0de03e0d0251bf1b30fdd6ba7432c5e4d6",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "80ae361a0b13980a10d50febaca13090842791fd",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "`fn handle_events` would panic, if there were multiple events for one\ntoken, and the first one that happened to be handled was an error.\nIndeed it is concerning if a token is encountered that was never\nregistered before. However, tokens that were just deregistered must be\ntracked.\n\nUsing `Vec` here seems a bit costly, in the future,\n`smallvec::SmallVec` could be considered.\n\nThe \"unregister\" methods are renamed to \"deregister\" to better line up\nwith `mio` vocabulary.\n\nLog stamtements that helped analysis of the panic that occurred here\nis overhauled and improved, requiring a `Debug` bound on types that\nobviously implement it.",
"base": "384c506489dd6a4cbf8c80b0370b2b2a8de7835b",
"oid": "6180c256afafb3b5c06942b87bf7972c09af6b23",
"timestamp": 1760471598
},
{
"id": "5756a29ea83971577888c3042e552ae9efd1567c",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "Fix errors in commit message",
"base": "384c506489dd6a4cbf8c80b0370b2b2a8de7835b",
"oid": "554266e32f891a48c95dc9747600fe8308031d29",
"timestamp": 1760472058
},
{
"id": "98c2bed398d03b8b149e9435e8077f18524e7ebc",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "Rebase",
"base": "532e5a0de03e0d0251bf1b30fdd6ba7432c5e4d6",
"oid": "39b90878ffca0197c86d78ea3a0de82472dc4ce3",
"timestamp": 1760472364
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "50c695a2-c889-41bd-91b8-22c7c202686a"
},
"info_url": "https://cci.rad.levitte.org//50c695a2-c889-41bd-91b8-22c7c202686a.html"
}
Started at: 2025-10-14 22:06:09.080665+02: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/50c695a2-c889-41bd-91b8-22c7c202686a/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 125 issues · 18 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 80ae361a0b13980a10d50febaca13090842791fd
✓ Switched to branch patch/80ae361 at revision 98c2bed
✓ Branch patch/80ae361 setup to track rad/patches/80ae361a0b13980a10d50febaca13090842791fd
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout 39b90878ffca0197c86d78ea3a0de82472dc4ce3
HEAD is now at 39b90878 node/reactor: Correctly handle error events
Exit code: 0
$ git show 39b90878ffca0197c86d78ea3a0de82472dc4ce3
commit 39b90878ffca0197c86d78ea3a0de82472dc4ce3
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Mon Oct 13 17:42:37 2025 +0200
node/reactor: Correctly handle error events
`fn handle_events` would panic, if there were multiple events for one
token, and the first one that happened to be handled was an error.
Indeed it is concerning if a token is encountered that was never
registered before. However, tokens that were just deregistered must be
tracked.
Using `Vec` here seems a bit costly, in the future,
`smallvec::SmallVec` could be considered.
The "unregister" methods are renamed to "deregister" to better line up
with `mio` vocabulary.
Log statements that helped analysis of the panic that occurred here
are overhauled and improved, requiring a `Debug` bound on types that
obviously implement it.
diff --git a/crates/radicle-node/src/reactor.rs b/crates/radicle-node/src/reactor.rs
index 0aeda592..1ad2be62 100644
--- a/crates/radicle-node/src/reactor.rs
+++ b/crates/radicle-node/src/reactor.rs
@@ -203,13 +203,13 @@ pub trait ReactionHandler: Send + Iterator<Item = Action<Self::Listener, Self::T
/// Listener resources are resources which may spawn more resources and can't be written to. A
/// typical example of a listener resource is a [`std::net::TcpListener`], however this may also
/// be a special form of a peripheral device or something else.
- type Listener: EventHandler + Source + Send;
+ type Listener: EventHandler + Source + Send + Debug;
/// Type for a transport resource.
///
/// Transport is a "full" resource which can be read from - and written to. Usual files, network
/// connections, database connections etc are all fall into this category.
- type Transport: EventHandler + Source + Send + 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.
fn tick(&mut self, time: localtime::LocalTime);
@@ -428,50 +428,56 @@ impl<H: ReactionHandler> Runtime<H> {
///
/// Whether one of the events was originated from the waker.
fn handle_events(&mut self, time: LocalTime, events: Events) -> bool {
+ log::trace!(target: "reactor", "Handling events");
let mut awoken = false;
+ let mut deregistered = Vec::new();
for event in events.into_iter() {
- let id = event.token();
+ let token = event.token();
- if id == WAKER {
+ if token == WAKER {
log::trace!(target: "reactor", "Awoken by the controller");
awoken = true;
- } else if self.listeners.contains_key(&id) {
- log::trace!(target: "reactor", event:debug; "From listener");
+ } else if self.listeners.contains_key(&token) {
+ log::trace!(target: "reactor", token=token.0; "Event from listener with token {}: {:?}", token.0, event);
if !event.is_error() {
- let listener = self.listeners.get_mut(&id).expect("resource disappeared");
+ let listener = self
+ .listeners
+ .get_mut(&token)
+ .expect("resource disappeared");
listener
.handle(event)
.into_iter()
.for_each(|service_event| {
- self.service.listener_reacted(id, service_event, time);
+ self.service.listener_reacted(token, service_event, time);
});
} else {
- let listener = self
- .unregister_listener(id)
- .expect("listener has disappeared");
+ let listener = self.deregister_listener(token).unwrap_or_else(|| panic!("listener with token {} has disappeared", token.0));
self.service
- .handle_error(Error::ListenerDisconnect(id, listener));
+ .handle_error(Error::ListenerDisconnect(token, listener));
+ deregistered.push(token);
}
- } else if self.transports.contains_key(&id) {
- log::trace!(target: "reactor", event:debug; "From transport");
+ } else if self.transports.contains_key(&token) {
+ log::trace!(target: "reactor", token=token.0; "Event from transport with token {}: {:?}", token.0, event);
if !event.is_error() {
- let transport = self.transports.get_mut(&id).expect("resource disappeared");
+ let transport = self
+ .transports
+ .get_mut(&token)
+ .expect("resource disappeared");
transport
.handle(event)
.into_iter()
.for_each(|service_event| {
- self.service.transport_reacted(id, service_event, time);
+ self.service.transport_reacted(token, service_event, time);
});
} else {
- let transport = self
- .unregister_transport(id)
- .expect("transport has disappeared");
+ let transport = self.deregister_transport(token).unwrap_or_else(|| panic!("transport with token {} has disappeared", token.0));
self.service
- .handle_error(Error::TransportDisconnect(id, transport));
+ .handle_error(Error::TransportDisconnect(token, transport));
+ deregistered.push(token);
}
- } else {
- panic!("token in poll which is not a known waker, listener or transport")
+ } else if !deregistered.contains(&token) {
+ log::error!(target: "reactor", token=token.0; "Event from unknown token {}: {:?}", token.0, event);
}
}
@@ -498,7 +504,7 @@ impl<H: ReactionHandler> Runtime<H> {
) -> Result<(), Error<H::Listener, H::Transport>> {
match action {
Action::RegisterListener(token, mut listener) => {
- log::debug!(target: "reactor", token=token.0; "Registering listener");
+ log::trace!(target: "reactor", token=token.0; "Registering listener {:?} with token {}", listener, token.0);
self.poll
.registry()
@@ -520,33 +526,33 @@ impl<H: ReactionHandler> Runtime<H> {
.transport_registered(token, &self.transports[&token]);
}
Action::UnregisterListener(token) => {
- let Some(listener) = self.unregister_listener(token) else {
+ let Some(listener) = self.deregister_listener(token) else {
return Ok(());
};
- log::debug!(target: "reactor", token=token.0; "Handing over listener");
+ log::debug!(target: "reactor", token=token.0; "Handing over listener {listener:?} with token {}", token.0);
self.service.handover_listener(token, listener);
}
Action::UnregisterTransport(token) => {
- let Some(transport) = self.unregister_transport(token) else {
+ let Some(transport) = self.deregister_transport(token) else {
return Ok(());
};
- log::debug!(target: "reactor", token=token.0; "Handing over transport");
+ log::debug!(target: "reactor", token=token.0; "Handing over transport {transport:?} with token {}", token.0);
self.service.handover_transport(token, transport);
}
Action::Send(token, data) => {
- log::trace!(target: "reactor", "Sending {} bytes to {token:?}", data.len());
+ log::trace!(target: "reactor", token=token.0; "Sending {} bytes to {token:?}", data.len());
if let Some(transport) = self.transports.get_mut(&token) {
if let Err(e) = transport.write_atomic(&data) {
log::error!(target: "reactor", "Fatal error writing to transport {token:?}, disconnecting. Error details: {e:?}");
- if let Some(transport) = self.unregister_transport(token) {
+ if let Some(transport) = self.deregister_transport(token) {
return Err(Error::TransportDisconnect(token, transport));
}
}
} else {
- log::error!(target: "reactor", "Transport {token:?} is not in the reactor");
+ log::error!(target: "reactor", token=token.0; "No transport with token {token:?} is known!");
}
}
Action::SetTimer(duration) => {
@@ -562,27 +568,27 @@ impl<H: ReactionHandler> Runtime<H> {
log::info!(target: "reactor", "Shutdown");
}
- fn unregister_listener(&mut self, token: Token) -> Option<H::Listener> {
+ fn deregister_listener(&mut self, token: Token) -> Option<H::Listener> {
let Some(mut source) = self.listeners.remove(&token) else {
- log::warn!(target: "reactor", token=token.0; "Unregistering non-registered listener");
+ log::warn!(target: "reactor", token=token.0; "Deregistering non-registered listener with token {}", token.0);
return None;
};
if let Err(err) = self.poll.registry().deregister(&mut source) {
- log::warn!(target: "reactor", token=token.0; "Failed to deregister listener from mio: {err}");
+ log::warn!(target: "reactor", token=token.0; "Failed to deregister listener with token {} from mio: {err}", token.0);
}
Some(source)
}
- fn unregister_transport(&mut self, token: Token) -> Option<H::Transport> {
+ fn deregister_transport(&mut self, token: Token) -> Option<H::Transport> {
let Some(mut source) = self.transports.remove(&token) else {
- log::warn!(target: "reactor", token=token.0; "Unregistering non-registered transport");
+ log::warn!(target: "reactor", token=token.0; "Deregistering non-registered transport with token {}", token.0);
return None;
};
if let Err(err) = self.poll.registry().deregister(&mut source) {
- log::warn!(target: "reactor", token=token.0; "Failed to deregister transport from mio: {err}");
+ log::warn!(target: "reactor", token=token.0; "Failed to deregister transport with token {} from mio: {err}", token.0);
}
Some(source)
diff --git a/crates/radicle-node/src/reactor/session.rs b/crates/radicle-node/src/reactor/session.rs
index 38bcdcf1..74885e05 100644
--- a/crates/radicle-node/src/reactor/session.rs
+++ b/crates/radicle-node/src/reactor/session.rs
@@ -100,7 +100,7 @@ impl<M: StateMachine, S: Session> Display for ProtocolArtifact<M, S> {
}
}
-#[derive(Copy, Clone, Eq, PartialEq)]
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub struct Protocol<M: StateMachine, S: Session> {
pub(crate) state: M,
pub(crate) session: S,
diff --git a/crates/radicle-node/src/runtime.rs b/crates/radicle-node/src/runtime.rs
index 7b24c994..115ef587 100644
--- a/crates/radicle-node/src/runtime.rs
+++ b/crates/radicle-node/src/runtime.rs
@@ -1,6 +1,7 @@
pub mod handle;
pub mod thread;
+use std::fmt::Debug;
use std::path::PathBuf;
use std::{fs, io, net};
@@ -131,7 +132,11 @@ impl Runtime {
signer: Device<G>,
) -> Result<Runtime, Error>
where
- G: crypto::signature::Signer<crypto::Signature> + Ecdh<Pk = NodeId> + Clone + 'static,
+ G: crypto::signature::Signer<crypto::Signature>
+ + Ecdh<Pk = NodeId>
+ + Clone
+ + Debug
+ + 'static,
{
let id = *signer.public_key();
let alias = config.alias.clone();
diff --git a/crates/radicle-node/src/test/node.rs b/crates/radicle-node/src/test/node.rs
index f15109d7..2256b7cd 100644
--- a/crates/radicle-node/src/test/node.rs
+++ b/crates/radicle-node/src/test/node.rs
@@ -1,3 +1,4 @@
+use std::fmt::Debug;
use std::io::BufRead as _;
use std::mem::ManuallyDrop;
use std::path::Path;
@@ -455,7 +456,7 @@ impl Node<MockSigner> {
}
}
-impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer<Signature> + Clone> Node<G> {
+impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer<Signature> + Clone + Debug> Node<G> {
/// Spawn a node in its own thread.
pub fn spawn(self) -> NodeHandle<G> {
let alias = self.config.alias.clone();
diff --git a/crates/radicle-node/src/wire.rs b/crates/radicle-node/src/wire.rs
index 9aeded65..1cb4b330 100644
--- a/crates/radicle-node/src/wire.rs
+++ b/crates/radicle-node/src/wire.rs
@@ -3,6 +3,7 @@
//! We use the Noise XK handshake pattern to establish an encrypted stream with a remote peer.
use std::collections::hash_map::Entry;
use std::collections::VecDeque;
+use std::fmt::Debug;
use std::sync::Arc;
use std::{io, net, time};
@@ -490,7 +491,7 @@ impl<D, S, G> reactor::ReactionHandler for Wire<D, S, G>
where
D: service::Store + Send,
S: WriteStorage + Send + 'static,
- G: crypto::signature::Signer<crypto::Signature> + Ecdh<Pk = NodeId> + Clone + Send,
+ G: crypto::signature::Signer<crypto::Signature> + Ecdh<Pk = NodeId> + Clone + Send + Debug,
{
type Listener = Listener;
type Transport = Transport<WireSession<G>>;
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 50c695a2-c889-41bd-91b8-22c7c202686a -v /opt/radcis/ci.rad.levitte.org/cci/state/50c695a2-c889-41bd-91b8-22c7c202686a/s:/50c695a2-c889-41bd-91b8-22c7c202686a/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/50c695a2-c889-41bd-91b8-22c7c202686a/w:/50c695a2-c889-41bd-91b8-22c7c202686a/w -w /50c695a2-c889-41bd-91b8-22c7c202686a/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:bookworm bash /50c695a2-c889-41bd-91b8-22c7c202686a/s/script.sh
+ export 'RUSTDOCFLAGS=-D warnings'
+ RUSTDOCFLAGS='-D warnings'
+ cargo --version
info: syncing channel updates for '1.88-x86_64-unknown-linux-gnu'
info: latest update on 2025-06-26, rust version 1.88.0 (6b00bc388 2025-06-23)
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.88.0 (873a06493 2025-05-10)
+ rustc --version
rustc 1.88.0 (6b00bc388 2025-06-23)
+ cargo fmt --check
Diff in /50c695a2-c889-41bd-91b8-22c7c202686a/w/crates/radicle-node/src/reactor.rs:452:
self.service.listener_reacted(token, service_event, time);
});
} else {
- let listener = self.deregister_listener(token).unwrap_or_else(|| panic!("listener with token {} has disappeared", token.0));
+ let listener = self.deregister_listener(token).unwrap_or_else(|| {
+ panic!("listener with token {} has disappeared", token.0)
+ });
self.service
.handle_error(Error::ListenerDisconnect(token, listener));
deregistered.push(token);
Diff in /50c695a2-c889-41bd-91b8-22c7c202686a/w/crates/radicle-node/src/reactor.rs:471:
self.service.transport_reacted(token, service_event, time);
});
} else {
- let transport = self.deregister_transport(token).unwrap_or_else(|| panic!("transport with token {} has disappeared", token.0));
+ let transport = self.deregister_transport(token).unwrap_or_else(|| {
+ panic!("transport with token {} has disappeared", token.0)
+ });
self.service
.handle_error(Error::TransportDisconnect(token, transport));
deregistered.push(token);
Exit code: 1
{
"response": "finished",
"result": "failure"
}