use std::sync::Arc;
use std::time::Duration;
const DEFAULT_MAX_CONNECTIONS: usize = 1024;
const DEFAULT_HANDSHAKE_TIMEOUT_SECS: u64 = 15;
const MAX_CONNECTIONS_ENV: &str = "REDDB_WIRE_MAX_CONNECTIONS";
const HANDSHAKE_TIMEOUT_ENV: &str = "REDDB_WIRE_HANDSHAKE_TIMEOUT_SECS";
pub(crate) type ConnectionPermit = tokio::sync::OwnedSemaphorePermit;
#[derive(Clone)]
pub(crate) struct ConnectionAdmission {
semaphore: Arc<tokio::sync::Semaphore>,
transport: &'static str,
}
impl ConnectionAdmission {
pub(crate) fn new(transport: &'static str) -> Self {
Self {
semaphore: Arc::new(tokio::sync::Semaphore::new(max_connections())),
transport,
}
}
pub(crate) fn try_admit(&self) -> Option<ConnectionPermit> {
match Arc::clone(&self.semaphore).try_acquire_owned() {
Ok(permit) => Some(permit),
Err(_) => {
tracing::warn!(
target: "reddb::security",
transport = self.transport,
limit = max_connections(),
"refusing connection: listener at capacity"
);
None
}
}
}
}
fn max_connections() -> usize {
[
MAX_CONNECTIONS_ENV,
"RED_MAX_CONNECTIONS",
"REDDB_MAX_CONNECTIONS",
]
.iter()
.find_map(|name| {
std::env::var(name)
.ok()
.and_then(|raw| raw.trim().parse::<usize>().ok())
.filter(|value| *value > 0)
})
.unwrap_or(DEFAULT_MAX_CONNECTIONS)
}
pub(crate) fn handshake_deadline() -> Option<Duration> {
let secs = std::env::var(HANDSHAKE_TIMEOUT_ENV)
.ok()
.and_then(|raw| raw.trim().parse::<u64>().ok())
.unwrap_or(DEFAULT_HANDSHAKE_TIMEOUT_SECS);
(secs > 0).then(|| Duration::from_secs(secs))
}
pub(crate) async fn with_handshake_deadline<F>(future: F) -> std::io::Result<F::Output>
where
F: std::future::Future,
{
match handshake_deadline() {
None => Ok(future.await),
Some(deadline) => tokio::time::timeout(deadline, future).await.map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("handshake did not complete within {}s", deadline.as_secs()),
)
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn admission_refuses_once_every_permit_is_held() {
let admission = ConnectionAdmission {
semaphore: Arc::new(tokio::sync::Semaphore::new(2)),
transport: "test",
};
let first = admission.try_admit().expect("first connection admitted");
let second = admission.try_admit().expect("second connection admitted");
assert!(admission.try_admit().is_none(), "listener is at capacity");
drop(first);
assert!(admission.try_admit().is_some(), "slot reused after close");
drop(second);
}
#[test]
fn handshake_deadline_is_overridable_and_disablable() {
let previous = std::env::var(HANDSHAKE_TIMEOUT_ENV).ok();
std::env::set_var(HANDSHAKE_TIMEOUT_ENV, "0");
assert_eq!(handshake_deadline(), None, "0 disables the deadline");
std::env::set_var(HANDSHAKE_TIMEOUT_ENV, "42");
assert_eq!(handshake_deadline(), Some(Duration::from_secs(42)));
std::env::set_var(HANDSHAKE_TIMEOUT_ENV, "not-a-number");
assert_eq!(
handshake_deadline(),
Some(Duration::from_secs(DEFAULT_HANDSHAKE_TIMEOUT_SECS)),
"a malformed value falls back to the default rather than failing to boot"
);
match previous {
Some(value) => std::env::set_var(HANDSHAKE_TIMEOUT_ENV, value),
None => std::env::remove_var(HANDSHAKE_TIMEOUT_ENV),
}
}
}