feat: 新增 C++ 和 Rust 客户端 SDK 及 Docker 构建支持

- 新增 kenaito-conf-cpp-client / kenaito-conf-rust-client 及对应 demo
- 新增 Dockerfile.cpp / Dockerfile.rust
- README 目录结构补充 C++/Rust 客户端
- README_DOCKERFILE 补充 Windows docker run 命令,新增 C++/Rust 段落
This commit is contained in:
2026-08-11 21:14:04 +08:00
parent 436971d154
commit f63bcfa78b
19 changed files with 26281 additions and 12 deletions
+312
View File
@@ -0,0 +1,312 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::io::{Read, Write};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
const CACHE_FILE: &str = "config-cache.json";
#[derive(Debug, Clone)]
pub struct Config {
pub base_url: String,
pub app_name: String,
pub env: String,
pub version: String,
pub token: String,
pub sync_interval: u64,
pub ignore_global: bool,
pub cache_dir: String,
}
impl Default for Config {
fn default() -> Self {
Self {
base_url: "http://localhost:8080".to_string(),
app_name: String::new(),
env: "dev".to_string(),
version: "latest".to_string(),
token: String::new(),
sync_interval: 60,
ignore_global: false,
cache_dir: ".".to_string(),
}
}
}
type ChangeHandler = Box<dyn Fn(&HashMap<String, String>) + Send + Sync + 'static>;
struct Cache {
dir: String,
}
impl Cache {
fn new(dir: &str) -> Self {
Self { dir: dir.to_string() }
}
fn write(&self, data: &str) -> Result<(), std::io::Error> {
let path = PathBuf::from(&self.dir).join(CACHE_FILE);
let mut file = fs::File::create(path)?;
file.write_all(data.as_bytes())?;
Ok(())
}
fn read(&self) -> Option<String> {
let path = PathBuf::from(&self.dir).join(CACHE_FILE);
let mut file = fs::File::open(path).ok()?;
let mut contents = String::new();
file.read_to_string(&mut contents).ok()?;
Some(contents)
}
}
pub struct Client {
config: Config,
cache: Cache,
handlers: Arc<Mutex<Vec<ChangeHandler>>>,
configs: Arc<Mutex<HashMap<String, String>>>,
last_time: Arc<Mutex<i64>>,
stop_flag: Arc<Mutex<bool>>,
}
impl Client {
pub fn new(config: Config) -> Self {
let cache_dir = if config.cache_dir.is_empty() { ".".to_string() } else { config.cache_dir.clone() };
let version = if config.version.is_empty() { "latest".to_string() } else { config.version.clone() };
let sync_interval = if config.sync_interval == 0 { 60 } else { config.sync_interval };
let mut cfg = config;
cfg.cache_dir = cache_dir;
cfg.version = version;
cfg.sync_interval = sync_interval;
Self {
cache: Cache::new(&cfg.cache_dir),
config: cfg,
handlers: Arc::new(Mutex::new(Vec::new())),
configs: Arc::new(Mutex::new(HashMap::new())),
last_time: Arc::new(Mutex::new(0)),
stop_flag: Arc::new(Mutex::new(false)),
}
}
pub fn on_change<F>(&self, handler: F)
where
F: Fn(&HashMap<String, String>) + Send + Sync + 'static,
{
let mut handlers = self.handlers.lock().unwrap();
handlers.push(Box::new(handler));
}
pub fn get(&self, key: &str) -> String {
let configs = self.configs.lock().unwrap();
configs.get(key).cloned().unwrap_or_default()
}
pub fn get_all(&self) -> HashMap<String, String> {
self.configs.lock().unwrap().clone()
}
pub fn start(&self) -> Result<(), String> {
match self.fetch_all() {
Ok(_) => {}
Err(e) => {
eprintln!("[ConfigClient] fetch all configs error: {}, trying cache", e);
match self.cache.read() {
Some(cached) => {
match serde_json::from_str::<serde_json::Value>(&cached) {
Ok(val) => {
let mut data = HashMap::new();
if let Some(obj) = val.as_object() {
for (k, v) in obj {
if let Some(s) = v.as_str() {
data.insert(k.clone(), s.to_string());
}
}
}
self.apply_configs(&data);
eprintln!("[ConfigClient] loaded {} configs from cache", self.configs.lock().unwrap().len());
}
Err(_) => return Err("fetch configs failed and no cache".to_string()),
}
}
None => return Err("fetch configs failed and no cache".to_string()),
}
}
}
Ok(())
}
pub fn sync_loop(&self) {
let interval = Duration::from_secs(self.config.sync_interval);
loop {
thread::sleep(interval);
if *self.stop_flag.lock().unwrap() {
break;
}
if let Err(e) = self.fetch_changed() {
eprintln!("[ConfigClient] sync error: {}", e);
}
}
}
pub fn sync_once(&self) {
if let Err(e) = self.fetch_changed() {
eprintln!("[ConfigClient] sync error: {}", e);
}
}
pub fn stop(&self) {
*self.stop_flag.lock().unwrap() = true;
}
fn http_post(&self, url: &str, body: &str) -> Result<String, String> {
let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(10))
.build()
.map_err(|e| e.to_string())?;
let resp = client
.post(url)
.header("Content-Type", "application/json")
.body(body.to_string())
.send()
.map_err(|e| e.to_string())?;
if !resp.status().is_success() {
return Err(format!("HTTP {}", resp.status()));
}
resp.text().map_err(|e| e.to_string())
}
fn fetch_all(&self) -> Result<(), String> {
let url = format!("{}/api/fetchAllConfigs", self.config.base_url);
let payload = serde_json::json!({
"appName": self.config.app_name,
"env": self.config.env,
"version": self.config.version,
"ignoreGlobal": self.config.ignore_global,
"token": self.config.token,
});
let resp = self.http_post(&url, &payload.to_string())?;
let val: serde_json::Value = serde_json::from_str(&resp).map_err(|e| e.to_string())?;
if let Some(code) = val.get("code").and_then(|c| c.as_i64()) {
if code != 0 {
let msg = val.get("msg").and_then(|m| m.as_str()).unwrap_or("unknown");
return Err(format!("server error: {}", msg));
}
}
if let Some(ts) = val.get("latest.fetch.timestamp").and_then(|t| t.as_str()) {
if let Ok(t) = ts.parse::<i64>() {
*self.last_time.lock().unwrap() = t;
}
}
// cache raw response
let _ = self.cache.write(&resp);
// apply configs
let mut data = HashMap::new();
if let Some(obj) = val.as_object() {
for (k, v) in obj {
if let Some(s) = v.as_str() {
data.insert(k.clone(), s.to_string());
}
}
}
self.apply_configs(&data);
eprintln!("[ConfigClient] fetched {} configs", self.configs.lock().unwrap().len());
Ok(())
}
fn fetch_changed(&self) -> Result<(), String> {
let url = format!("{}/api/fetchChangedConfigs", self.config.base_url);
let payload = serde_json::json!({
"appName": self.config.app_name,
"env": self.config.env,
"version": self.config.version,
"lastTime": *self.last_time.lock().unwrap(),
"ignoreGlobal": self.config.ignore_global,
});
let resp = self.http_post(&url, &payload.to_string())?;
let val: serde_json::Value = serde_json::from_str(&resp).map_err(|e| e.to_string())?;
if let Some(code) = val.get("code").and_then(|c| c.as_i64()) {
if code != 0 {
return Err(format!("server error code: {}", code));
}
}
let data = val.get("data").and_then(|d| d.as_object()).cloned().unwrap_or_default();
if let Some(ts) = data.get("latest.fetch.timestamp").and_then(|t| t.as_str()) {
if let Ok(t) = ts.parse::<i64>() {
*self.last_time.lock().unwrap() = t;
}
}
// handle deleted keys
if let Some(deleted_str) = data.get("deletedKeys").and_then(|d| d.as_str()) {
if !deleted_str.is_empty() {
let deleted_keys: Vec<&str> = deleted_str.split(',').collect();
let mut configs = self.configs.lock().unwrap();
for key in &deleted_keys {
configs.remove(*key);
}
eprintln!("[ConfigClient] {} configs deleted", deleted_keys.len());
}
}
// handle changed configs
let mut changed = HashMap::new();
for (k, v) in &data {
if k == "latest.fetch.timestamp" || k == "deletedKeys" {
continue;
}
if let Some(s) = v.as_str() {
changed.insert(k.clone(), s.to_string());
}
}
if !changed.is_empty() {
{
let mut configs = self.configs.lock().unwrap();
for (k, v) in &changed {
configs.insert(k.clone(), v.clone());
}
}
let handlers = self.handlers.lock().unwrap();
for h in handlers.iter() {
h(&changed);
}
let keys: Vec<&str> = changed.keys().map(|s| s.as_str()).collect();
eprintln!("[ConfigClient] {} configs changed: {}", changed.len(), keys.join(", "));
}
Ok(())
}
fn apply_configs(&self, data: &HashMap<String, String>) {
let mut configs = self.configs.lock().unwrap();
for (k, v) in data {
if k.starts_with("kenaito.") || k == "latest.fetch.timestamp" {
continue;
}
configs.insert(k.clone(), v.clone());
}
}
}