Skip to content

Dev - #5

Merged
codename-test merged 4 commits into
mainfrom
dev
Sep 19, 2026
Merged

Dev#5
codename-test merged 4 commits into
mainfrom
dev

Conversation

@codename-test

Copy link
Copy Markdown
Owner

update to 1.3.0

版本号 1.2.4 → 1.3.0(main.py / build.py / api/pages.py / version.json / README)

可靠性
- 通道熔断(#19):新增 circuit_breaker.py,CLOSED/OPEN/HALF_OPEN 三态机;
  滑动窗口 60s 失败率 >50%(样本≥5)或连续失败≥5 熔断;冷却指数退避 30→600s;
  HALF_OPEN 连续 3 次探测成功恢复;4xx 不计失败;状态持久化并可重启恢复
- 出站限流(#21):新增 rate_limiter.py,每通道独立令牌桶(条/分钟),
  拿不到令牌延迟重排而非阻塞 worker
- 延迟重排:queue_backend.defer() 放回队列但不消耗重试次数,
  避免故障期内消息被重试耗尽跌入死信

入口并发
- 端口数据源单线程 HTTPServer → 固定大小线程池(_IngressPool + _ThreadPoolHTTPServer),
  慢请求不再阻塞同数据源其它请求;线程复用使 DB 连接随之复用
- request_queue_size 默认 5 → 128,消除突发时 ~1.1s 的 TCP SYN 重传长尾
- stop_source() 补 server_close(),释放监听 socket 与线程池
- 实测:并发 8 时 p50 127ms → 21ms;加线程不提升吞吐(受单进程串行段限制),故默认取 8

性能
- db/connection.py 补 synchronous=NORMAL + cache_size=-64000
- 实测单次 commit 6.98ms → 0.02ms(约 350×),吞吐 31 → 322 req/s(约 10×)

Bug 修复(严重)
- 重试被推迟约 8 小时:nack() 用 Python datetime.now()(本地时间)写 next_retry_at,
  而 dequeue() 比较 SQLite datetime('now')(UTC);非 UTC 时区下重试形同失效。
  改用 datetime('now','+N seconds') 统一 UTC

API
- GET /api/resilience、POST /api/resilience/rate_limit/<id>、
  POST /api/resilience/breaker/<id>/reset

测试:102 → 131 passed(新增入口并发 3、熔断 13、限流 9、延迟重排 4)
文档:doc/changelog.md 补 v1.3.0;doc/roadmap.md 韧性增强移入已交付
四类问题,均为既有缺陷(非本次引入):

1) 每条消息重复投递 3 次(自 v1.1.0 / commit 47ac7f9 起存在)
   source_manager.process_message 除事件链外,自己又重复 emit 了
   message.parsed 与 message.routed,导致同一消息被投递 3 次
   (解析链内各发一次 + 这里再发两次)。
   定位过程:线上临时探针显示 receivers=1(信号只有一个订阅者)却触发 3 次
   → 不是重复注册,而是重复 emit。
   修复:链路改为只由事件总线单向驱动,process_message 只 emit 一次
   message.received;extra_fields(如 sub_path)改由 parser_engine 在触发
   message.parsed 之前合并(原先它只在冗余 emit 里才生效)。

2) sent_at 与 created_at 时区基准不一致
   dt_now_str() 用本地时间写 sent_at,而 created_at 由 CURRENT_TIMESTAMP 生成
   (UTC),两者相差一个时区偏移——消息列表里"创建/发送时间"对不上,
   按二者计算延迟会错好几小时。修复:dt_now_str() 返回 UTC。

3) cleanup_old_messages 比较时多带了 localtime
   与上一条同类:created_at 是 UTC,却用 datetime('now','localtime',…) 比较,
   导致清理比配置的保留时长晚一个时区偏移。

