valeriansaliou/sonic · critical · std::io::Error
BrokenPipe
Error message
BrokenPipe
What it means
attach sends a MultiplexerTask::Attach message to the multiplexer task with send_timeout(SEND_TIMEOUT). A timeout means the multiplexer task is not running, so the new connection cannot be registered; it is surfaced as ErrorKind::BrokenPipe. connect() calls this, so a failed attach aborts connection establishment.
Source
Thrown at client/src/multiplexer.rs:56
let poll_waker = Arc::new(mio::Waker::new(poll.registry(), mio::Token(usize::MAX))?);
// TODO: Do not auto-start? So one can spawn the task differently or
// observe events (e.g. in tests)?
let event_loop_handle = std::thread::spawn(move || run_event_loop(&mut poll, rx));
Ok(Self {
_event_loop_handle: event_loop_handle,
tx,
poll_waker,
})
}
pub(crate) fn attach<C: SonicConnectionTrait + 'static>(&self, conn: C) -> std::io::Result<()> {
if let Err(error) =
(self.tx).send_timeout(MultiplexerTask::Attach(Box::new(conn)), SEND_TIMEOUT)
{
return Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
error.to_string(),
));
};
self.poll_waker.wake()
}
}
pub(crate) trait SonicConnectionTrait: AsMut<mio::net::TcpStream> + Send {
fn wants_to_write(&self) -> bool;
fn wants_to_read(&self) -> bool;
fn interest(&self) -> Option<mio::Interest> {
match (self.wants_to_write(), self.wants_to_read()) {
(false, false) => None,
(true, false) => Some(mio::Interest::WRITABLE),
(false, true) => Some(mio::Interest::READABLE),View on GitHub (pinned to e6a72da6a5)
Solutions
- Recreate the multiplexer/client instead of reusing a dead one; attach requires a live multiplexer task.
- Ensure connect() is not called after the client was shut down/dropped; guard with a state check.
- Keep the runtime alive while connections are being established.
- If timeouts are marginal under load, ensure the multiplexer task isn't starved rather than raising SEND_TIMEOUT blindly.
Example fix
// before
let conn = MyConn::connect(addr)?;
multiplexer.attach(conn)?;
// after
match multiplexer.attach(conn) {
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => {
// multiplexer task is dead; rebuild the client
let client = Client::connect(addr)?;
}
Ok(()) => {}
} Defensive patterns
Strategy: try-catch
Validate before calling
// Attach only to a live multiplexer:
if multiplexer_task_handle.is_finished() {
return Err("multiplexer task dead; rebuild the client".into());
} Type guard
fn is_broken_pipe(e: &std::io::Error) -> bool {
e.kind() == std::io::ErrorKind::BrokenPipe
} Try / catch
match multiplexer.attach(conn) {
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => {
// multiplexer is gone: rebuild client from scratch
let client = Client::connect(addr)?;
}
other => other?,
} Prevention
- Never call connect/attach on a client whose multiplexer task already exited
- Serialize shutdown vs. new connection setup to avoid races
- Rebuild the whole client (not just the connection) after multiplexer death
- Keep the runtime alive during connection establishment
When it happens
Trigger: Calling connect (which calls attach) when the multiplexer task has already exited — e.g. the previous connection killed it, the runtime shut it down, or attach is invoked after multiplexer shutdown; also when the multiplexer mailbox is full and never drains.
Common situations: Reconnect attempts after the multiplexer task died from a prior connection error; attaching connections from multiple threads during shutdown; runtime teardown racing with new connect() calls.
Related errors
AI-assisted analysis of valeriansaliou/sonic@e6a72da6a5 (2026-09-01).
Data as JSON: /api/errors/046a889ab5a83bca.
Report an issue: GitHub.