use std::collections::HashMap;
#[cfg(test)]
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use std::time::Duration;
use tonic::transport::{Channel, ClientTlsConfig, Endpoint};
use crate::error::SailError;
#[derive(Default)]
pub(crate) struct ChannelCache {
channels: Mutex<HashMap<String, Channel>>,
#[cfg(test)]
generation: AtomicU64,
}
impl ChannelCache {
pub(crate) fn new() -> ChannelCache {
ChannelCache::default()
}
pub(crate) fn get(&self, target: &str) -> Result<Channel, SailError> {
let mut channels = self
.channels
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(channel) = channels.get(target) {
return Ok(channel.clone());
}
let channel = open_channel(target)?;
#[cfg(test)]
self.generation.fetch_add(1, Ordering::Relaxed);
channels.insert(target.to_string(), channel.clone());
Ok(channel)
}
pub(crate) fn invalidate(&self, target: &str) {
self.channels
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(target);
}
#[cfg(test)]
fn generation(&self) -> u64 {
self.generation.load(Ordering::Relaxed)
}
}
fn is_insecure_target(target: &str) -> bool {
let normalized = target.trim().to_lowercase();
["localhost:", "127.0.0.1:", "[::1]:"]
.iter()
.any(|prefix| normalized.starts_with(prefix))
}
fn open_channel(target: &str) -> Result<Channel, SailError> {
let trimmed = target.trim();
let invalid = |e: tonic::transport::Error| SailError::Config {
message: format!("invalid workerproxy endpoint {trimmed:?}: {e}"),
};
let endpoint = if is_insecure_target(target) {
Endpoint::from_shared(format!("http://{trimmed}")).map_err(invalid)?
} else {
Endpoint::from_shared(format!("https://{trimmed}"))
.map_err(invalid)?
.tls_config(ClientTlsConfig::new().with_native_roots())
.map_err(|e| SailError::Internal {
message: format!("TLS config failed for {trimmed:?}: {e}"),
})?
};
let endpoint = endpoint
.http2_keep_alive_interval(Duration::from_secs(15))
.keep_alive_timeout(Duration::from_secs(10))
.keep_alive_while_idle(true);
Ok(endpoint.connect_lazy())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn insecure_targets() {
assert!(is_insecure_target("localhost:50052"));
assert!(is_insecure_target(" LOCALHOST:50052 "));
assert!(is_insecure_target("127.0.0.1:1"));
assert!(is_insecure_target("[::1]:50052"));
assert!(!is_insecure_target("workerproxy.sailresearch.com:443"));
assert!(!is_insecure_target("localhost.evil.com:443"));
}
#[tokio::test]
async fn cache_reuses_and_invalidates() {
let cache = ChannelCache::new();
cache.get("localhost:50052").unwrap();
cache.get("localhost:50052").unwrap();
assert_eq!(cache.generation(), 1);
cache.invalidate("localhost:50052");
cache.get("localhost:50052").unwrap();
assert_eq!(cache.generation(), 2);
}
}