feat(redis): 加 REDIS_CLUSTER 支持 OSS Cluster 端点(heicode-rd 必需)

heicode-rd(Azure Redis Enterprise)database clusteringPolicy=OSSCluster,
裸 redis.Redis 客户端在多分片下 keys()/跨 slot 操作会误路由/抛 MOVED。

- REDIS_CLUSTER truthy → 用 redis.asyncio.cluster.RedisCluster(URL 或
  host/port 两种入参,密码/TLS 同样支持)。cluster 模式无 DB select,
  REDIS_DB 被忽略(仅逻辑 DB0)。
- 不设时维持 standalone 行为,完全向后兼容。
- 测试加 cluster 用例;manifest/DELIVERY 补 REDIS_CLUSTER 说明。

验证:连接配置单测 4 项 + REDIS_FAKE 回退 + test-runtime-contract /
test-contract-freeze / test-merge-smoke 全 PASS。

影响范围:仅 agent_swarm orchestrator 连接层;不改契约/计费/审计/密钥落地。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Fasthei
2026-06-12 17:00:37 +08:00
co-authored by Claude Opus 4.8
parent aa498fc318
commit 35aab3a643
4 changed files with 54 additions and 13 deletions
+1
View File
@@ -79,6 +79,7 @@ kubectl apply -f k8s/agent-deployment-v2.yaml
| `REDIS_HOST` / `REDIS_PORT` / `REDIS_DB` | Redis 连接(明文)| 生产必需(或用 `REDIS_URL`)|
| `REDIS_URL` | 完整连接串,优先于上面离散变量;`rediss://` 启用 TLS,凭据写在 URL 里 | 用托管 TLS Redis(Azure Cache / Redis Enterprise)时设置;经 Secret/secret_ref 注入,勿内联 |
| `REDIS_PASSWORD` / `REDIS_SSL` | 离散方式的密码 / 启用 TLS(truthy)| 配合 `REDIS_HOST` 用;密码经 Secret 注入 |
| `REDIS_CLUSTER` | 用 Redis Cluster 协议端点(truthy)| 连 Azure Redis Enterprise(clusteringPolicy=OSSCluster)必设,否则多分片下 `keys()`/跨 slot 误路由 |
| `REDIS_FAKE` / `ALLOW_MEMORY_STORE` | 内存回退 | **仅开发/CI**,生产禁用 |
| `ENABLE_PLANNER_FALLBACK` | 无 Manager 分工时启用规划回退 | 默认关闭 |
| `ENABLE_REVIEW_LOOP` / `MAX_REVIEW_CYCLES` | 主控评审/重做循环 + 汇总 | 默认关闭 / 默认 2 |
+14 -9
View File
@@ -47,18 +47,23 @@ spec:
value: "6379"
- name: REDIS_DB
value: "0"
# To use a managed TLS Redis (e.g. Azure Cache / Redis Enterprise)
# instead of the in-cluster StatefulSet, drop the REDIS_HOST/PORT above
# and inject a credentialed URL from a Secret (never inline a password):
# - name: REDIS_URL
# valueFrom:
# secretKeyRef:
# name: swarm-integration # rediss://:<password>@<host>:10000/0
# key: redis_url
# Or use discrete vars: REDIS_HOST/REDIS_PORT + REDIS_SSL=1 and
# To use a managed Redis (e.g. Azure Redis Enterprise heicode-rd)
# instead of the in-cluster StatefulSet, point REDIS_HOST at it, enable
# cluster mode (Enterprise uses clusteringPolicy=OSSCluster), and inject
# the access key from a Secret (never inline a password):
# - name: REDIS_HOST
# value: "heicode-rd.southeastasia.redis.azure.net"
# - name: REDIS_PORT
# value: "10000"
# - name: REDIS_CLUSTER # required for OSSCluster endpoints
# value: "1"
# - name: REDIS_SSL # "1" only if the DB clientProtocol=Encrypted
# value: "0"
# - name: REDIS_PASSWORD
# valueFrom:
# secretKeyRef: { name: swarm-integration, key: redis_password }
# Or supply a single credentialed URL: REDIS_URL=rediss://:<key>@host:10000/0
# (also set REDIS_CLUSTER=1 for an OSSCluster endpoint).
- name: LOG_LEVEL
value: "INFO"
resources:
+22 -4
View File
@@ -45,6 +45,10 @@ class RedisClient:
self.db = int(os.getenv("REDIS_DB", "0"))
self.password = os.getenv("REDIS_PASSWORD") or None
self.ssl = _truthy(os.getenv("REDIS_SSL"))
# Cluster mode is required for endpoints speaking the Redis Cluster protocol
# (e.g. Azure Redis Enterprise with clusteringPolicy=OSSCluster). A plain
# client there silently mis-routes keys() / cross-slot ops on multi-shard DBs.
self.cluster = _truthy(os.getenv("REDIS_CLUSTER"))
def _fallback_allowed(self) -> bool:
return _truthy(os.getenv("REDIS_FAKE")) or _truthy(os.getenv("ALLOW_MEMORY_STORE"))
@@ -53,15 +57,28 @@ class RedisClient:
"""Build a real redis-py client from config (URL preferred, else discrete vars).
Connection kwargs only — does NOT connect. Credential-bearing values
(URL/password) are never logged here or by callers.
(URL/password) are never logged here or by callers. When ``REDIS_CLUSTER``
is set, a cluster-aware client is built (cluster mode has no DB select, so
``REDIS_DB`` is ignored — Redis Cluster only exposes logical DB 0).
"""
if redis is None:
raise RuntimeError("redis-py is not installed")
common = dict(
decode_responses=True,
socket_connect_timeout=5,
socket_keepalive=True,
)
if self.cluster:
from redis.asyncio.cluster import RedisCluster # lazy: only when cluster mode on
if self.url:
return RedisCluster.from_url(self.url, **common)
kwargs = dict(host=self.host, port=self.port, **common)
if self.password:
kwargs["password"] = self.password
if self.ssl:
kwargs["ssl"] = True
return RedisCluster(**kwargs)
common["socket_keepalive"] = True
if self.url:
# from_url honours the scheme: rediss:// → TLS, and any user:pass in the URL.
return redis.Redis.from_url(self.url, **common)
@@ -74,10 +91,11 @@ class RedisClient:
def _target_desc(self) -> str:
"""Human-readable target for logs — no credentials. Never logs the URL itself."""
mode = "cluster" if self.cluster else "standalone"
if self.url:
scheme = self.url.split("://", 1)[0] if "://" in self.url else "redis"
return f"REDIS_URL ({scheme}://, TLS={scheme == 'rediss'})"
return f"{self.host}:{self.port} (TLS={self.ssl})"
return f"REDIS_URL ({scheme}://, TLS={scheme == 'rediss'}, {mode})"
return f"{self.host}:{self.port} (TLS={self.ssl}, {mode})"
def _make_fake_client(self):
"""Return an in-process fakeredis client (dev/CI fallback only)."""
+17
View File
@@ -88,6 +88,22 @@ def test_url_precedence_and_tls():
print("ok: REDIS_URL precedence + rediss:// TLS, credentials not logged")
def test_cluster_mode():
# OSS Cluster endpoint (e.g. Azure Redis Enterprise clusteringPolicy=OSSCluster):
# must build a cluster-aware client so keys()/cross-slot routing is correct.
c, _ = _fresh_client({
"REDIS_CLUSTER": "1",
"REDIS_HOST": "heicode-rd.southeastasia.redis.azure.net",
"REDIS_PORT": "10000",
"REDIS_PASSWORD": "key-from-secret",
})
real = c._make_real_client() # constructs lazily; does NOT connect
assert type(real).__name__ == "RedisCluster", type(real).__name__
assert "cluster" in c._target_desc(), c._target_desc()
assert "key-from-secret" not in c._target_desc(), c._target_desc()
print("ok: REDIS_CLUSTER → RedisCluster client (OSS Cluster safe)")
def main():
try:
import redis # noqa: F401
@@ -97,6 +113,7 @@ def main():
test_default_plaintext()
test_discrete_password_and_ssl()
test_url_precedence_and_tls()
test_cluster_mode()
print("\nALL PASS: redis connection config")
return 0