Skip to content

Commit 4bdfe0f

Browse files
rpc: expire stalled client connections
Bound HTTP header and body reads to 30 seconds so idle TCP and Unix clients release the shared connection cap. Preserve complete long-poll requests and verify recovery through the socket journeys.
1 parent 6e6dc9a commit 4bdfe0f

6 files changed

Lines changed: 207 additions & 118 deletions

File tree

‎Cargo.lock‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎changelog.d/rpc-read-timeout.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fixed
2+
3+
- RPC connections now expire stalled headers, request bodies, and idle
4+
keep-alive sockets after 30 seconds, freeing capacity for TCP and Unix-socket
5+
clients. Complete long-poll RPC requests keep their existing deadlines.

‎crates/rbitcoin-rpc/Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ rbitcoin-net = { workspace = true }
2525
rbitcoin-consensus = { workspace = true }
2626
bitcoin = { workspace = true }
2727
axum = { workspace = true }
28+
hyper = { version = "1", features = ["http1", "server"] }
29+
hyper-util = { version = "0.1", features = ["tokio", "service"] }
2830
tokio = { workspace = true }
2931
serde = { workspace = true }
3032
serde_json = { workspace = true, features = ["arbitrary_precision"] }
@@ -33,6 +35,7 @@ base64 = "0.23"
3335

3436
[dev-dependencies]
3537
rbitcoin-consensus = { workspace = true }
38+
tokio = { workspace = true, features = ["test-util"] }
3639

3740
[[test]]
3841
name = "suite"

‎crates/rbitcoin-rpc/src/server.rs‎

Lines changed: 54 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ use crate::auth::{
66
};
77
use crate::methods::{RpcContext, RpcRegtest};
88
use axum::body::Bytes;
9-
use axum::extract::{DefaultBodyLimit, State};
9+
use axum::extract::{DefaultBodyLimit, FromRequest, Request, State};
1010
use axum::http::{header, HeaderMap, StatusCode};
1111
use axum::middleware::{from_fn_with_state, Next};
1212
use axum::response::{IntoResponse, Response};
1313
use axum::routing::{get, post};
1414
use axum::Router;
15+
use hyper::server::conn::http1;
16+
use hyper_util::rt::{TokioIo, TokioTimer};
17+
use hyper_util::service::TowerToHyperService;
1518

1619
/// Axum's default request-body cap, named so auth and 413 share one limit.
1720
pub const RPC_MAX_HTTP_BODY: usize = 2 * 1024 * 1024;
@@ -20,6 +23,7 @@ pub const DEFAULT_RPC_WORK_QUEUE: usize = 16;
2023
/// Accept cap, copied from the Electrum public listener. Not a knob.
2124
#[doc(hidden)]
2225
pub const RPC_MAX_CONNECTIONS: usize = 256;
26+
const RPC_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
2327
use rbitcoin_log::info;
2428
#[cfg(unix)]
2529
use rbitcoin_log::warn;
@@ -28,14 +32,11 @@ use rbitcoin_primitives::Network;
2832
use rbitcoin_query::Query;
2933
use std::net::SocketAddr;
3034
use std::path::PathBuf;
31-
use std::pin::Pin;
3235
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
3336
use std::sync::Arc;
34-
use std::task::{Context, Poll};
3537
use std::time::Instant;
36-
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
3738
use tokio::net::TcpListener;
38-
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
39+
use tokio::sync::Semaphore;
3940

4041
#[cfg(unix)]
4142
#[doc(hidden)]
@@ -48,83 +49,40 @@ pub fn bind_unix_mode(
4849
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))?;
4950
Ok(listener)
5051
}
51-
use tokio::task::JoinHandle;
52+
use tokio::task::{JoinHandle, JoinSet};
5253

