Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

fix(server): _parse_redis_url 只认一种 URL 形态且无 match 时静默回落 localhost——执行状态存储与队列/事件总线连到两个 Redis(rediss:// 明文降级、ACL 形态静默失效、启动期一次性错配不自愈)

Closed
#88 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
45/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python, redis
Domain
backend, databases

Research direction

Start with _parse_redis_url in plaita/server/factory.py (lines 18-32) and its caller in create_storage_component (lines 92-104), then check how plaita/storage/redis.py (around lines 160-165) uses host, port, db and password. Add tests/unit/test_redis_url_parse.py covering the URL forms listed in the issue. Done when those forms return the expected tuples, redis:// with an empty target raises instead of falling back to localhost, and the old warning path no longer runs.

Written by the indexing model from the issue text.

Description

优先级 P1 · 依赖:无

基线 HEAD = 65f48f2

Summary

create_storage_component 只对 execution / flow 两类存储走 _parse_redis_url 解析出的
连接参数,而同一进程里的任务队列与事件总线用的是原始 redis_url。_parse_redis_url
的正则只认 redis://[:password@]host:port[/db] 这一种形态,且 match is None 时既不报错
也不降级提示,直接返回默认的 localhost:6379/0。两种 URL 形态因此被解析到不同端点。

  1. plaita/server/factory.py:18-32 —— 解析器本体(正则只接受带端口的 redis://;无
    match 分支零日志):
def _parse_redis_url(redis_url: str):
    """解析 Redis URL 为连接参数"""
    host, port, db, password = "localhost", 6379, 0, None
    if redis_url:
        try:
            pattern = r'redis://(:(.*?)@)?(.*?):(.*?)(/(\d+))?$'
            match = re.match(pattern, redis_url)
            if match:
                password = match.group(2) or None
                host = match.group(3) or host
                port = int(match.group(4)) if match.group(4) else port
                db = int(match.group(6)) if match.group(6) else db
        except Exception as e:
            logger.warning("解析Redis URL失败: %s, 使用默认连接参数", e)
    return host, port, db, password
  1. plaita/server/factory.py:92-104 —— 解析结果被当作连接的唯一来源,
    execution / flow 存储都用它构造:
        if component_type in ("execution", "flow"):
            host, port, db, password = _parse_redis_url(redis_url)
            conn = dict(host=host, port=port, db=db, password=password)
            # 多租户路由包装器:按 ContextVar 租户选择 namespace
            # (default → 历史 plaita 前缀;其余 plaita:{tenant})
            if kwargs.get("tenant_routing"):
                from plaita.server.tenant_context import (
                    TenantRoutingExecutionStorage,
                    TenantRoutingFlowStorage,
                )
                if component_type == "execution":
                    return TenantRoutingExecutionStorage(**conn)
                return TenantRoutingFlowStorage(**conn)
  1. plaita/storage/redis.py:160-165 —— 这四个参数确实被用来建客户端,解析值是实际生效的
    连接目标(不是被忽略的配置):
            self.client = redis.Redis(
                host=host,
                port=port,
                db=db,
                password=password,
                decode_responses=True,  # 自动将字节解码为字符串
  1. plaita/server/flow_worker.py:3262-3267 —— 集群档 CLI 里存储组件由上述函数创建,
    传入的是未加工的 args.redis_url(见 :3258):
        execution_storage = create_storage_component(
            args.execution_storage_type,
            "execution",
            tenant_routing=True,
            **storage_kwargs
        )
  1. plaita/server/flow_worker.py:2024 —— 同一个 worker 的队列客户端用原始 URL:
        redis_client = redis_client or Redis.from_url(redis_url)
  1. plaita/server/event_filter.py:407 —— 事件过滤器同样用原始 URL:
        redis_client = Redis.from_url(redis_url)
  1. plaita/event/redis.py:773 —— 事件总线也用原始 URL:
            self.redis = await aioredis.from_url(self.redis_url)
  1. plaita/server/flow_worker.py:3114-3116 —— 该 URL 在集群档由 PLAITA_REDIS_URL 注入,
    默认值 redis://localhost:6379/0(也就是「不配置时恰好正确」的形态):
    # Redis参数(支持环境变量 PLAITA_REDIS_URL / REDIS_URL)
    parser.add_argument("--redis-url",
                      default=os.environ.get("PLAITA_REDIS_URL", os.environ.get("REDIS_URL", "redis://localhost:6379/0")),
  1. plaita-console/cluster_config.yaml:45,56 —— 用户配置的 redis.url 原样注入到
    worker / 各外延服务的环境变量:
      PLAITA_REDIS_URL: ${redis.url}
  1. plaita-console/backend/api/cluster.py:1285 —— 该字段是自由文本,无格式校验:
    redis_url: str = Field("redis://localhost:6379/0", description="Redis URL")

全仓只有这一个 Redis URL 解析器,且没有任何测试覆盖它:

$ grep -rn "_parse_redis_url" --include='*.py' .
./plaita/server/factory.py:18:def _parse_redis_url(redis_url: str):
./plaita/server/factory.py:93:            host, port, db, password = _parse_redis_url(redis_url)

$ grep -rn "_parse_redis_url" tests/ plaita-console/
grep exit=1
(零命中)

实测(/tmp/plaita-audit-venv/bin/python,真 plaita.server.factory):

$ /tmp/plaita-audit-venv/bin/python -c "from plaita.server.factory import _parse_redis_url as p; [print(repr(u),'->',p(u)) for u in ['redis://plaita-redis','rediss://redis.internal:6380/0','redis://user:pass@host:6379/0','redis://host/3','redis://localhost:6379/0']]"
解析Redis URL失败: invalid literal for int() with base 10: 'pass@host:6379', 使用默认连接参数
'redis://plaita-redis' -> ('localhost', 6379, 0, None)
'rediss://redis.internal:6380/0' -> ('localhost', 6379, 0, None)
'redis://user:pass@host:6379/0' -> ('user', 6379, 0, None)
'redis://host/3' -> ('localhost', 6379, 0, None)
'redis://localhost:6379/0' -> ('localhost', 6379, 0, None)

补充形态(同一次实测):

$ /tmp/plaita-audit-venv/bin/python -c "from plaita.server.factory import _parse_redis_url as p; [print(repr(u),'->',p(u)) for u in ['redis://:pw@host:6379/2','redis://host:6379','rediss://user:pw@host:6380/1']]"
'redis://:pw@host:6379/2' -> ('host', 6379, 2, 'pw')
'redis://host:6379' -> ('host', 6379, 0, None)
'rediss://user:pw@host:6380/1' -> ('localhost', 6379, 0, None)

即:rediss://、user:pass@、无端口(redis://host、redis://host/3)三种形态被静默
改写成 localhost:6379;user:pass@ 形态还会把用户名当成 host(('user', …))并丢掉密码,
而 int() 异常只在 logger.warning 里出现一次,进程继续用错误参数运行。只有
redis://:password@host:port/db(无用户名)与 redis://host:port[/db] 是正确的。

影响

部署形态:console 集群档(plaita-console/cluster_config.yaml 把 redis.url 注入
PLAITA_REDIS_URL)或任何用 --redis-url / 环境变量指向非 localhost 或非 redis://
形态
的 worker / 外延服务。

有条件(redis://host:port 之外)的 URL 一配置就会出现同一进程连两个 Redis:
任务队列(flow_worker.py:2024)、事件过滤器(event_filter.py:407)、事件总线
(event/redis.py:773)连到运维指定的实例;而执行状态/流程定义存储
(factory.py:93 → RedisExecutionStorage)连回 localhost:6379/0。后果:

  • 状态分裂:worker 在正确的 Redis 上领取任务,却把 running/error 终态写进另一个
    Redis(或写不进去——容器里 localhost 通常根本没有 Redis,于是每次读写都抛连接错误);
    队列上「任务已投递」与存储里「无此 execution」并存,console 看到的执行记录与实际调度脱节。
  • rediss:// 是明文降级:TLS 目标被改写为明文 redis://localhost:6379/0,凭据与执行
    状态(含节点输入输出)要么落到未预期的本机实例,要么以明文尝试连接,属于安全边界失效。
  • ACL 形态静默失效:user:pass@ 被解析成 host=user、password=None,连接要么失败、
    要么以错误的身份/无认证重试,且 int() 异常只留一行 warning,运维侧无告警、无 fail-fast。
  • 这是启动期的一次性错配,不会自愈:worker 能启动、能领取任务,错误只在业务终态写盘时
    以「连不上」或「状态对不上」的形式显现,排查成本远高于启动时直接报错。

不修的后果:运维改一次 redis.url(引入 rediss://、用户名或省略端口)就会把执行状态写散
到两个 Redis,且系统全程不报错。

建议

修复点唯一:plaita/server/factory.py:18-32 的 _parse_redis_url。

  • 最小修法:用标准库替换手写正则,一次性拿到全部连接字段并保留 scheme:

    from urllib.parse import urlparse
    u = urlparse(redis_url)
    return u.hostname, u.port or 6379, int((u.path or "").lstrip("/") or 0), u.password
    

    并按 u.scheme(redis / rediss)决定是否启用 TLS,把 username 一并透出
    (RedisExecutionStorage 需相应接受 username / ssl 参数)。

  • match is None、urlparse 失败、或 scheme 不在白名单内时,改为抛异常中止启动
    (fail-closed),不要静默回退默认端点。

  • 更稳的形态:让 execution / flow 存储与队列共用同一个
    Redis.from_url(redis_url) 客户端(现有 RedisExecutionStorage.__init__ 已支持
    client= 注入,factory.py 只需把它透传),从根上消除「一个 URL 两种解析」的可能。

验收条件:

  • 新增 tests/unit/test_redis_url_parse.py,对
    redis://host:6379/0、redis://host、redis://host/3、redis://:pw@host:6379/2、
    redis://user:pw@host:6379/0、rediss://user:pw@host:6380/1 断言解析结果分别等于
    ('host',6379,0,None)、('host',6379,0,None)、('host',6379,3,None)、
    ('host',6379,2,'pw')、('host',6379,0,'pw')(username=user、scheme=rediss)——即
    上表里的 ('localhost', 6379, 0, None) 全部消失。
  • 断言非法形态(如 redis:// 空目标)抛异常而非返回默认值。
  • 复现命令应输出:'redis://plaita-redis' -> ('plaita-redis', 6379, 0, None)、
    'rediss://redis.internal:6380/0' -> ('redis.internal', 6380, 0, None)、
    'redis://user:pass@host:6379/0' -> ('host', 6379, 0, 'pass')(username=user),
    且 stderr 无 解析Redis URL失败 的降级 warning。

边界

  • 只处理 _parse_redis_url 这一处解析错误与它的静默回退;不顺手改动存储/队列的其余配置
    面(连接池、超时、TLS 证书校验等)。
  • 不改 plaita/node/redis.py:32-40:那里的节点用自己的 urlparse,已正确取
    username/password,不属于本缺陷。
  • console 侧对 redis.url 无格式校验(cluster.py 只做 Redis.from_url 连通性测试)是
    本缺陷的放大条件而非根因,本单只要求解析器 fail-closed;给 console 加字段校验可另开单。
  • 仓内默认值(flow_worker.py:3116、event_filter.py:497、cluster.py:1285)都是
    redis://localhost:6379/0,恰好是解析器唯一能正确处理的形态,所以默认部署与现有 E2E
    (plaita-console/e2e.yaml 用的 redis://plaita-e2e-redis:6379/0 带端口)都不会触发;
    触发前提是运维把 URL 改成 rediss://、user:pass@ 或省略端口。
  • 查重:gh issue list -R jeffkit/plaita --state all --limit 100 返回的 31 条中无 Redis URL
    解析相关单(#24 租户凭据路由、#40 存储后端列表均为不同组件/不同根因),本单不与既有单重复。
Dominant language
Python
Stars
0
Forks
1
Avg merge
2h 33m
Merged PRs (30d)
9

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from jeffkit/plaita

All issues in jeffkit/plaita

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.