use super::{Connector, QueuePayload, QueuePayloadType};
use crate::FlagdOptions;
use crate::error::FlagdError;
use crate::flagd::sync::v1::{SyncFlagsRequest, flag_sync_service_client::FlagSyncServiceClient};
use crate::resolver::common::upstream::UpstreamConfig;
use hyper_util::rt::TokioIo;
use std::str::FromStr;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use std::time::Duration;
use tokio::net::UnixStream;
use tokio::sync::Mutex;
use tokio::sync::mpsc::{Receiver, Sender, channel};
use tokio::time::sleep;
use tonic::transport::{Channel, Endpoint, Uri};
use tower::service_fn;
use tracing::{debug, error, warn};
const CONNECTION_TIMEOUT_SECS: u64 = 5;
#[derive(Clone)]
pub struct GrpcStreamConnector {
target: String,
selector: Option<String>,
sender: Sender<QueuePayload>,
stream: Arc<Mutex<Option<Receiver<QueuePayload>>>>,
shutdown: Arc<AtomicBool>,
retry_backoff_ms: u32,
retry_backoff_max_ms: u32,
retry_grace_period: u32,
keep_alive_time_ms: u64,
authority: Option<String>, provider_id: String, channel: Arc<Mutex<Option<Channel>>>, tls: bool, socket_path: Option<String>, cert_path: Option<String>, }
impl GrpcStreamConnector {
pub fn new(
target: String,
selector: Option<String>,
options: &FlagdOptions,
authority: Option<String>,
) -> Self {
debug!("Creating new GrpcStreamConnector with target: {}", target);
let (sender, receiver) = channel(1000);
Self {
target,
selector,
sender,
stream: Arc::new(Mutex::new(Some(receiver))),
shutdown: Arc::new(AtomicBool::new(false)),
retry_backoff_ms: options.retry_backoff_ms,
retry_backoff_max_ms: options.retry_backoff_max_ms,
retry_grace_period: options.retry_grace_period,
keep_alive_time_ms: options.keep_alive_time_ms,
authority,
provider_id: options
.provider_id
.clone()
.unwrap_or_else(|| "rust-flagd-provider".to_string()),
channel: Arc::new(Mutex::new(None)),
tls: options.tls,
socket_path: None,
cert_path: options.cert_path.clone(),
}
}
pub fn new_unix(
target: String,
socket_path: String,
selector: Option<String>,
options: &FlagdOptions,
) -> Self {
debug!(
"Creating new GrpcStreamConnector for Unix socket: {}",
socket_path
);
let (sender, receiver) = channel(1000);
Self {
target,
selector,
sender,
stream: Arc::new(Mutex::new(Some(receiver))),
shutdown: Arc::new(AtomicBool::new(false)),
retry_backoff_ms: options.retry_backoff_ms,
retry_backoff_max_ms: options.retry_backoff_max_ms,
retry_grace_period: options.retry_grace_period,
keep_alive_time_ms: options.keep_alive_time_ms,
authority: None, provider_id: options
.provider_id
.clone()
.unwrap_or_else(|| "rust-flagd-provider".to_string()),
channel: Arc::new(Mutex::new(None)),
tls: options.tls,
socket_path: Some(socket_path),
cert_path: options.cert_path.clone(),
}
}
async fn establish_connection_using(
&self,
config: &UpstreamConfig,
) -> Result<Channel, FlagdError> {
debug!("Created endpoint: {:?}", config.endpoint().uri());
let mut endpoint = config.endpoint().clone();
endpoint = endpoint
.http2_adaptive_window(true)
.connect_timeout(Duration::from_secs(CONNECTION_TIMEOUT_SECS))
.tcp_keepalive(Some(Duration::from_secs(60)));
if self.keep_alive_time_ms > 0 {
endpoint = endpoint
.http2_keep_alive_interval(Duration::from_millis(self.keep_alive_time_ms))
.keep_alive_timeout(Duration::from_secs(20))
.keep_alive_while_idle(true);
}
if let Some(ref authority) = self.authority {
let authority_uri = Uri::from_str(&format!("http://{}", authority))
.map_err(|e| FlagdError::Config(format!("Invalid authority URI: {}", e)))?;
endpoint = endpoint.origin(authority_uri);
}
endpoint
.timeout(Duration::from_secs(CONNECTION_TIMEOUT_SECS))
.connect()
.await
.map_err(|e| {
FlagdError::Connection(format!(
"Failed to connect to gRPC server {}: {}",
self.target, e
))
})
}
async fn connect_with_timeout_using(
&self,
config: &UpstreamConfig,
) -> Result<Channel, FlagdError> {
debug!(
"Attempting connection with timeout to target: {}",
self.target
);
let mut current_delay = self.retry_backoff_ms;
let mut attempts = 0;
while !self.shutdown.load(Ordering::Relaxed) {
match self.establish_connection_using(config).await {
Ok(channel) => {
debug!("Successfully established channel connection");
return Ok(channel);
}
Err(e) => {
attempts += 1;
if attempts >= self.retry_grace_period {
error!("Connection attempts exhausted: {}", e);
return Err(FlagdError::Connection(format!(
"Max retries exceeded: {}",
e
)));
}
let delay = Duration::from_millis(current_delay as u64);
warn!(
"Connection attempt {} failed, retrying in {}ms: {}",
attempts,
delay.as_millis(),
e
);
sleep(delay).await;
current_delay = (current_delay * 2).min(self.retry_backoff_max_ms);
}
}
}
Err(FlagdError::Connection(
"Shutdown requested during connection attempts".to_string(),
))
}
async fn get_or_create_channel(&self) -> Result<Channel, FlagdError> {
let mut channel_guard = self.channel.lock().await;
if let Some(ref channel) = *channel_guard {
debug!("Reusing existing channel connection");
return Ok(channel.clone());
}
debug!("Creating new channel connection to {}", self.target);
let channel = if let Some(ref socket_path) = self.socket_path {
debug!("Using Unix socket connection to: {}", socket_path);
let path = socket_path.clone();
Endpoint::try_from("http://[::]:50051")
.map_err(|e| FlagdError::Config(format!("Invalid endpoint: {}", e)))?
.connect_with_connector(service_fn(move |_: Uri| {
let path = path.clone();
async move {
let stream = UnixStream::connect(path).await?;
Ok::<_, std::io::Error>(TokioIo::new(stream))
}
}))
.await
.map_err(|e| {
FlagdError::Connection(format!(
"Failed to connect to Unix socket {}: {}",
self.target, e
))
})?
} else {
let config = UpstreamConfig::new(
self.target.clone(),
true,
self.tls,
self.cert_path.as_deref(),
)?;
self.connect_with_timeout_using(&config).await?
};
*channel_guard = Some(channel.clone());
Ok(channel)
}
async fn invalidate_channel(&self) {
let mut channel_guard = self.channel.lock().await;
*channel_guard = None;
debug!("Invalidated cached channel");
}
async fn start_stream(&self) -> Result<(), FlagdError> {
debug!("Starting sync stream connection to {}", self.target);
let channel = self.get_or_create_channel().await?;
debug!("Using authority: {:?}", self.authority);
let mut client = FlagSyncServiceClient::new(channel);
let request = tonic::Request::new(SyncFlagsRequest {
provider_id: self.provider_id.clone(),
selector: self.selector.clone().unwrap_or_default(),
});
debug!("Sending sync request with selector: {:?}", self.selector);
match client.sync_flags(request).await {
Ok(response) => {
let mut stream = response.into_inner();
while let Ok(Some(msg)) = stream.message().await {
if self.shutdown.load(Ordering::Relaxed) {
break;
}
debug!(
"Received flag configuration update: {} bytes",
msg.flag_configuration.len()
);
self.sender
.send(QueuePayload {
payload_type: QueuePayloadType::Data,
flag_data: msg.flag_configuration,
metadata: None,
})
.await?;
}
Ok(())
}
Err(status) => {
error!("Error in sync stream: {}", status);
Ok(())
}
}
}
async fn run_sync_stream(&self) {
let mut current_delay = self.retry_backoff_ms;
loop {
if self.shutdown.load(Ordering::Relaxed) {
debug!("Shutdown requested; stopping sync stream loop");
break;
}
match self.start_stream().await {
Ok(_) => {
debug!("Sync stream ended; invalidating channel and reconnecting");
self.invalidate_channel().await;
current_delay = self.retry_backoff_ms; }
Err(e) => {
error!(
"Sync stream encountered error: {}. Retrying in {}ms",
e, current_delay
);
self.invalidate_channel().await;
}
}
sleep(Duration::from_millis(current_delay as u64)).await;
current_delay = (current_delay * 2).min(self.retry_backoff_max_ms);
}
}
}
#[async_trait::async_trait]
impl Connector for GrpcStreamConnector {
async fn init(&self) -> Result<(), FlagdError> {
debug!("Initializing GrpcStreamConnector");
let connector = self.clone();
tokio::spawn(async move {
debug!("Starting sync stream on {}", connector.target);
connector.run_sync_stream().await;
});
debug!("Initialized sync stream connector");
Ok(())
}
fn get_stream(&self) -> Arc<Mutex<Option<Receiver<QueuePayload>>>> {
self.stream.clone()
}
async fn shutdown(&self) -> Result<(), FlagdError> {
debug!("Shutting down GrpcStreamConnector");
self.shutdown.store(true, Ordering::Relaxed);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FlagdOptions;
use crate::resolver::common::upstream::UpstreamConfig;
use serial_test::serial;
use tempfile::TempDir;
use test_log::test;
use tokio::net::{TcpListener, UnixListener};
use tokio::time::Instant;
#[test(tokio::test(flavor = "multi_thread", worker_threads = 1))]
#[serial]
async fn test_retry_mechanism_inprocess() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
drop(listener);
let mut options = FlagdOptions::default();
options.host = addr.ip().to_string();
options.resolver_type = crate::ResolverType::InProcess;
options.port = addr.port();
options.deadline_ms = 100; options.retry_backoff_ms = 100;
options.retry_backoff_max_ms = 400;
options.retry_grace_period = 3;
options.stream_deadline_ms = 500;
options.tls = false;
options.cache_settings = None;
let target = format!("{}:{}", addr.ip(), addr.port());
let connector = GrpcStreamConnector::new(target.clone(), None, &options, None);
let config = UpstreamConfig::new(target, false, false, None)
.expect("failed to create upstream config");
let start = Instant::now();
let result = connector.connect_with_timeout_using(&config).await;
let elapsed = start.elapsed();
assert!(result.is_err(), "Expected error on connection attempts");
assert!(
elapsed.as_millis() >= 300,
"Elapsed time {}ms is less than expected",
elapsed.as_millis()
);
assert!(
elapsed.as_millis() < 600,
"Elapsed time {}ms is too high",
elapsed.as_millis()
);
}
#[test(tokio::test)]
async fn test_unix_socket_connector_stores_socket_path() {
let tmp_dir = TempDir::new().unwrap();
let socket_path = tmp_dir.path().join("test.sock");
let socket_path_str = socket_path.to_str().unwrap().to_string();
let options = FlagdOptions::default();
let target = format!("unix://{}", socket_path_str);
let connector =
GrpcStreamConnector::new_unix(target.clone(), socket_path_str.clone(), None, &options);
assert_eq!(connector.socket_path, Some(socket_path_str));
}
#[test(tokio::test)]
async fn test_unix_socket_connection() {
let tmp_dir = TempDir::new().unwrap();
let socket_path = tmp_dir.path().join("test.sock");
let socket_path_str = socket_path.to_str().unwrap().to_string();
let listener = UnixListener::bind(&socket_path).unwrap();
let accept_handle = tokio::spawn(async move {
let _conn = listener.accept().await;
});
let options = FlagdOptions::default();
let target = format!("unix://{}", socket_path_str);
let connector =
GrpcStreamConnector::new_unix(target, socket_path_str.clone(), None, &options);
let result = connector.get_or_create_channel().await;
accept_handle.abort();
assert!(
result.is_ok() || result.is_err(),
"Connection attempt was made"
);
}
}