use crate::{ConfigError, ConfigSource};
use async_trait::async_trait;
use etcd_client::{Client, GetOptions, WatchOptions};
use futures::StreamExt;
use serde_json::Value;
use std::collections::HashMap;
pub struct EtcdConfigSource {
endpoints: Vec<String>,
prefix: String,
}
impl EtcdConfigSource {
pub fn new(endpoints: Vec<String>, prefix: impl Into<String>) -> Self {
Self {
endpoints,
prefix: prefix.into(),
}
}
async fn fetch(client: &mut Client, prefix: &str) -> Result<HashMap<String, Value>, ConfigError> {
let resp = client
.get(prefix, Some(GetOptions::new().with_prefix()))
.await
.map_err(|e| ConfigError::Remote(format!("etcd get 失败: {}", e)))?;
let mut map = HashMap::new();
for kv in resp.kvs() {
let key = String::from_utf8_lossy(kv.key()).to_string();
let key = key
.strip_prefix(prefix)
.unwrap_or(&key)
.trim_start_matches('/')
.to_string();
let value_str = String::from_utf8_lossy(kv.value()).to_string();
map.insert(key, crate::parse_config_value(&value_str));
}
Ok(map)
}
}
#[async_trait]
impl ConfigSource for EtcdConfigSource {
async fn load(&self) -> Result<HashMap<String, Value>, ConfigError> {
let mut client = Client::connect(self.endpoints.clone(), None)
.await
.map_err(|e| ConfigError::Remote(format!("连接 etcd 失败: {}", e)))?;
Self::fetch(&mut client, &self.prefix).await
}
async fn watch(
&self,
callback: Box<dyn Fn(HashMap<String, Value>) + Send + Sync>,
) -> Result<(), ConfigError> {
let endpoints = self.endpoints.clone();
let prefix = self.prefix.clone();
std::thread::spawn(move || {
let rt = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(e) => {
tracing::error!("etcd watch: 创建运行时失败: {}", e);
return;
}
};
rt.block_on(async move {
let mut client = match Client::connect(endpoints, None).await {
Ok(c) => c,
Err(e) => {
tracing::error!("etcd watch: 连接失败: {}", e);
return;
}
};
let (_watcher, mut stream) = match client
.watch(prefix.clone(), Some(WatchOptions::new().with_prefix()))
.await
{
Ok(t) => t,
Err(e) => {
tracing::error!("etcd watch: 注册失败: {}", e);
return;
}
};
while let Some(result) = stream.next().await {
match result {
Ok(_watch_response) => match Self::fetch(&mut client, &prefix).await {
Ok(map) => callback(map),
Err(e) => tracing::warn!("etcd watch: 重载配置失败: {}", e),
},
Err(e) => tracing::warn!("etcd watch: 事件错误: {}", e),
}
}
tracing::warn!("etcd watch: 流已结束");
});
});
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore = "需要真实 etcd 服务,通过 ETCD_ENDPOINTS 环境变量指定端点"]
async fn test_etcd_load_real() {
let endpoints: Vec<String> = std::env::var("ETCD_ENDPOINTS")
.unwrap_or_else(|_| "localhost:2379".to_string())
.split(',')
.map(|s| s.to_string())
.collect();
let src = EtcdConfigSource::new(endpoints, "sz-orm-test/");
let data = src.load().await;
println!("etcd load result: {:?}", data);
}
#[tokio::test]
#[ignore = "需要真实 etcd 服务"]
async fn test_etcd_load_unreachable() {
let src = EtcdConfigSource::new(vec!["localhost:19999".to_string()], "test/");
let result = src.load().await;
assert!(result.is_err(), "连接不存在的 etcd 应返回错误");
}
}