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:
co-authored by
Claude Opus 4.8
parent
aa498fc318
commit
35aab3a643
@@ -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 |
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user