4) 通道页 3 个按钮点了就报错(后端路由缺失)
   - POST   /api/channels/<id>/test       → 404(路由不存在)
   - POST   /api/channels/<id>/duplicate  → 404(路由不存在)
   - DELETE /api/channels/<id>            → 405(该路径只注册了 PUT)
   修复:补齐三个接口。删除同时做级联清理——连接未开启 PRAGMA foreign_keys
   (SQLite 默认关闭),schema 里声明的 ON DELETE CASCADE 并不生效,
   不显式清理会留下指向已删通道的孤儿绑定、导致路由反复失败;
   该通道待发的队列任务移入死信队列(而非静默丢弃)。

注:api/channels.py 的入参校验与上述接口落在同一 diff hunk,无法再拆,
故一并进入本提交;db/schema.py 的 dedup_keys 建表也被带入,因为删除通道的
级联清理依赖该表存在(否则中间提交会 500)。

验证:修正后的树上 tests/test_channel_endpoints.py 9/9 通过。
一、P1(渠道级语义与并发一致性)
- 渠道级重发:retry_message(msg_id, mode, scope="failed") 默认只重发上次失败的
  渠道,不再把已成功的渠道重复推送;结果条目补 channel_id 作为定位依据,
  旧记录缺该字段时回退"整条重发"。scope="all" 保留整体重推。
- 渠道级去重:粒度从「整条消息」下沉到 message × channel。新增
  dedup_keys(channel_id, dedup_key, sent_at) 表,逐绑定判定——命中的只跳过该
  渠道,全部命中才标记 DISCARDED。修掉原实现只取第一个绑定的去重表达式
  (其余被 break 忽略)、以及任一命中就丢弃整条消息的问题。
- 多 worker 结果回写一致性:channel_results 是「读-改-写」,并发下会丢更新。
  实测 60 线程并发写同一条 trace:加锁保留 60/60,不加锁只活下来 2/60。
  修复:按 hash(trace_id) 取模的条带锁(固定 64 把,不增长、无需清理)。

二、P2 收口
- 优雅停机:worker.stop_workers(timeout=30) join 等在途任务,超时把未完成的
  刷入死信队列;main.shutdown() 明确「先停接收再停 worker」的顺序。
- 可观测性:GET /api/metrics(队列深度/死信/各通道成功率/端到端延迟/熔断限流),
  ?hours=N 可调窗口;不上 Prometheus。
