Skip to content

Commit c7752f3

Browse files
authored
feat: Add API for poisoning connections (#121)
This is a port of hyperium/hyper#3145 from hyper v0.14.x. It introduces a PoisonPill atomic onto connection info. When set to true, this prevents the connection from being returned to the pool.
1 parent 7afb1ed commit c7752f3

File tree

3 files changed

+129
-3
lines changed

3 files changed

+129
-3
lines changed

src/client/legacy/client.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,10 @@ impl<B> PoolClient<B> {
750750
}
751751
}
752752

753+
fn is_poisoned(&self) -> bool {
754+
self.conn_info.poisoned.poisoned()
755+
}
756+
753757
fn is_ready(&self) -> bool {
754758
match self.tx {
755759
#[cfg(feature = "http1")]
@@ -826,7 +830,7 @@ where
826830
B: Send + 'static,
827831
{
828832
fn is_open(&self) -> bool {
829-
self.is_ready()
833+
!self.is_poisoned() && self.is_ready()
830834
}
831835

832836
fn reserve(self) -> pool::Reservation<Self> {

src/client/legacy/connect/mod.rs

+52-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,13 @@
6262
//! [`Read`]: hyper::rt::Read
6363
//! [`Write`]: hyper::rt::Write
6464
//! [`Connection`]: Connection
65-
use std::fmt;
65+
use std::{
66+
fmt::{self, Formatter},
67+
sync::{
68+
atomic::{AtomicBool, Ordering},
69+
Arc,
70+
},
71+
};
6672

6773
use ::http::Extensions;
6874

@@ -94,6 +100,39 @@ pub struct Connected {
94100
pub(super) alpn: Alpn,
95101
pub(super) is_proxied: bool,
96102
pub(super) extra: Option<Extra>,
103+
pub(super) poisoned: PoisonPill,
104+
}
105+
106+
#[derive(Clone)]
107+
pub(crate) struct PoisonPill {
108+
poisoned: Arc<AtomicBool>,
109+
}
110+
111+
impl fmt::Debug for PoisonPill {
112+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
113+
// print the address of the pill—this makes debugging issues much easier
114+
write!(
115+
f,
116+
"PoisonPill@{:p} {{ poisoned: {} }}",
117+
self.poisoned,
118+
self.poisoned.load(Ordering::Relaxed)
119+
)
120+
}
121+
}
122+
123+
impl PoisonPill {
124+
pub(crate) fn healthy() -> Self {
125+
Self {
126+
poisoned: Arc::new(AtomicBool::new(false)),
127+
}
128+
}
129+
pub(crate) fn poison(&self) {
130+
self.poisoned.store(true, Ordering::Relaxed)
131+
}
132+
133+
pub(crate) fn poisoned(&self) -> bool {
134+
self.poisoned.load(Ordering::Relaxed)
135+
}
97136
}
98137

99138
pub(super) struct Extra(Box<dyn ExtraInner>);
@@ -111,6 +150,7 @@ impl Connected {
111150
alpn: Alpn::None,
112151
is_proxied: false,
113152
extra: None,
153+
poisoned: PoisonPill::healthy(),
114154
}
115155
}
116156

@@ -170,13 +210,24 @@ impl Connected {
170210
self.alpn == Alpn::H2
171211
}
172212

213+
/// Poison this connection
214+
///
215+
/// A poisoned connection will not be reused for subsequent requests by the pool
216+
pub fn poison(&self) {
217+
self.poisoned.poison();
218+
tracing::debug!(
219+
poison_pill = ?self.poisoned, "connection was poisoned. this connection will not be reused for subsequent requests"
220+
);
221+
}
222+
173223
// Don't public expose that `Connected` is `Clone`, unsure if we want to
174224
// keep that contract...
175225
pub(super) fn clone(&self) -> Connected {
176226
Connected {
177227
alpn: self.alpn,
178228
is_proxied: self.is_proxied,
179229
extra: self.extra.clone(),
230+
poisoned: self.poisoned.clone(),
180231
}
181232
}
182233
}

tests/legacy_client.rs

+72-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::io::{Read, Write};
44
use std::net::{SocketAddr, TcpListener};
55
use std::pin::Pin;
66
use std::sync::atomic::Ordering;
7+
use std::sync::Arc;
78
use std::task::Poll;
89
use std::thread;
910
use std::time::Duration;
@@ -891,7 +892,6 @@ fn capture_connection_on_client() {
891892
let addr = server.local_addr().unwrap();
892893
thread::spawn(move || {
893894
let mut sock = server.accept().unwrap().0;
894-
//drop(server);
895895
sock.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
896896
sock.set_write_timeout(Some(Duration::from_secs(5)))
897897
.unwrap();
@@ -908,3 +908,74 @@ fn capture_connection_on_client() {
908908
rt.block_on(client.request(req)).expect("200 OK");
909909
assert!(captured_conn.connection_metadata().is_some());
910910
}
911+
912+
#[cfg(not(miri))]
913+
#[test]
914+
fn connection_poisoning() {
915+
use std::sync::atomic::AtomicUsize;
916+
917+
let _ = pretty_env_logger::try_init();
918+
919+
let rt = runtime();
920+
let connector = DebugConnector::new();
921+
922+
let client = Client::builder(TokioExecutor::new()).build(connector);
923+
924+
let server = TcpListener::bind("127.0.0.1:0").unwrap();
925+
let addr = server.local_addr().unwrap();
926+
let num_conns: Arc<AtomicUsize> = Default::default();
927+
let num_requests: Arc<AtomicUsize> = Default::default();
928+
let num_requests_tracker = num_requests.clone();
929+
let num_conns_tracker = num_conns.clone();
930+
thread::spawn(move || loop {
931+
let mut sock = server.accept().unwrap().0;
932+
num_conns_tracker.fetch_add(1, Ordering::Relaxed);
933+
let num_requests_tracker = num_requests_tracker.clone();
934+
thread::spawn(move || {
935+
sock.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
936+
sock.set_write_timeout(Some(Duration::from_secs(5)))
937+
.unwrap();
938+
let mut buf = [0; 4096];
939+
loop {
940+
if sock.read(&mut buf).expect("read 1") > 0 {
941+
num_requests_tracker.fetch_add(1, Ordering::Relaxed);
942+
sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")
943+
.expect("write 1");
944+
}
945+
}
946+
});
947+
});
948+
let make_request = || {
949+
Request::builder()
950+
.uri(&*format!("http://{}/a", addr))
951+
.body(Empty::<Bytes>::new())
952+
.unwrap()
953+
};
954+
let mut req = make_request();
955+
let captured_conn = capture_connection(&mut req);
956+
rt.block_on(client.request(req)).expect("200 OK");
957+
assert_eq!(num_conns.load(Ordering::SeqCst), 1);
958+
assert_eq!(num_requests.load(Ordering::SeqCst), 1);
959+
960+
rt.block_on(client.request(make_request())).expect("200 OK");
961+
rt.block_on(client.request(make_request())).expect("200 OK");
962+
// Before poisoning the connection is reused
963+
assert_eq!(num_conns.load(Ordering::SeqCst), 1);
964+
assert_eq!(num_requests.load(Ordering::SeqCst), 3);
965+
captured_conn
966+
.connection_metadata()
967+
.as_ref()
968+
.unwrap()
969+
.poison();
970+
971+
rt.block_on(client.request(make_request())).expect("200 OK");
972+
973+
// After poisoning, a new connection is established
974+
assert_eq!(num_conns.load(Ordering::SeqCst), 2);
975+
assert_eq!(num_requests.load(Ordering::SeqCst), 4);
976+
977+
rt.block_on(client.request(make_request())).expect("200 OK");
978+
// another request can still reuse:
979+
assert_eq!(num_conns.load(Ordering::SeqCst), 2);
980+
assert_eq!(num_requests.load(Ordering::SeqCst), 5);
981+
}

0 commit comments

Comments
 (0)