Files
kenaito-conf/kenaito-conf-python-client/kenaito_conf/client.py
T
odboy 639475b730 feat: 实现配置删除通知机制(deletedKeys)
- 服务端 fetchChangedConfigs 接口新增 deletedKeys 字段,返回已删除配置的 key 列表
- Go/Node.js/Python/Spring Boot 客户端支持解析 deletedKeys 并删除本地配置
- Spring Boot 客户端新增 removeConfigs 方法,同步更新 Spring Environment
2026-06-17 21:07:51 +08:00

144 lines
4.6 KiB
Python

import logging
import threading
import time
import requests
from kenaito_conf.cache import Cache
from kenaito_conf.config import Config
from kenaito_conf.listener import Listener
logger = logging.getLogger("kenaito_conf")
class ConfigClient:
def __init__(self, config: Config):
self._config = config
self._cache = Cache(config.cache_dir)
self._listener = Listener()
self._configs: dict[str, str] = {}
self._last_time: int = 0
self._lock = threading.RLock()
self._stop_event = threading.Event()
self._started = False
def on_change(self, handler) -> None:
self._listener.on_change(handler)
def get(self, key: str) -> str | None:
with self._lock:
return self._configs.get(key)
def get_all(self) -> dict[str, str]:
with self._lock:
return dict(self._configs)
def start(self) -> None:
with self._lock:
if self._started:
return
self._started = True
try:
self._fetch_all()
except Exception as e:
logger.warning("[ConfigClient] fetch all configs error: %s, trying cache", e)
cached = self._cache.read()
if cached is not None:
self._apply_configs(cached)
logger.info("[ConfigClient] loaded %d configs from cache", len(self._configs))
else:
raise RuntimeError(f"fetch configs failed and no cache: {e}") from e
t = threading.Thread(target=self._sync_loop, daemon=True)
t.start()
def stop(self) -> None:
self._stop_event.set()
def _fetch_all(self) -> None:
url = f"{self._config.base_url}/api/fetchAllConfigs"
payload = {
"appName": self._config.app_name,
"env": self._config.env,
"version": self._config.version,
"ignoreGlobal": self._config.ignore_global,
"token": self._config.token,
}
resp = requests.post(url, json=payload, timeout=10)
resp.raise_for_status()
data = resp.json()
if data.get("code", 0) != 0:
raise RuntimeError(f"server error: {data.get('msg', 'unknown')}")
ts = data.get("latest.fetch.timestamp")
if ts:
self._last_time = int(ts)
self._cache.write(data)
self._apply_configs(data)
logger.info("[ConfigClient] fetched %d configs", len(self._configs))
def _fetch_changed(self) -> None:
url = f"{self._config.base_url}/api/fetchChangedConfigs"
payload = {
"appName": self._config.app_name,
"env": self._config.env,
"version": self._config.version,
"lastTime": self._last_time,
"ignoreGlobal": self._config.ignore_global,
"token": self._config.token,
}
resp = requests.post(url, json=payload, timeout=10)
resp.raise_for_status()
result = resp.json()
if result.get("code", -1) != 0:
raise RuntimeError(f"server error code: {result.get('code')}")
changed_data = result.get("data", {})
ts = changed_data.pop("latest.fetch.timestamp", None)
if ts:
self._last_time = int(ts)
# 处理删除的配置
deleted_keys_str = changed_data.pop("deletedKeys", None)
if deleted_keys_str:
deleted_keys = deleted_keys_str.split(",")
with self._lock:
for key in deleted_keys:
self._configs.pop(key, None)
logger.info("[ConfigClient] %d configs deleted", len(deleted_keys))
if changed_data:
changed = {}
for k, v in changed_data.items():
changed[k] = v
with self._lock:
self._configs.update(changed)
self._listener.notify(changed)
logger.info("[ConfigClient] %d configs changed", len(changed))
def _apply_configs(self, data: dict) -> None:
with self._lock:
for k, v in data.items():
if k.startswith("kenaito.") or k == "latest.fetch.timestamp":
continue
if not isinstance(v, str):
continue
self._configs[k] = v
def _sync_loop(self) -> None:
while not self._stop_event.is_set():
self._stop_event.wait(self._config.sync_interval)
if self._stop_event.is_set():
break
try:
self._fetch_changed()
except Exception:
logger.exception("[ConfigClient] sync error")