- API 入参校验(improvement #27):新增 api/validation.py,ValidationError 由
  统一错误处理器转 400,覆盖数据源/通道/模板/设置/消息批量写接口。
- 去重判定收敛(#31):两处发送路径共用 sender_engine._plan_dedup()。
- 模块解耦:log.py 不再 import db,DBLogHandler 移到 db/log_handler.py,
  依赖方向固定为 main → db → log。
- 清理间隔可配:EGO_CLEANUP_INTERVAL(默认 600s)。
- EventBus 生命周期收口:message.sending / message.sent 真正接入发送链路
  (终态事件刻意在释放条带锁之后 emit,避免订阅者回调造成自锁)。
- requirements.txt 移除 UTF-8 BOM。
- 系统级测试:tests/test_system_reliability.py(故障注入 / 熔断×队列 /
  重试→死信 / 崩溃恢复 / 并发消费 / 消息守恒)。

三、韧性 WebUI(此前只有 API,只能 curl 或改库)
- 通道编辑弹窗新增「出站限流(条/分钟)」输入框,保存通道时一并写入;
  复制通道会连带复制限流配置。
- 通道列表新增「韧性」列:限流徽章、熔断倒计时徽章 + 一键手工恢复。
- 设置页新增「韧性(通道熔断)」卡片,7 个参数可直接手工调整
  (留空 = 用默认/环境变量)。参数取值优先级:system_config > 环境变量 > 默认,
  带 TTL 缓存并在保存后立即失效,改完即时生效、无需重启。
  取值优先级这样设计是为了不把调整能力绑在 API 上——API 尚未完备时,
  直接改库或走设置页同样有效。

四、文档
- doc/changelog.md 补全 v1.3.0;doc/roadmap.md 更新已交付与待办。
- README 中英文:配置章节改为「SQLite 为运行时真相源、config/*.json 为导出备份
  介质」并列出三条加载规则;环境变量表补 13 个新参数(中英一致)。

测试:102 → 214 passed。
补上此前 README 完全没提的两块能力:

- 通道熔断:触发条件、指数退避冷却、半开探测、4xx 不计失败、
  熔断期间消息留在队列(不消耗重试、不丢弃)
- 出站限流:按通道(条/分钟),令牌耗尽时排队等待而非丢弃
- 韧性界面:通道列表「韧性」列、通道弹窗限流输入、设置页熔断参数卡片;
  参数取值优先级 system_config > 环境变量 > 默认,改完即时生效
- 可观测性:/api/metrics、/api/resilience、/api/queue/stats、/api/health
- 优雅停机:收到 SIGTERM 先停接收、再等在途任务、超时转死信

同时修正两处已过时的描述:

- 「消息去重」写明粒度是 message × channel(各绑定独立键与窗口,
  全部命中才整条 DISCARDED)
- 「消息重发」写明默认只重发失败渠道(scope=all 可整体重推)
@codename-test
codename-test merged commit c8a4b09 into main Sep 19, 2026
1 check passed
codename-test added a commit that referenced this pull request Sep 26, 2026
v1.3.1 改进清单 #5。

后端已到位(SMTPChannel 有 test() 做 连接+登录+NOOP;
api/channels.py test 端点经 create_channel 按 type 路由,
channel_loader._unpack_test_result 兼容 bool/(ok,err)),故本次:

- i18n.py 新增 ch.test_conn=「测试连接」/ch.test_send=「测试发送」
  (en: Test connection / Test send)。
- templates/channels_page.html 测试按钮 title 按类型:
  ch.type=='smtp_email' → 测试连接,否则 → 测试发送。
- 测试补充:
  * test_smtp_email_channel.py:SMTP test() 成功路径
    (连接+登录+NOOP 全过 → (True,''),命令含 AUTH/NOOP)。
  * test_channel_endpoints.py:非 SMTP 通道(wechat_work_bot)
    走现有 .test() 逻辑,起真 webhook 桩回 {errcode:0} → {ok:true};
    + 前端 i18n 文案 key 存在性断言。

全量 311 passed。

Co-Authored-by: QwenPaw <[email protected]>
codename-test added a commit that referenced this pull request Sep 26, 2026
v1.3.2 改进清单 #5。

- 现状:conflict_reason 上传即拒同名内置,但「手动放同名文件到用户目录」
  或迁移残留会静默遮蔽内置(用户版优先),无任何提示。
- plugin_paths 新增 shadows_builtin(kind, fn):用户目录存在该文件
  且内置目录也有同名 → True。
- list_plugins 每项加 shadow_builtin 字段(用户遮蔽内置=True,否则 False)。
- api/parsers.py::_decorate 给解析器补 shadow_builtin。
- 前端 channels_page / parsers_page 列表加「遮蔽内置」警告徽章
  (bg-warning ⚠),不强制删除。
- i18n 新增 ch.shadow_builtin / parser.shadow_builtin(中英)。
- 测试:test_plugin_dirs.py 新增 2 项(列表标记 + /api/parsers 暴露字段)。

全量 313 passed。

Co-Authored-by: QwenPaw <[email protected]>
codename-test added a commit that referenced this pull request Sep 26, 2026
#5 补充 + 文档收口:

- #5 通道主表 shadow 徽章:
  * api/channels.py api_channels() 每项加 plugin_shadowed
    (通道引用的插件被同名用户副本遮蔽 → True)
  * channels_page.html 主表类型列加 ⚠ ch.shadow_builtin 徽章
  * test_plugin_dirs.py 新增 TestChannelApiShadowedPlugin
- #6 迁移说明(中英):README 加「从旧版本升级 v1.3.0→v1.3.1+」
  三步(导出/升级镜像/恢复)
- #7 卷操作语义(中英):up/down/down -v 三行警告,down -v 删卷丢插件
- #8 令牌桶(中):出站限流段补 Token Bucket + burst=分钟额度

全量 314 passed。

Co-Authored-by: QwenPaw <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant