Direct mycelium runs and our pkexec spawns are both healthy (sidecar logs show acquired routes streaming for 20+s). Yet our reqwest poller can't reach 127.0.0.1:port after the first successful request. Smoking gun: failure happens ~10s after the first reply — exactly when an idle keep-alive connection would have been reaped. Disable pooling (pool_max_idle_per_host(0)) so every call opens a fresh TCP connection. Loopback overhead is negligible (~50us per request) and we're immune to server-side idle closes. Also pin connect_timeout to 3s so a wedged half-open doesn't block for the full 10s request timeout.
80 lines
2.3 KiB
Rust
80 lines
2.3 KiB
Rust
pub mod admin;
|
|
pub mod messages;
|
|
pub mod peers;
|
|
pub mod pubkey;
|
|
pub mod routes;
|
|
pub mod topics;
|
|
|
|
use crate::error::{AppError, AppResult};
|
|
use reqwest::{Client, Response};
|
|
use serde::de::DeserializeOwned;
|
|
use std::time::Duration;
|
|
|
|
/// Thin REST client for the mycelium daemon's HTTP API.
|
|
///
|
|
/// The base URL is set after the sidecar reports ready; clients are cheap
|
|
/// to clone (the inner `reqwest::Client` keeps a shared connection pool).
|
|
#[derive(Debug, Clone)]
|
|
pub struct MyceliumClient {
|
|
base: String,
|
|
http: Client,
|
|
}
|
|
|
|
impl MyceliumClient {
|
|
pub fn new(base: impl Into<String>) -> Self {
|
|
// Mycelium's HTTP server seems to drop idle keep-alive connections
|
|
// around the 10s mark; reusing a pooled stale connection surfaces
|
|
// as a generic "error sending request" once `start_daemon`
|
|
// returned. Open a fresh TCP connection per request — overhead is
|
|
// negligible on loopback and immune to server-side closes.
|
|
let http = Client::builder()
|
|
.pool_max_idle_per_host(0)
|
|
.timeout(Duration::from_secs(10))
|
|
.connect_timeout(Duration::from_secs(3))
|
|
.build()
|
|
.expect("reqwest client build");
|
|
Self {
|
|
base: base.into(),
|
|
http,
|
|
}
|
|
}
|
|
|
|
pub fn base_url(&self) -> &str {
|
|
&self.base
|
|
}
|
|
|
|
pub(crate) fn url(&self, path: &str) -> String {
|
|
format!("{}/api/v1{}", self.base, path)
|
|
}
|
|
|
|
pub(crate) async fn parse<T: DeserializeOwned>(resp: Response) -> AppResult<T> {
|
|
let status = resp.status();
|
|
if status.is_success() {
|
|
resp.json::<T>().await.map_err(AppError::from)
|
|
} else {
|
|
let body = resp.text().await.unwrap_or_default();
|
|
Err(AppError::DaemonStatus {
|
|
status: status.as_u16(),
|
|
body,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn check_status(resp: Response) -> AppResult<()> {
|
|
let status = resp.status();
|
|
if status.is_success() {
|
|
Ok(())
|
|
} else {
|
|
let body = resp.text().await.unwrap_or_default();
|
|
Err(AppError::DaemonStatus {
|
|
status: status.as_u16(),
|
|
body,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub(crate) fn http(&self) -> &Client {
|
|
&self.http
|
|
}
|
|
}
|