A long-running rc.33 daemon reached OOM with roughly 22 GiB resident and 24 GiB swapped. Live inspection found 40 GiB and 10/20 GiB anonymous container mappings; the active worker stack and tracing metadata identified iroh's scheduled path-open retry loop. In iroh 1.2.0 every failed connection requeues the same path, while each retry visits every connection again. Separate control and data connections multiply the queue geometrically. Vendor the published iroh 1.2.0 source and select it with a workspace Cargo patch. Deduplicate retry admission and cap distinct pending paths at 64, evicting the oldest before inserting a new candidate. Keep the current and next retry batches bounded without combining control/data connections or changing wire protocols, identity derivation, authentication or state. Document source provenance, the live evidence and removal conditions. Upstream PR 4551 merged on October 6, but inspection of the official 1.3.0 crate still shows the unconditional enqueue, so that upgrade alone is not a fix. No version bump, release tag or installed-service change. The default workspace suite includes the exact vendored admission module. Three regressions failed with the original enqueue and pass with the fix: repeated failures across connections, duplicate admission at capacity, and 10,000 distinct candidates with bounded length/capacity and FIFO eviction. Transport integration tests continue using real endpoints and encryption. Checks on Linux in nix develop: - cargo fmt --all -- --check - cargo clippy --locked --workspace --all-targets -- -D warnings - cargo test --locked --workspace --all-targets, with only the inherited TSUNAGI_CONTROL_SOCKET removed from the test environment - git diff --cached --check Initial full-suite runs failed six DNS readiness checks because the inherited system socket path overrode their temporary state directories. Clearing that environment variable resolves those failures without changing application code. Root/network-dependent ignored tests and other operating systems were not run.
122 lines
4.8 KiB
Rust
122 lines
4.8 KiB
Rust
//! Very basic example showing how to implement a basic echo protocol,
|
|
//! without using the `Router` API. (For the router version, check out the echo.rs example.)
|
|
//!
|
|
//! The echo protocol echos any data sent to it in the first stream.
|
|
//!
|
|
//! ## Running the Example
|
|
//!
|
|
//! cargo run --example echo-no-router
|
|
|
|
use iroh::{Endpoint, EndpointAddr, endpoint::presets};
|
|
use n0_error::{AnyError as Error, Result, StdResultExt};
|
|
|
|
/// Each protocol is identified by its ALPN string.
|
|
///
|
|
/// The ALPN, or application-layer protocol negotiation, is exchanged in the connection handshake,
|
|
/// and the connection is aborted unless both endpoints pass the same bytestring.
|
|
const ALPN: &[u8] = b"iroh-example/echo/0";
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let endpoint = start_accept_side().await?;
|
|
|
|
// wait for the endpoint to be online
|
|
endpoint.online().await;
|
|
|
|
connect_side(endpoint.addr()).await?;
|
|
|
|
// This makes sure the endpoint is closed properly and connections close gracefully
|
|
// and will indirectly close the tasks spawned by `start_accept_side`.
|
|
endpoint.close().await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn connect_side(addr: EndpointAddr) -> Result<()> {
|
|
let endpoint = Endpoint::bind(presets::N0).await?;
|
|
|
|
// Open a connection to the accepting endpoint
|
|
let conn = endpoint.connect(addr, ALPN).await?;
|
|
|
|
// Open a bidirectional QUIC stream
|
|
let (mut send, mut recv) = conn.open_bi().await.anyerr()?;
|
|
|
|
// Send some data to be echoed
|
|
send.write_all(b"Hello, world!").await.anyerr()?;
|
|
|
|
// Signal the end of data for this particular stream
|
|
send.finish().anyerr()?;
|
|
|
|
// Receive the echo, but limit reading up to maximum 1000 bytes
|
|
let response = recv.read_to_end(1000).await.anyerr()?;
|
|
assert_eq!(&response, b"Hello, world!");
|
|
|
|
// Explicitly close the whole connection.
|
|
conn.close(0u32.into(), b"bye!");
|
|
|
|
// The above call only queues a close message to be sent (see how it's not async!).
|
|
// We need to actually call this to make sure this message is sent out.
|
|
endpoint.close().await;
|
|
// If we don't call this, but continue using the endpoint, then the queued
|
|
// close call will eventually be picked up and sent.
|
|
// But always try to wait for endpoint.close().await to go through before dropping
|
|
// the endpoint to ensure any queued messages are sent through and connections are
|
|
// closed gracefully.
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn start_accept_side() -> Result<Endpoint> {
|
|
let endpoint = Endpoint::builder(presets::N0)
|
|
// The accept side needs to opt-in to the protocols it accepts,
|
|
// as any connection attempts that can't be found with a matching ALPN
|
|
// will be rejected.
|
|
.alpns(vec![ALPN.to_vec()])
|
|
.bind()
|
|
.await?;
|
|
|
|
// spawn a task so that `start_accept_side` returns immediately and we can continue in main().
|
|
tokio::spawn({
|
|
let endpoint = endpoint.clone();
|
|
async move {
|
|
// This task won't leak, because we call `endpoint.close()` in `main()`,
|
|
// which causes `endpoint.accept().await` to return `None`.
|
|
// In a more serious environment, we recommend avoiding `tokio::spawn` and use either a `TaskTracker` or
|
|
// `JoinSet` instead to make sure you're not accidentally leaking tasks.
|
|
while let Some(incoming) = endpoint.accept().await {
|
|
// spawn a task for each incoming connection, so we can serve multiple connections asynchronously
|
|
tokio::spawn(async move {
|
|
let connection = incoming.await.anyerr()?;
|
|
|
|
// We can get the remote's endpoint id from the connection.
|
|
let endpoint_id = connection.remote_id();
|
|
println!("accepted connection from {endpoint_id}");
|
|
|
|
// Our protocol is a simple request-response protocol, so we expect the
|
|
// connecting peer to open a single bi-directional stream.
|
|
let (mut send, mut recv) = connection.accept_bi().await.anyerr()?;
|
|
|
|
// Echo any bytes received back directly.
|
|
// This will keep copying until the sender signals the end of data on the stream.
|
|
let bytes_sent = tokio::io::copy(&mut recv, &mut send).await.anyerr()?;
|
|
println!("Copied over {bytes_sent} byte(s)");
|
|
|
|
// By calling `finish` on the send stream we signal that we will not send anything
|
|
// further, which makes the receive stream on the other end terminate.
|
|
send.finish().anyerr()?;
|
|
|
|
// Wait until the remote closes the connection, which it does once it
|
|
// received the response.
|
|
connection.closed().await;
|
|
|
|
Ok::<_, Error>(())
|
|
});
|
|
}
|
|
|
|
Ok::<_, Error>(())
|
|
}
|
|
});
|
|
|
|
Ok(endpoint)
|
|
}
|