Skip to content

Commit 5ce08b8

Browse files
committed
✨ Add connect and I/O timeouts to exp module
1 parent 06e57cd commit 5ce08b8

5 files changed

Lines changed: 208 additions & 39 deletions

File tree

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ rand = "0.10"
2121
enum_dispatch = "0.3"
2222
openssl = { version = "^0.10", optional = true }
2323
r2d2 = "^0.8"
24-
tokio = { version = "1", features = ["io-util", "net"], optional = true }
24+
tokio = { version = "1", features = ["io-util", "net", "time"], optional = true }
2525

2626
[dev-dependencies]
27-
tokio = { version = "1", features = ["io-util", "net", "rt", "macros"] }
27+
tokio = { version = "1", features = ["io-util", "net", "rt", "macros", "time"] }

src/exp/async_client.rs

Lines changed: 88 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22
33
use std::borrow::Cow;
44
use std::collections::HashMap;
5+
use std::future::Future;
56
use std::net::SocketAddr;
67
use std::sync::{Arc, Mutex};
8+
use std::time::Duration;
79

810
use tokio::net::{ToSocketAddrs, lookup_host};
911

1012
use crate::error::{ClientError, MemcacheError, ServerError};
1113

1214
use super::async_connection::AsyncMetaConnection;
13-
use super::client::{DEFAULT_MAX_IDLE, default_hash_function, jump_hash};
15+
use super::client::{DEFAULT_MAX_IDLE, Timeouts, default_hash_function, jump_hash};
1416
use super::core::{self, Operation};
1517
use super::meta_api::{ArithmeticMode, build_debug, build_noop, parse_debug_result, parse_meta_result};
1618
use super::meta_command::{MetaCommand, ReturnCode};
@@ -19,6 +21,22 @@ use super::request::Request;
1921
use super::result::OpResult;
2022
use super::value::ToValue;
2123