53-
/// One accepted connection. The permit is the accept slot and dies with the socket.
54-
#[doc(hidden)]
55-
pub struct HeldConn<S> {
56-
pub io: S,
57-
pub _permit: OwnedSemaphorePermit,
58-
}
59-
60-
impl<S: AsyncRead + Unpin> AsyncRead for HeldConn<S> {
61-
fn poll_read(
62-
self: Pin<&mut Self>,
63-
cx: &mut Context<'_>,
64-
buf: &mut ReadBuf<'_>,
65-
) -> Poll<std::io::Result<()>> {
66-
Pin::new(&mut self.get_mut().io).poll_read(cx, buf)
67-
}
68-
}
69-
70-
impl<S: AsyncWrite + Unpin> AsyncWrite for HeldConn<S> {
71-
fn poll_write(
72-
self: Pin<&mut Self>,
73-
cx: &mut Context<'_>,
74-
buf: &[u8],
75-
) -> Poll<std::io::Result<usize>> {
76-
Pin::new(&mut self.get_mut().io).poll_write(cx, buf)
77-
}
78-
79-
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
80-
Pin::new(&mut self.get_mut().io).poll_flush(cx)
81-
}
82-
83-
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
84-
Pin::new(&mut self.get_mut().io).poll_shutdown(cx)
85-
}
86-
}
87-
88-
/// Drops a new socket when [`RPC_MAX_CONNECTIONS`] are already open.
89-
#[doc(hidden)]
90-
pub struct AcceptCap<L> {
91-
pub listener: L,
92-
pub sem: Arc<Semaphore>,
93-
}
94-
95-
impl<L> axum::serve::Listener for AcceptCap<L>
54+
async fn serve_rpc<L>(mut listener: L, app: Router, sem: Arc<Semaphore>, shutdown: Arc<AtomicBool>)
9655
where
9756
L: axum::serve::Listener,
98-
L::Io: AsyncRead + AsyncWrite + Unpin,
9957
L::Addr: std::fmt::Debug,
10058
{
101-
type Io = HeldConn<L::Io>;
102-
type Addr = L::Addr;
103-
104-
async fn accept(&mut self) -> (Self::Io, Self::Addr) {
105-
loop {
106-
let (io, addr) = axum::serve::Listener::accept(&mut self.listener).await;
107-
match self.sem.clone().try_acquire_owned() {
108-
Ok(permit) => {
109-
return (
110-
HeldConn {
111-
io,
112-
_permit: permit,
113-
},
114-
addr,
115-
)
59+
let mut connections = JoinSet::new();
60+
let mut shutdown_tick = tokio::time::interval(std::time::Duration::from_millis(50));
61+
loop {
62+
tokio::select! {
63+
_ = shutdown_tick.tick() => {
64+
if shutdown.load(Ordering::SeqCst) {
65+
break;
11666
}
117-
Err(_) => {
67+
}
68+
Some(_) = connections.join_next(), if !connections.is_empty() => {}
69+
(io, addr) = listener.accept() => {
70+
let Ok(permit) = sem.clone().try_acquire_owned() else {
11871
info!("rpc: reject {addr:?} (at max connections={RPC_MAX_CONNECTIONS})");
119-
drop(io);
120-
}
72+
continue;
73+
};
74+
let service = TowerToHyperService::new(app.clone());
75+
connections.spawn(async move {
76+
let _permit = permit;
77+
let _ = http1::Builder::new()
78+
.timer(TokioTimer::new())
79+
.header_read_timeout(RPC_READ_TIMEOUT)
80+
.serve_connection(TokioIo::new(io), service)
81+
.await;
82+
});
12183
}
12284
}
12385
}
124-
125-
fn local_addr(&self) -> std::io::Result<Self::Addr> {
126-
self.listener.local_addr()
127-
}
12886
}
12987

13088
/// RPC listen configuration.
@@ -288,22 +246,9 @@ pub async fn run_rpc(
288246
let app = rpc_app(state);
289247
let shutdown_w = Arc::clone(&shutdown);
290248
let accept_sem = Arc::clone(&accept_sem);
291-
tasks.push(tokio::spawn(async move {
292-
axum::serve(
293-
AcceptCap {
294-
listener,
295-
sem: accept_sem,
296-
},
297-
app,
298-
)
299-
.with_graceful_shutdown(async move {
300-
while !shutdown_w.load(Ordering::SeqCst) {
301-
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
302-
}
303-
})
304-
.await
305-
.ok();
306-
}));
249+
tasks.push(tokio::spawn(serve_rpc(
250+
listener, app, accept_sem, shutdown_w,
251+
)));
307252
info!(
308253
"rpc: HTTP JSON-RPC on {bound} (bearer token {})",
309254
token_path.display()
@@ -354,22 +299,9 @@ pub async fn run_rpc(
354299
let app = rpc_app(state);
355300
let shutdown_w = Arc::clone(&shutdown);
356301
let accept_sem = Arc::clone(&accept_sem);
357-
tasks.push(tokio::spawn(async move {
358-
axum::serve(
359-
AcceptCap {
360-
listener,
361-
sem: accept_sem,
362-
},
363-
app,
364-
)
365-
.with_graceful_shutdown(async move {
366-
while !shutdown_w.load(Ordering::SeqCst) {
367-
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
368-
}
369-
})
370-
.await
371-
.ok();
372-
}));
302+
tasks.push(tokio::spawn(serve_rpc(
303+
listener, app, accept_sem, shutdown_w,
304+
)));
373305
info!("rpc: unix JSON-RPC on {}", sock.display());
374306
Some(sock.clone())
375307
} else {
@@ -422,9 +354,10 @@ pub async fn rest_entry(State(state): State<AppState>, req: axum::extract::Reque
422354
// and REST does not use the RPC work queue.
423355
let path = req.uri().path().to_string();
424356
let query = req.uri().query().unwrap_or("").to_string();
425-
let body = axum::body::to_bytes(req.into_body(), RPC_MAX_HTTP_BODY)
426-
.await
427-
.unwrap_or_default();
357+
let body = match read_http_body(req).await {
358+
Ok(body) => body,
359+
Err(response) => return response,
360+
};
428361
let _permit = match state.rest_queue.try_acquire() {
429362
Ok(p) => p,
430363
Err(_) => {
@@ -623,7 +556,20 @@ pub async fn recv_tip(tips: &mut Option<tokio::sync::broadcast::Receiver<rbitcoi
623556
}
624557
}
625558

626-
pub async fn rpc_post(State(state): State<AppState>, body: Bytes) -> Response {
559+
async fn read_http_body(req: Request) -> Result<Bytes, Response> {
560+
match tokio::time::timeout(RPC_READ_TIMEOUT, Bytes::from_request(req, &())).await {
561+
Ok(body) => body.map_err(IntoResponse::into_response),
562+
Err(_) => {
563+
Err((StatusCode::REQUEST_TIMEOUT, [(header::CONNECTION, "close")]).into_response())
564+
}
565+
}
566+
}
567+
568+
pub async fn rpc_post(State(state): State<AppState>, req: Request) -> Response {
569+
let body = match read_http_body(req).await {
570+
Ok(body) => body,
571+
Err(response) => return response,
572+
};
627573
let parsed: serde_json::Value = match serde_json::from_slice(&body) {
628574
Ok(v) => v,
629575
Err(_) => {

0 commit comments

Comments
 (0)