Skip to content

Commit 2eb6b96

Browse files
authored
perf: remove connection pool (#230)
Replaces `SmtpConnectionPool` with a simpler solution. Related: #219 Signed-off-by: Jagoda Ślązak <jslazak@jslazak.com>
1 parent 4fe1b40 commit 2eb6b96

5 files changed

Lines changed: 78 additions & 119 deletions

File tree

‎src/inbound.rs‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
use crate::config::Config;
44
use crate::dkim_verifier::DkimVerifier;
55
use crate::message::{check_encrypted, is_securejoin};
6-
use crate::smtp_client::SmtpConnectionPool;
76
use crate::smtp_responses::ENCRYPTION_NEEDED_523;
87
pub use crate::smtp_server::Envelope;
98
use crate::smtp_server::{SmtpHandler, Transaction};
@@ -21,7 +20,7 @@ pub struct IncomingBeforeQueueHandler<S: TcpConnect> {
2120
dns_resolver: Arc<TokioResolver>,
2221
dkim_verifier: DkimVerifier,
2322
skip_dkim: bool,
24-
smtp_connection_pool: Arc<SmtpConnectionPool<S>>,
23+
connection_context: S::ConnectionContext,
2524
}
2625

2726
impl<S> IncomingBeforeQueueHandler<S>
@@ -36,7 +35,7 @@ where
3635
dns_resolver: dns_resolver.clone(),
3736
dkim_verifier: DkimVerifier::new(dns_resolver),
3837
skip_dkim,
39-
smtp_connection_pool: SmtpConnectionPool::new(Default::default()),
38+
connection_context: Default::default(),
4039
})
4140
}
4241

