feat(config): integrate etcd for service discovery and config management
- Add etcd-client dependency for distributed configuration storage - Implement EtcdConfig with priority: etcd > environment variables > defaults - Add ServiceRegistry for service registration with lease keep-alive - Integrate etcd-based service discovery for appks gRPC connections - Add service watcher for real-time service instance updates - Migrate Redis configuration from single URL to cluster node list - Update Dockerfile with default IMKS_HOST and IMKS_PORT environment variables - Add etcd bootstrap configuration through environment variables - Implement Redis cluster URL building with optional authentication
This commit is contained in:
+24
-39
@@ -1,71 +1,56 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# imks — IM 实时消息服务 环境变量配置
|
# imks — IM 实时消息服务 环境变量配置
|
||||||
# 复制此文件为 .env 并修改相应值
|
# 复制此文件为 .env 并修改相应值
|
||||||
|
#
|
||||||
|
# 配置优先级: etcd > 环境变量 > 默认值
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# --- 部署模式 ---
|
# --- etcd 连接(启动引导,必须从环境变量读取)---
|
||||||
# Adapter 模式: "local" (单节点) | "redis" | "nats"
|
ETCD_ENDPOINTS=http://localhost:2379
|
||||||
IMKS_ADAPTER=local
|
ETCD_KEY_PREFIX=/appks/
|
||||||
|
|
||||||
# 当前节点唯一标识(默认取主机名)
|
# --- 服务自身 ---
|
||||||
|
# 注册到 etcd 的地址
|
||||||
|
# IMKS_ADDR=0.0.0.0:3000
|
||||||
|
|
||||||
|
# --- 部署模式 ---
|
||||||
|
# Adapter: "local" (单节点) | "redis" | "nats"
|
||||||
|
IMKS_ADAPTER=redis
|
||||||
|
|
||||||
|
# 当前节点唯一标识
|
||||||
# IMKS_SERVER_ID=imks-node-1
|
# IMKS_SERVER_ID=imks-node-1
|
||||||
|
|
||||||
# Redis 连接(IMKS_ADAPTER=redis 时必需)
|
# Redis Cluster 节点列表(逗号分隔 host:port)
|
||||||
# IMKS_REDIS_URL=redis://localhost:6379
|
IMKS_REDIS_CLUSTER_NODES=localhost:6379,localhost:6380,localhost:6381,localhost:6382,localhost:6383,localhost:6384
|
||||||
|
|
||||||
# NATS 连接(IMKS_ADAPTER=nats 时必需)
|
# Redis 密码(可选)
|
||||||
|
# IMKS_REDIS_PASSWORD=
|
||||||
|
|
||||||
|
# NATS 连接(IMKS_ADAPTER=nats 时使用)
|
||||||
# IMKS_NATS_URL=nats://localhost:4222
|
# IMKS_NATS_URL=nats://localhost:4222
|
||||||
|
|
||||||
# --- WebTransport (QUIC) ---
|
# --- WebTransport (QUIC) ---
|
||||||
# 启用 WebTransport 服务(需要 TLS 证书)
|
|
||||||
# IMKS_WT_ENABLED=false
|
# IMKS_WT_ENABLED=false
|
||||||
# IMKS_WT_PORT=3001
|
# IMKS_WT_PORT=3001
|
||||||
# IMKS_WT_CERT_PATH=/path/to/cert.pem
|
# IMKS_WT_CERT_PATH=/etc/imks/cert.pem
|
||||||
# IMKS_WT_KEY_PATH=/path/to/key.pem
|
# IMKS_WT_KEY_PATH=/etc/imks/key.pem
|
||||||
|
|
||||||
# --- 数据库 ---
|
# --- 数据库 ---
|
||||||
# PostgreSQL 连接字符串
|
|
||||||
# DATABASE_URL=postgres://imks:password@localhost:5432/imks
|
# DATABASE_URL=postgres://imks:password@localhost:5432/imks
|
||||||
DATABASE_URL=postgres://localhost/imks
|
DATABASE_URL=postgres://localhost/imks
|
||||||
|
|
||||||
# 连接池配置
|
|
||||||
# DATABASE_MAX_CONNECTIONS=10
|
# DATABASE_MAX_CONNECTIONS=10
|
||||||
# DATABASE_MIN_CONNECTIONS=2
|
# DATABASE_MIN_CONNECTIONS=2
|
||||||
# DATABASE_CONNECT_TIMEOUT=30
|
|
||||||
# DATABASE_IDLE_TIMEOUT=600
|
|
||||||
|
|
||||||
# --- appks gRPC 连接 ---
|
# --- appks gRPC 连接 ---
|
||||||
# appks 核心服务地址
|
# fallback:imks 优先通过 etcd 发现 appks 地址
|
||||||
# APPKS_GRPC_ADDR=http://localhost:50051
|
# APPKS_GRPC_ADDR=http://localhost:50051
|
||||||
|
|
||||||
# 连接超时(秒)
|
|
||||||
# APPKS_GRPC_TIMEOUT=10
|
# APPKS_GRPC_TIMEOUT=10
|
||||||
|
|
||||||
# mTLS 配置(生产环境必需)
|
|
||||||
# APPKS_GRPC_TLS_CA_CERT=/path/to/ca.pem
|
|
||||||
# APPKS_GRPC_TLS_CLIENT_CERT=/path/to/client.pem
|
|
||||||
# APPKS_GRPC_TLS_CLIENT_KEY=/path/to/client-key.pem
|
|
||||||
# APPKS_GRPC_TLS_DOMAIN=appks.internal
|
|
||||||
|
|
||||||
# --- OpenTelemetry 可观测性 ---
|
# --- OpenTelemetry 可观测性 ---
|
||||||
# 服务名
|
|
||||||
# OTEL_SERVICE_NAME=imks
|
# OTEL_SERVICE_NAME=imks
|
||||||
# OTEL_SERVICE_VERSION=0.1.0
|
# OTEL_SERVICE_VERSION=0.1.0
|
||||||
|
|
||||||
# OTLP 收集器地址
|
|
||||||
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
||||||
# 协议: grpc | http/protobuf
|
|
||||||
# OTEL_EXPORTER_OTLP_PROTOCOL=grpc
|
|
||||||
|
|
||||||
# 启用/禁用 telemetry
|
# --- 日志 ---
|
||||||
# OTEL_TRACES_ENABLED=true
|
|
||||||
# OTEL_METRICS_ENABLED=true
|
|
||||||
# OTEL_LOGS_ENABLED=true
|
|
||||||
|
|
||||||
# 日志级别: trace | debug | info | warn | error
|
|
||||||
RUST_LOG=info
|
RUST_LOG=info
|
||||||
# 日志格式: json | pretty
|
|
||||||
# LOG_FORMAT=json
|
|
||||||
|
|
||||||
# 部署环境标识
|
|
||||||
# OTEL_RESOURCE_ATTRIBUTES_DEPLOYMENT=development
|
|
||||||
|
|||||||
Generated
+20
@@ -939,6 +939,24 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "etcd-client"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ed900ba953ca6bf1fadb75e0c6b73d8463b9e2bb6bdb7b4573e8e7295852fbe"
|
||||||
|
dependencies = [
|
||||||
|
"http 1.4.2",
|
||||||
|
"prost",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"tonic",
|
||||||
|
"tonic-build",
|
||||||
|
"tonic-prost",
|
||||||
|
"tonic-prost-build",
|
||||||
|
"tower",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "etcetera"
|
name = "etcetera"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -1612,6 +1630,7 @@ dependencies = [
|
|||||||
"base64",
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
|
"etcd-client",
|
||||||
"fred",
|
"fred",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
@@ -1629,6 +1648,7 @@ dependencies = [
|
|||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
"tonic",
|
"tonic",
|
||||||
"tonic-build",
|
"tonic-build",
|
||||||
"tonic-health",
|
"tonic-health",
|
||||||
|
|||||||
+12
-10
@@ -14,15 +14,15 @@ name = "imks"
|
|||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tonic = { version = "0.14.6", features = ["tls-ring"] }
|
tonic = { version = "0.14", features = ["tls-ring"] }
|
||||||
prost = "0.14.3"
|
prost = "0.14"
|
||||||
prost-types = "0.14"
|
prost-types = "0.14"
|
||||||
tonic-build = "0.14.6"
|
tonic-build = "0.14"
|
||||||
tonic-health = "0.14.6"
|
tonic-health = "0.14"
|
||||||
tonic-prost = "0.14.6"
|
tonic-prost = "0.14"
|
||||||
tokio = { version = "1.52.3", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
actix-web = { version = "4.13.0", features = [] }
|
actix-web = { version = "4", features = [] }
|
||||||
actix-ws = { version = "0.4.0", features = [] }
|
actix-ws = { version = "0.4", features = [] }
|
||||||
actix-rt = "2"
|
actix-rt = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = { version = "1" }
|
serde_json = { version = "1" }
|
||||||
@@ -34,6 +34,8 @@ rand = "0.9"
|
|||||||
wtransport = "0.7"
|
wtransport = "0.7"
|
||||||
dashmap = "6"
|
dashmap = "6"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
|
etcd-client = { version = "0.18", features = ["tls"] }
|
||||||
|
tokio-stream = { version = "0.1", features = ["sync"] }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "fmt", "registry"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "fmt", "registry"] }
|
||||||
@@ -52,5 +54,5 @@ arc-swap = "1"
|
|||||||
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tonic-prost-build = "0.14.6"
|
tonic-prost-build = "0.14"
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5"
|
||||||
+3
-2
@@ -18,7 +18,6 @@ RUN cargo build --release --bin imks && \
|
|||||||
strip target/release/imks
|
strip target/release/imks
|
||||||
|
|
||||||
FROM ubuntu:26.04
|
FROM ubuntu:26.04
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends ca-certificates curl && \
|
apt-get install -y --no-install-recommends ca-certificates curl && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
@@ -27,10 +26,12 @@ COPY --from=builder /app/target/release/imks /usr/local/bin/imks
|
|||||||
COPY --from=builder /app/migrate/ /app/migrate/
|
COPY --from=builder /app/migrate/ /app/migrate/
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN useradd -m -u 1000 imks && chown -R imks:imks /app
|
RUN useradd -m -u 1000 imks && chown -R imks:imks /app
|
||||||
USER imks
|
USER imks
|
||||||
|
|
||||||
|
ENV IMKS_HOST=0.0.0.0
|
||||||
|
ENV IMKS_PORT=3000
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
HEALTHCHECK --interval=15s --timeout=3s --start-period=10s --retries=3 \
|
HEALTHCHECK --interval=15s --timeout=3s --start-period=10s --retries=3 \
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
use etcd_client::{Client, PutOptions, GetOptions, WatchOptions};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
|
||||||
|
/// etcd-backed config reader. Priority: etcd > env var > default.
|
||||||
|
pub struct EtcdConfig {
|
||||||
|
client: Arc<Mutex<Client>>,
|
||||||
|
prefix: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EtcdConfig {
|
||||||
|
pub async fn connect(endpoints: Vec<String>, prefix: &str) -> Result<Self, String> {
|
||||||
|
let client = Client::connect(endpoints, None)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("etcd connect: {e}"))?;
|
||||||
|
Ok(Self { client: Arc::new(Mutex::new(client)), prefix: prefix.to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get config value: etcd first, then env var, then default.
|
||||||
|
pub async fn get(&self, key: &str, default: &str) -> String {
|
||||||
|
let etcd_key = format!("{}config/{}", self.prefix, key);
|
||||||
|
if let Ok(mut c) = self.client.try_lock() {
|
||||||
|
if let Ok(resp) = c.get(etcd_key.as_str(), None).await {
|
||||||
|
if let Some(kv) = resp.kvs().first() {
|
||||||
|
if let Ok(v) = kv.value_str() {
|
||||||
|
if !v.is_empty() {
|
||||||
|
tracing::info!(key, value = v, "config from etcd");
|
||||||
|
return v.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(v) = std::env::var(key) {
|
||||||
|
if !v.is_empty() {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_parsed<T: std::str::FromStr>(&self, key: &str, default: T) -> T
|
||||||
|
where T::Err: std::fmt::Display, T: std::fmt::Display
|
||||||
|
{
|
||||||
|
let s = self.get(key, &default.to_string()).await;
|
||||||
|
s.parse().unwrap_or(default)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the etcd client for use by ServiceRegistry.
|
||||||
|
pub fn client(&self) -> Arc<Mutex<Client>> {
|
||||||
|
self.client.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Discover service instances registered under /{prefix}/services/{service_name}/.
|
||||||
|
pub async fn discover_service(&self, service_name: &str) -> Result<Vec<String>, String> {
|
||||||
|
let prefix = format!("{}services/{}/", self.prefix, service_name);
|
||||||
|
let mut client = self.client.lock().await;
|
||||||
|
let resp = client
|
||||||
|
.get(prefix.as_str(), Some(GetOptions::new().with_prefix()))
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("etcd get {prefix}: {e}"))?;
|
||||||
|
|
||||||
|
let mut addrs = Vec::new();
|
||||||
|
for kv in resp.kvs() {
|
||||||
|
if let Ok(value) = kv.value_str() {
|
||||||
|
if let Ok(instance) = serde_json::from_str::<serde_json::Value>(value) {
|
||||||
|
if let Some(addr) = instance.get("addr").and_then(|v| v.as_str()) {
|
||||||
|
addrs.push(addr.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tracing::info!(service = service_name, count = addrs.len(), "discovered instances");
|
||||||
|
Ok(addrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Watch a service for live updates.
|
||||||
|
pub fn start_service_watcher(&self, service_name: &str) {
|
||||||
|
let client = self.client.clone();
|
||||||
|
let prefix = self.prefix.clone();
|
||||||
|
let svc = service_name.to_string();
|
||||||
|
let watch_prefix = format!("{}services/{}/", prefix, svc);
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let mut stream = {
|
||||||
|
let mut c = client.lock().await;
|
||||||
|
match c.watch(watch_prefix.as_str(), Some(WatchOptions::new().with_prefix())).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(service = %svc, error = %e, "watch failed, retry in 3s");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
while let Some(resp) = stream.next().await {
|
||||||
|
if let Ok(resp) = resp {
|
||||||
|
for event in resp.events() {
|
||||||
|
if let Some(kv) = event.kv() {
|
||||||
|
let addr = kv.value_str().unwrap_or_default();
|
||||||
|
let key = kv.key_str().unwrap_or_default();
|
||||||
|
match event.event_type() {
|
||||||
|
etcd_client::EventType::Put => {
|
||||||
|
tracing::info!(service = %svc, key, addr, "service up");
|
||||||
|
}
|
||||||
|
etcd_client::EventType::Delete => {
|
||||||
|
tracing::info!(service = %svc, key, "service down");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register this service instance in etcd.
|
||||||
|
pub struct ServiceRegistry {
|
||||||
|
client: Arc<Mutex<Client>>,
|
||||||
|
prefix: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceRegistry {
|
||||||
|
pub fn new(client: Arc<Mutex<Client>>, prefix: &str) -> Self {
|
||||||
|
Self { client, prefix: prefix.to_string() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn register(&self, service_name: &str, addr: &str) -> Result<(), String> {
|
||||||
|
let instance_id = uuid::Uuid::now_v7().to_string();
|
||||||
|
let addr = addr.to_string();
|
||||||
|
let key = format!("{}services/{}/{}", self.prefix, service_name, instance_id);
|
||||||
|
|
||||||
|
let instance = serde_json::json!({
|
||||||
|
"addr": &addr,
|
||||||
|
"port": 0,
|
||||||
|
"version": env!("CARGO_PKG_VERSION"),
|
||||||
|
});
|
||||||
|
let value = serde_json::to_string(&instance).map_err(|e| format!("json: {e}"))?;
|
||||||
|
|
||||||
|
let lease = {
|
||||||
|
let mut client = self.client.lock().await;
|
||||||
|
client.lease_grant(15, None).await.map_err(|e| format!("lease: {e}"))?
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut client = self.client.lock().await;
|
||||||
|
let opts = PutOptions::new().with_lease(lease.id());
|
||||||
|
client.put(key.clone(), value, Some(opts)).await.map_err(|e| format!("put: {e}"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!(service = service_name, instance = %instance_id, addr = %addr, "registered in etcd");
|
||||||
|
|
||||||
|
let c = self.client.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let result = {
|
||||||
|
let mut client = c.lock().await;
|
||||||
|
client.lease_keep_alive(lease.id()).await
|
||||||
|
};
|
||||||
|
match result {
|
||||||
|
Ok((_keeper, mut stream)) => {
|
||||||
|
while stream.next().await.is_some() {}
|
||||||
|
}
|
||||||
|
Err(e) => tracing::warn!(lease_id = lease.id(), error = %e, "keepalive failed"),
|
||||||
|
}
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||||
|
let new_lease = {
|
||||||
|
let mut client = c.lock().await;
|
||||||
|
client.lease_grant(15, None).await
|
||||||
|
};
|
||||||
|
if let Ok(lr) = new_lease {
|
||||||
|
let instance = serde_json::json!({"addr": &addr, "port": 0, "version": env!("CARGO_PKG_VERSION")});
|
||||||
|
if let Ok(v) = serde_json::to_string(&instance) {
|
||||||
|
let mut client = c.lock().await;
|
||||||
|
let opts = PutOptions::new().with_lease(lr.id());
|
||||||
|
let _ = client.put(key.clone(), v, Some(opts)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ pub mod auth;
|
|||||||
pub mod database;
|
pub mod database;
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod etcd;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod pb;
|
pub mod pb;
|
||||||
pub mod repo;
|
pub mod repo;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::sync::{Arc, OnceLock};
|
|||||||
|
|
||||||
use imks::database::{Database, DatabaseConfig};
|
use imks::database::{Database, DatabaseConfig};
|
||||||
use imks::engine::server::EngineConfig;
|
use imks::engine::server::EngineConfig;
|
||||||
|
use imks::etcd::{EtcdConfig, ServiceRegistry};
|
||||||
use imks::repo::MessageRepo;
|
use imks::repo::MessageRepo;
|
||||||
use imks::rpc::{AppksClients, RpcConfig};
|
use imks::rpc::{AppksClients, RpcConfig};
|
||||||
use imks::socket::adapter::{LocalBroadcastFn, NatsAdapter, RedisAdapter};
|
use imks::socket::adapter::{LocalBroadcastFn, NatsAdapter, RedisAdapter};
|
||||||
@@ -17,6 +18,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
telemetry::health::init_counters();
|
telemetry::health::init_counters();
|
||||||
|
|
||||||
let deploy = DeployConfig::from_env();
|
let deploy = DeployConfig::from_env();
|
||||||
|
|
||||||
|
// Read etcd bootstrap config from env (these MUST come from env, not etcd)
|
||||||
|
let etcd_endpoints: Vec<String> = std::env::var("ETCD_ENDPOINTS")
|
||||||
|
.unwrap_or_else(|_| "http://localhost:2379".to_string())
|
||||||
|
.split(',')
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect();
|
||||||
|
let etcd_prefix = std::env::var("ETCD_KEY_PREFIX")
|
||||||
|
.unwrap_or_else(|_| "/appks/".to_string());
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
adapter = %deploy.adapter_mode,
|
adapter = %deploy.adapter_mode,
|
||||||
server_id = %deploy.server_id,
|
server_id = %deploy.server_id,
|
||||||
@@ -24,11 +36,33 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
"Starting imks server"
|
"Starting imks server"
|
||||||
);
|
);
|
||||||
|
|
||||||
let addr = "0.0.0.0:3000";
|
let addr = "0.0.0.0:50048";
|
||||||
|
|
||||||
let rt = tokio::runtime::Runtime::new()?;
|
let rt = tokio::runtime::Runtime::new()?;
|
||||||
|
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
|
// --- etcd: connect, register, discover appks ---
|
||||||
|
let etcd = EtcdConfig::connect(etcd_endpoints, &etcd_prefix).await
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
tracing::error!(error = %e, "etcd required but unavailable");
|
||||||
|
panic!("etcd required: {e}")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register this service so others can discover us
|
||||||
|
let registry = ServiceRegistry::new(etcd.client(), &etcd_prefix);
|
||||||
|
let imks_addr = etcd.get("IMKS_ADDR", "0.0.0.0:3000").await;
|
||||||
|
registry.register("imks", &imks_addr).await.ok();
|
||||||
|
|
||||||
|
// Discover appks from etcd (priority > env)
|
||||||
|
let appks_addr = etcd.discover_service("appks").await
|
||||||
|
.ok()
|
||||||
|
.and_then(|addrs| addrs.into_iter().next())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
std::env::var("APPKS_GRPC_ADDR").unwrap_or_else(|_| "http://localhost:50051".to_string())
|
||||||
|
});
|
||||||
|
tracing::info!(appks_addr = %appks_addr, "appks discovered via etcd");
|
||||||
|
etcd.start_service_watcher("appks");
|
||||||
|
|
||||||
let engine_config = EngineConfig::default();
|
let engine_config = EngineConfig::default();
|
||||||
let mut builder = SocketServerBuilder::new(engine_config);
|
let mut builder = SocketServerBuilder::new(engine_config);
|
||||||
let namespace_holder: Arc<OnceLock<Arc<imks::socket::namespace::NamespaceManager>>> =
|
let namespace_holder: Arc<OnceLock<Arc<imks::socket::namespace::NamespaceManager>>> =
|
||||||
@@ -37,10 +71,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// Pre-configure adapter for Redis/NATS mode.
|
// Pre-configure adapter for Redis/NATS mode.
|
||||||
match deploy.adapter_mode.as_str() {
|
match deploy.adapter_mode.as_str() {
|
||||||
"redis" => {
|
"redis" => {
|
||||||
|
let cluster_url = deploy.redis_cluster_url();
|
||||||
let message_bus = Arc::new(
|
let message_bus = Arc::new(
|
||||||
RedisMessageBus::new(&deploy.redis_url)
|
RedisMessageBus::new(&cluster_url)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to connect to Redis: {e}"))?,
|
.map_err(|e| format!("Failed to connect to Redis cluster: {e}"))?,
|
||||||
);
|
);
|
||||||
let redis_client = message_bus.client().clone();
|
let redis_client = message_bus.client().clone();
|
||||||
let server_id = deploy.server_id.clone();
|
let server_id = deploy.server_id.clone();
|
||||||
@@ -88,7 +123,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// Initialize database + gRPC + service
|
// Initialize database + gRPC + service
|
||||||
let service: Option<Arc<MessageService>> = {
|
let service: Option<Arc<MessageService>> = {
|
||||||
let rpc_config = RpcConfig::from_env();
|
let rpc_config = RpcConfig {
|
||||||
|
appks_addr: appks_addr.clone(),
|
||||||
|
..RpcConfig::from_env()
|
||||||
|
};
|
||||||
let db_config = DatabaseConfig::from_env();
|
let db_config = DatabaseConfig::from_env();
|
||||||
|
|
||||||
match AppksClients::connect(&rpc_config).await {
|
match AppksClients::connect(&rpc_config).await {
|
||||||
|
|||||||
@@ -12,14 +12,17 @@ pub struct RedisMessageBus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RedisMessageBus {
|
impl RedisMessageBus {
|
||||||
pub async fn new(redis_url: &str) -> Result<Self, MessageBusError> {
|
/// Connect to a Redis cluster.
|
||||||
|
///
|
||||||
|
/// `cluster_url` should be in `redis-cluster://` format, e.g.:
|
||||||
|
/// `redis-cluster://host1:6379,host2:6379,host3:6379`
|
||||||
|
pub async fn new(cluster_url: &str) -> Result<Self, MessageBusError> {
|
||||||
let config =
|
let config =
|
||||||
Config::from_url(redis_url).map_err(|e| MessageBusError::Redis(e.to_string()))?;
|
Config::from_url(cluster_url).map_err(|e| MessageBusError::Redis(e.to_string()))?;
|
||||||
|
|
||||||
let client = Client::new(config.clone(), None, None, None);
|
let client = Client::new(config.clone(), None, None, None);
|
||||||
let subscriber = SubscriberClient::new(config, None, None, None);
|
let subscriber = SubscriberClient::new(config, None, None, None);
|
||||||
|
|
||||||
// connect() starts the connection task; result is checked by wait_for_connect()
|
|
||||||
let _ = client.connect().await;
|
let _ = client.connect().await;
|
||||||
let _ = subscriber.connect().await;
|
let _ = subscriber.connect().await;
|
||||||
|
|
||||||
@@ -32,6 +35,7 @@ impl RedisMessageBus {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| MessageBusError::Redis(e.to_string()))?;
|
.map_err(|e| MessageBusError::Redis(e.to_string()))?;
|
||||||
|
|
||||||
|
tracing::info!(cluster_url, "Redis cluster connected");
|
||||||
Ok(Self { client, subscriber })
|
Ok(Self { client, subscriber })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
-4
@@ -10,8 +10,11 @@ use std::env;
|
|||||||
pub struct DeployConfig {
|
pub struct DeployConfig {
|
||||||
/// "local" | "redis" | "nats"
|
/// "local" | "redis" | "nats"
|
||||||
pub adapter_mode: String,
|
pub adapter_mode: String,
|
||||||
/// Redis connection URL (used when adapter_mode = "redis").
|
/// Redis cluster nodes, comma-separated host:port pairs.
|
||||||
pub redis_url: String,
|
/// Example: "redis1:6379,redis2:6379,redis3:6379"
|
||||||
|
pub redis_cluster_nodes: String,
|
||||||
|
/// Redis password (optional).
|
||||||
|
pub redis_password: String,
|
||||||
/// NATS connection URL (used when adapter_mode = "nats").
|
/// NATS connection URL (used when adapter_mode = "nats").
|
||||||
pub nats_url: String,
|
pub nats_url: String,
|
||||||
/// Unique server ID for this node.
|
/// Unique server ID for this node.
|
||||||
@@ -32,8 +35,9 @@ impl DeployConfig {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
adapter_mode: env::var("IMKS_ADAPTER").unwrap_or_else(|_| "local".into()),
|
adapter_mode: env::var("IMKS_ADAPTER").unwrap_or_else(|_| "local".into()),
|
||||||
redis_url: env::var("IMKS_REDIS_URL")
|
redis_cluster_nodes: env::var("IMKS_REDIS_CLUSTER_NODES")
|
||||||
.unwrap_or_else(|_| "redis://localhost:6379".into()),
|
.unwrap_or_else(|_| "localhost:6379,localhost:6380,localhost:6381".into()),
|
||||||
|
redis_password: env::var("IMKS_REDIS_PASSWORD").unwrap_or_default(),
|
||||||
nats_url: env::var("IMKS_NATS_URL").unwrap_or_else(|_| "nats://localhost:4222".into()),
|
nats_url: env::var("IMKS_NATS_URL").unwrap_or_else(|_| "nats://localhost:4222".into()),
|
||||||
server_id,
|
server_id,
|
||||||
webtransport_enabled: env::var("IMKS_WT_ENABLED")
|
webtransport_enabled: env::var("IMKS_WT_ENABLED")
|
||||||
@@ -47,6 +51,17 @@ impl DeployConfig {
|
|||||||
key_path: env::var("IMKS_WT_KEY_PATH").unwrap_or_default(),
|
key_path: env::var("IMKS_WT_KEY_PATH").unwrap_or_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a redis-cluster URL from cluster_nodes and optional password.
|
||||||
|
/// Format: redis-cluster://[:password@]host1:port1,host2:port2,...
|
||||||
|
pub fn redis_cluster_url(&self) -> String {
|
||||||
|
let auth = if self.redis_password.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(":{}@", self.redis_password)
|
||||||
|
};
|
||||||
|
format!("redis-cluster://{}{}", auth, self.redis_cluster_nodes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DeployConfig {
|
impl Default for DeployConfig {
|
||||||
|
|||||||
Reference in New Issue
Block a user