24+
/// Bound a transport future by a timeout; `None` means unbounded. A
25+
/// timeout surfaces as an io error and so poisons the connection like any
26+
/// other transport failure.
27+
async fn timed<T>(
28+
timeout: Option<Duration>,
29+
future: impl Future<Output = Result<T, MemcacheError>>,
30+
) -> Result<T, MemcacheError> {
31+
match timeout {
32+
Some(duration) => match tokio::time::timeout(duration, future).await {
33+
Ok(result) => result,
34+
Err(_) => Err(std::io::Error::from(std::io::ErrorKind::TimedOut).into()),
35+
},
36+
None => future.await,
37+
}
38+
}
39+
2240
/// One server: its resolved addresses and a stack of idle connections.
2341
/// The mutex is only held to pop/push, never across I/O.
2442
struct AsyncServer {
@@ -27,12 +45,12 @@ struct AsyncServer {
2745
}
2846

2947
impl AsyncServer {
30-
async fn checkout(&self) -> Result<AsyncMetaConnection, MemcacheError> {
48+
async fn checkout(&self, timeouts: &Timeouts) -> Result<AsyncMetaConnection, MemcacheError> {
3149
let reused = self.idle.lock().unwrap().pop();
3250
if let Some(connection) = reused {
3351
return Ok(connection);
3452
}
35-
AsyncMetaConnection::connect(self.addrs.as_slice()).await
53+
timed(timeouts.connect, AsyncMetaConnection::connect(self.addrs.as_slice())).await
3654
}
3755

3856
fn put_back(&self, connection: AsyncMetaConnection, max_idle: usize) {
@@ -61,6 +79,7 @@ pub struct AsyncMetaClient {
6179
servers: Arc<Vec<AsyncServer>>,
6280
hash_function: fn(&[u8]) -> u64,
6381
max_idle: usize,
82+
timeouts: Timeouts,
6483
}
6584

6685
impl AsyncMetaClient {
@@ -69,8 +88,8 @@ impl AsyncMetaClient {
6988
}
7089

7190
/// Connect to several servers; keys are distributed across them by the
72-
/// hash function. One connection per server is dialed up front so
73-
/// connection errors surface here.
91+
/// hash function. Addresses are resolved here, but connections are
92+
/// dialed lazily, so a down server surfaces at the first operation.
7493
pub async fn connect_multiple<A: ToSocketAddrs>(
7594
addrs: impl IntoIterator<Item = A>,
7695
) -> Result<AsyncMetaClient, MemcacheError> {
@@ -80,13 +99,10 @@ impl AsyncMetaClient {
8099
if resolved.is_empty() {
81100
return Err(ClientError::Error(Cow::Borrowed("address resolved to no socket addresses")).into());
82101
}
83-
let server = AsyncServer {
102+
servers.push(AsyncServer {
84103
addrs: resolved,
85104
idle: Mutex::new(Vec::new()),
86-
};
87-
let connection = AsyncMetaConnection::connect(server.addrs.as_slice()).await?;
88-
server.idle.lock().unwrap().push(connection);
89-
servers.push(server);
105+
});
90106
}
91107
if servers.is_empty() {
92108
return Err(ClientError::Error(Cow::Borrowed("at least one server address is required")).into());
@@ -95,6 +111,7 @@ impl AsyncMetaClient {
95111
servers: Arc::new(servers),
96112
hash_function: default_hash_function,
97113
max_idle: DEFAULT_MAX_IDLE,
114+
timeouts: Timeouts::default(),
98115
})
99116
}
100117

@@ -116,6 +133,23 @@ impl AsyncMetaClient {
116133
self
117134
}
118135

136+
/// Limit how long dialing a server may take (no limit by default).
137+
/// Configure before cloning: clones share connections but not this
138+
/// setting.
139+
pub fn with_connect_timeout(mut self, timeout: Duration) -> AsyncMetaClient {
140+
self.timeouts.connect = Some(timeout);
141+
self
142+
}
143+
144+
/// Limit how long one command or batch exchange may take (no limit by
145+
/// default). A timeout poisons the connection like any other transport
146+
/// error. Configure before cloning: clones share connections but not
147+
/// this setting.
148+
pub fn with_io_timeout(mut self, timeout: Duration) -> AsyncMetaClient {
149+
self.timeouts.io = Some(timeout);
150+
self
151+
}
152+
119153
fn connection_index(&self, key: &[u8]) -> usize {
120154
jump_hash((self.hash_function)(key), self.servers.len())
121155
}
@@ -155,10 +189,10 @@ impl AsyncMetaClient {
155189
pub async fn run<O: Operation>(&self, operation: O) -> Result<O::Output, MemcacheError> {
156190
let command = operation.prepare()?;
157191
let server = &self.servers[self.connection_index(operation.key())];
158-
let mut connection = server.checkout().await?;
192+
let mut connection = server.checkout(&self.timeouts).await?;
159193
// A failed exchange leaves the stream in an unknown state, so the
160194
// connection is dropped instead of returned to the pool.
161-
let response = connection.execute(&command).await?;
195+
let response = timed(self.timeouts.io, connection.execute(&command)).await?;
162196
server.put_back(connection, self.max_idle);
163197
operation.parse(parse_meta_result(response)?)
164198
}
@@ -187,8 +221,8 @@ impl AsyncMetaClient {
187221
.map(|&index| plan.commands[index].take().unwrap())
188222
.collect();
189223
let server = &self.servers[server];
190-
let mut connection = server.checkout().await?;
191-
let responses = connection.execute_batch(&commands).await?;
224+
let mut connection = server.checkout(&self.timeouts).await?;
225+
let responses = timed(self.timeouts.io, connection.execute_batch(&commands)).await?;
192226
server.put_back(connection, self.max_idle);
193227
for (&index, response) in indices.iter().zip(responses) {
194228
outputs[index] = Some(operations[index].parse(parse_meta_result(response)?)?);
@@ -204,8 +238,8 @@ impl AsyncMetaClient {
204238
/// health check.
205239
pub async fn noop(&self) -> Result<(), MemcacheError> {
206240
for server in self.servers.iter() {
207-
let mut connection = server.checkout().await?;
208-
let response = connection.execute(&build_noop()).await?;
241+
let mut connection = server.checkout(&self.timeouts).await?;
242+
let response = timed(self.timeouts.io, connection.execute(&build_noop())).await?;
209243
server.put_back(connection, self.max_idle);
210244
if response.rc != ReturnCode::Mn {
211245
return Err(ServerError::BadResponse("unexpected no-op response".into()).into());
@@ -219,8 +253,8 @@ impl AsyncMetaClient {
219253
let key = key.into();
220254
let server = &self.servers[self.connection_index(&key)];
221255
let command = build_debug(key)?;
222-
let mut connection = server.checkout().await?;
223-
let response = connection.execute(&command).await?;
256+
let mut connection = server.checkout(&self.timeouts).await?;
257+
let response = timed(self.timeouts.io, connection.execute(&command)).await?;
224258
server.put_back(connection, self.max_idle);
225259
parse_debug_result(&response)
226260
}
@@ -233,3 +267,39 @@ impl<'a, O: Operation> Request<'a, AsyncMetaClient, O> {
233267
client.run(operation).await
234268
}
235269
}
270+
271+
#[cfg(test)]
272+
mod tests {
273+
use std::io::{BufRead, BufReader, Write};
274+
use std::net::TcpListener;
275+
276+
use super::*;
277+
278+
#[tokio::test]
279+
async fn io_timeout_poisons_connection() {
280+
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
281+
let addr = listener.local_addr().unwrap();
282+
let handle = std::thread::spawn(move || {
283+
// First connection: read the request but never respond, so the
284+
// exchange times out and the connection is poisoned.
285+
let (stream, _) = listener.accept().unwrap();
286+
let mut reader = BufReader::new(stream);
287+
let mut line = Vec::new();
288+
reader.read_until(b'\n', &mut line).unwrap();
289+
// Second connection: respond normally.
290+
let (stream, _) = listener.accept().unwrap();
291+
let mut reader = BufReader::new(stream);
292+
let mut line = Vec::new();
293+
reader.read_until(b'\n', &mut line).unwrap();
294+
reader.get_mut().write_all(b"HD\r\n").unwrap();
295+
});
296+
297+
let client = AsyncMetaClient::connect(addr)
298+
.await
299+
.unwrap()
300+
.with_io_timeout(Duration::from_millis(100));
301+
assert!(client.delete("foo").send().await.is_err());
302+
assert!(client.delete("foo").send().await.unwrap().stored());
303+
handle.join().unwrap();
304+
}
305+
}

0 commit comments

Comments
 (0)