@@ -165,14 +164,15 @@ where
165164
client_hostname: &hostname,
166165
tls_config: None,
167166
lmtp: false,
167+
connection_context: self.connection_context.clone(),
168168
};
169-
crate::smtp_client::send(
169+
crate::smtp_client::send::<S>(
170170
&self.config.postfix_host,
171171
self.config.postfix_reinject_port_incoming,
172172
&transaction.envelope,
173173
client_config,
174174
self.dns_resolver.clone(),
175-
self.smtp_connection_pool.clone(),
175+
&mut None,
176176
)
177177
.await
178178
.map_err(|e| {

‎src/outbound.rs‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
33
use crate::config::Config;
44
use crate::message::{check_encrypted, is_securejoin};
5-
use crate::smtp_client::SmtpConnectionPool;
65
use crate::smtp_responses::ENCRYPTION_NEEDED_523;
76
use crate::smtp_responses::OK_250;
87
use crate::smtp_server::{SmtpHandler, Transaction};
@@ -34,7 +33,7 @@ pub struct OutgoingBeforeQueueHandler<S: TcpConnect> {
3433
MonotonicClock,
3534
NoOpMiddleware<std::time::Instant>,
3635
>,
37-
smtp_connection_pool: Arc<SmtpConnectionPool<S>>,
36+
connection_context: S::ConnectionContext,
3837
}
3938

4039
impl<S> OutgoingBeforeQueueHandler<S>
@@ -51,7 +50,7 @@ where
5150
config,
5251
dns_resolver,
5352
send_rate_limiter,
54-
smtp_connection_pool: SmtpConnectionPool::new(Default::default()),
53+
connection_context: Default::default(),
5554
})
5655
}
5756
}
@@ -173,14 +172,15 @@ where
173172
client_hostname: &hostname,
174173
tls_config: None,
175174
lmtp: false,
175+
connection_context: self.connection_context.clone(),
176176
};
177-
crate::smtp_client::send(
177+
crate::smtp_client::send::<S>(
178178
&self.config.postfix_host,
179179
self.config.postfix_reinject_port,
180180
&transaction.envelope,
181181
client_config,
182182
self.dns_resolver.clone(),
183-
self.smtp_connection_pool.clone(),
183+
&mut None,
184184
)
185185
.await
186186
.map_err(|e| {

‎src/smtp_client.rs‎

Lines changed: 51 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -7,73 +7,16 @@ use std::task::{Context, Poll};
77
use std::time::Duration;
88
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufStream};
99
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
10-
use tokio::task::{JoinHandle, JoinSet};
10+
use tokio::task::JoinSet;
1111
use tokio_io_timeout::TimeoutStream;
1212
use tokio_rustls::rustls::client::ClientSessionMemoryCache;
1313

1414
/// Wraps SMTP connection, contains stream and ESMTP support information.
15-
pub struct SmtpConnection<S> {
15+
pub struct SmtpConnection<S: TcpConnect> {
1616
pub stream: BufStream<SmtpStream<S>>,
1717
pub pipelining: bool,
1818
}
1919

20-
/// A connection pool for SMTP connections, keyed by (address, port).
21-
///
22-
/// Connections are cached for up to 100 seconds of idle time.
23-
///
24-
/// Only a single connection is cached per address/port pair.
25-
pub struct SmtpConnectionPool<S>
26-
where
27-
S: TcpStreamTrait + TcpConnect,
28-
{
29-
pool: Arc<retainer::Cache<(String, u16), SmtpConnection<S>>>,
30-
monitor_handle: JoinHandle<()>,
31-
context: S::ConnectionContext,
32-
}
33-
34-
impl<S> SmtpConnectionPool<S>
35-
where
36-
S: TcpStreamTrait + TcpConnect,
37-
{
38-
/// Creates a new connection pool and starts the cache monitoring task.
39-
pub fn new(context: S::ConnectionContext) -> Arc<Self> {
40-
let pool = Arc::new(retainer::Cache::new());
41-
let pool_clone = pool.clone();
42-
43-
let monitor_handle =
44-
tokio::spawn(async move { pool_clone.monitor(4, 0.25, Duration::from_secs(10)).await });
45-
46-
Arc::new(Self {
47-
pool,
48-
monitor_handle,
49-
context,
50-
})
51-
}
52-
53-
/// Takes a connection from the pool for the given address and port, if available.
54-
pub async fn take(&self, address: &str, port: u16) -> Option<SmtpConnection<S>> {
55-
self.pool.remove(&(address.to_string(), port)).await
56-
}
57-
58-
/// Puts a connection into the pool for the given address and port, with a 100s timeout.
59-
pub async fn put(&self, address: &str, port: u16, connection: SmtpConnection<S>) {
60-
// similarly to postfix default -> 100s max idle time.
61-
self.pool
62-
.insert(
63-
(address.to_string(), port),
64-
connection,
65-
Duration::from_secs(100),
66-
)
67-
.await;
68-
}
69-
}
70-
71-
impl<S: TcpConnect> Drop for SmtpConnectionPool<S> {
72-
fn drop(&mut self) {
73-
self.monitor_handle.abort();
74-
}
75-
}
76-
7720
/// A [`TcpStream`] wrapper used for SMTP communication.
7821
#[expect(clippy::large_enum_variant)]
7922
pub enum SmtpStream<S> {
@@ -242,7 +185,7 @@ where
242185
}
243186

244187
/// SMTP/LMTP client configuration options.
245-
pub struct ClientConfig<'a> {
188+
pub struct ClientConfig<'a, S: TcpConnect> {
246189
/// Client hostname used for greeting
247190
pub client_hostname: &'a str,
248191

@@ -252,49 +195,61 @@ pub struct ClientConfig<'a> {
252195

253196
/// If `true`, switches to `LHLO` greeting and returns per-recipient composite response.
254197
pub lmtp: bool,
198+
199+
/// Additional parameters for connection establishment,
200+
/// e.g. a reference to the session transcript channel in the tests
201+
/// (see [`RecTcpStream`]).
202+
///
203+
/// [`RecTcpStream`]: crate::tcp::rec_stream::RecTcpStream
204+
pub connection_context: S::ConnectionContext,
255205
}
256206

257207
/// Sends an email using an SMTP server at `smtp_addr`.
258208
/// If `address` is a domain that resolves to multiple IP addresses,
259209
/// all will be tried in parallel and the first successful connection will be used.
260210
///
261-
/// `pool` is used to reuse existing connections to the same address and port, if available.
211+
/// If `connection` is supplied, it will be used instead,
212+
/// and provided `address` and `port` will only serve as a fallback.
262213
pub async fn send<S>(
263214
address: &str,
264215
port: u16,
265216
envelope: &Envelope,
266-
config: ClientConfig<'_>,
217+
config: ClientConfig<'_, S>,
267218
dns_resolver: Arc<TokioResolver>,
268-
pool: Arc<SmtpConnectionPool<S>>,
219+
connection: &mut Option<SmtpConnection<S>>,
269220
) -> Result<(), crate::error::Error>
270221
where
271222
S: TcpStreamTrait + TcpConnect,
272223
{
273224
let greeting = if config.lmtp { "LHLO" } else { "EHLO" };
274225

275-
let (mut buf_stream, reused, mut pipelining) =
276-
if let Some(connection) = pool.take(address, port).await {
277-
log::debug!(
278-
"Reusing existing connection to {}",
279-
connection.stream.get_ref().format_host(address)
226+
let (mut buf_stream, reused, mut pipelining) = if let Some(con) = connection.take() {
227+
log::debug!(
228+
"Reusing existing connection to {}",
229+
con.stream.get_ref().format_host(address)
230+
);
231+
if config.tls_config.is_some() {
232+
// This should never happen,
233+
// assert to make sure we never accidentally use a plain connection while expecting TLS.
234+
assert!(
235+
matches!(con.stream.get_ref(), SmtpStream::Tls(_)),
236+
"Expected TLS stream from pool, but got plain stream."
280237
);
281-
if config.tls_config.is_some() {
282-
// This should never happen,
283-
// assert to make sure we never accidentally use a plain connection while expecting TLS.
284-
assert!(
285-
matches!(connection.stream.get_ref(), SmtpStream::Tls(_)),
286-
"Expected TLS stream from pool, but got plain stream."
287-
);
288-
}
289-
(connection.stream, true, connection.pipelining)
290-
} else {
291-
let stream = SmtpStream::plain(
292-
establish_tcp_connection(address, port, dns_resolver.clone(), pool.context.clone())
293-
.await?,
294-
);
295-
log::debug!("Successfully connected to {}", stream.format_host(address));
296-
(BufStream::new(stream), false, false)
297-
};
238+
}
239+
(con.stream, true, con.pipelining)
240+
} else {
241+
let stream = SmtpStream::plain(
242+
establish_tcp_connection(
243+
address,
244+
port,
245+
dns_resolver.clone(),
246+
config.connection_context.clone(),
247+
)
248+
.await?,
249+
);
250+
log::debug!("Successfully connected to {}", stream.format_host(address));
251+
(BufStream::new(stream), false, false)
252+
};
298253

299254
let mut response = String::new();
300255

@@ -359,8 +314,13 @@ where
359314
// e.g.: 421 example.org Service closing transmission channel - command timeout
360315
if response.starts_with("421") {
361316
log::debug!("Reused connection is dead; establishing new connection...");
362-
let stream: S =
363-
establish_tcp_connection(address, port, dns_resolver, pool.context.clone()).await?;
317+
let stream: S = establish_tcp_connection(
318+
address,
319+
port,
320+
dns_resolver,
321+
config.connection_context.clone(),
322+
)
323+
.await?;
364324
log::debug!("Successfully connected to {}", stream.peer_addr()?);
365325
buf_stream = BufStream::new(SmtpStream::plain(stream));
366326
false
@@ -497,15 +457,10 @@ where
497457
smtp_read!("end of DATA", "250")?;
498458
}
499459

500-
pool.put(
501-
address,
502-
port,
503-
SmtpConnection {
504-
stream: buf_stream,
505-
pipelining,
506-
},
507-
)
508-
.await;
460+
connection.replace(SmtpConnection {
461+
stream: buf_stream,
462+
pipelining,
463+
});
509464

510465
Ok(())
511466
}

‎src/transport.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,6 @@ where
203203
#[cfg(test)]
204204
mod tests {
205205
use super::*;
206-
use crate::smtp_client::SmtpConnectionPool;
207206
use crate::smtp_server::{Envelope, MockHandler, run_smtp_server};
208207
use crate::tcp::rec_stream::RecTcpStream;
209208
use rstest::{fixture, rstest};
@@ -275,18 +274,19 @@ mod tests {
275274
/// Does not fail on negative response.
276275
async fn lmtp_send(envelope: &Envelope) -> TestResult<String> {
277276
let (tx, mut rx) = tokio::sync::mpsc::channel(128);
278-
let client_config = crate::smtp_client::ClientConfig {
277+
let client_config = crate::smtp_client::ClientConfig::<'_, RecTcpStream> {
279278
client_hostname: "postfix",
280279
tls_config: None,
281280
lmtp: true,
281+
connection_context: tx,
282282
};
283283
let _ = crate::smtp_client::send(
284284
FILTERMAIL_IP,
285285
FILTERMAIL_PORT,
286286
envelope,
287287
client_config,
288288
Arc::new(crate::utils::build_resolver()?),
289-
SmtpConnectionPool::<RecTcpStream>::new(tx),
289+
&mut None,
290290
)
291291
.await;
292292

0 commit comments

Comments
 (0)