Skip to content

Latest commit

 

History

History
183 lines (146 loc) · 10.3 KB

File metadata and controls

183 lines (146 loc) · 10.3 KB

执行计划 —— 0.3.0:把连接池不变量写进类型里

输入 fix-plan-issue-15-16.md(19 项)+ openkal 体系适配
基线 origin/master @ 2cec1c1(0.2.10)
目标 0.3.0 —— 单 PR 全量实现 P0 + P1 + P2 + P3-A/B/D
分支 fix/pool-invariants-and-sigpipe

0. 为什么是一个 PR、为什么是 0.3.0

原方案建议拆 4 个 PR(0.2.11 → 0.2.12 → 0.2.13 → 0.3.0)。这里合并成一个,理由是依赖方向而不是省事:

  • P1 的十处补丁,和 P3-A 的 RAII guard,是同一件事的两种写法。先合 P1 再合 P3-A,等于把十个 connectionClose = true 写进去、下一个 PR 再全部删掉。
  • P2-C1/C2/C4(状态行、header 循环、Content-Length)在三个入口各有一份拷贝。P3-B 把三份合成一份之后,这三项只需要改一处;不合并就要改九处再删六处。
  • §1.1 的矩阵证明:分批修 = 每批只落到一两份拷贝上#14 就是这样引入 A1/A2 回归的。一个 PR 是让这张矩阵不再存在的唯一方式。

版本取 0.3.0 而不是 0.2.11:HttpResponse / HttpClientConfig 加了字段(非破坏),严格解析会把过去静默接受的畸形响应变成显式错误(行为变更)。语义化版本下,行为变更进 minor。

无感升级的边界(release note 必须点名):

  • 现有代码不需要改一行就能编译:只加字段,不改签名,ok() 语义不变。
  • 过去被静默接受的畸形响应现在会报错 —— 这不是新 bug,是过去在静默损坏数据。

1. 八个视角,各自要什么

视角 要求 落在哪
架构 池不变量不能靠十个早退路径各自记得维护 P3-A PooledConnection(默认丢弃)+ P3-B 三份 reader 合一
稳定性 库不得杀死宿主进程;不得因一个 header 分配 2 GiB P0(MSG_NOSIGNAL/SO_NOSIGPIPE)、P2-E2(分配上限)
优雅简洁 一个概念一处实现 parse_hex;read_response_head / read_body 各一份;parse_status_line 一份
用户体验 调用方要能知道 body 被截断了;空闲连接被服务端关掉不该变成 No response P2-F1 bodyComplete+bodyError、P3-D 陈旧连接重试
兼容性 只加字段,不改签名,ok() 不变 HttpResponse / HttpClientConfig 追加字段;TlsSocket::read 保留
跨平台 Linux / macOS / Windows / Termux / openkal 全绿 #ifdef 而非 if constexpr(5e7d66f 的教训);平台事实集中在 platform.cppm
一致性 send / send_stream / download_to_file 三个入口行为逐条相同 P3-B 之后三者共用同一个 head/body reader
无感升级 升级不需要改代码 见 §0

2. 任务依赖图

T0  分支 + 计划                                    ✔
 │
 ├─ T1  P0 SIGPIPE(socket.cppm)  必须最先
 │   │   P0-1 send() + MSG_NOSIGNAL
 │   │   P0-2 socket() 后 setsockopt(SO_NOSIGPIPE)
 │   │   理由:T5 让 pool_.erase() 在更多路径上执行,
 │   │        erase → ~TlsSocket → close_notify → send()。
 │   │        T1 没做之前,T5 等于扩大 SIGPIPE 触发面。
 │   │
 │   ├─ T2  P2-D1/D2(tls.cppm / http.cppm 的 write_all)
 │   │       read_some() 三态:Data / WouldBlock / Eof / Error
 │   │       write_all 遇 WANT_WRITE 改为 wait_writable + 总时限
 │   │       (T3 的 reader 建立在三态之上)
 │   │
 │   └─ T3  P3-B 统一 reader(http.cppm)  ← 吸收 P2-C1/C2/C4 + P1-D1
 │           parse_status_line(导出,严格)
 │           read_response_head → expected<ResponseHead, string>
 │           read_body → BodyOutcome{Complete|Truncated|ClosedByPeer|Stopped}
 │            ├ 吸收 P1-B2(parse_chunk_size_line,删 parse_hex)
 │            ├ 吸收 P2-C3(块后 CRLF 校验)
 │            └ 吸收 P2-E2(分配上限,chunk 分片读)
 │           │
 │           ├─ T4  P3-A PooledConnection(默认丢弃)← 吸收 P1-A/B/C 全部十项
 │           │   │   递归重定向:递归前必须 keep() 或 drop(),不能让析构跨过递归
 │           │   │
 │           │   ├─ T5  P2-E1 bounded drain(重定向/非 2xx/文件打不开时省一次握手)
 │           │   ├─ T6  P2-F1/F2 bodyComplete + bodyError(statusText 不再被覆盖)
 │           │   └─ T7  P3-D 陈旧连接重试(一次,仅在零响应字节时)
 │           │
 │           └─ T8  纯单测:parse_status_line / parse_chunk_size_line / parse_content_length
 │
 ├─ T9  集成测试:进程内 mbedtls TLS listener(T4 后)
 │       tinyhttps 只支持 https,本地明文 listener 用不了 → 必须自带 TLS 服务端
 │       T1..T11 用例见 §4
 │
 ├─ T10 openkal 体系(与 T1..T9 并行调研,结论落地在 T1/platform.cppm)
 │
 └─ T11 文档 / release note / README ← 依赖全部
     └─ T12 CI 全绿 → 自审 → PR → release → gtc 补 gitcode → mcpp-index → 生态验证

关键路径:T1 → T3 → T4 → T9 → T12。 可并行:T8、T10 与 T2..T7 无共享状态。


3. 原方案 19 项 → 本计划落点

原 ID 落在 备注
P0-1 / P0-2 T1 逐字采纳
P1-A1/A2/A3、B1、C1/C2/C3/C4/C5 T4 十项全部由 PooledConnection 默认丢弃消除,不逐条打补丁
P1-B2 T3 并入统一 chunk reader;parse_hex 删除
P1-D1 T3 ResponseHead::contentLength 只有 int64_t 一种类型
P2-C1 T3 新增导出 parse_status_line
P2-C2 T3 read_response_head 全程用 read_complete_line
P2-C3 T3 块后 CRLF 必须是空行
P2-C4 T3 统一 parse_content_length
P2-E1 T5 drain_body,上限 64 KiB
P2-E2 T3 maxResponseBodyBytes(默认 64 MiB);chunk 按固定缓冲分片读,不按块大小分配
P2-D1 T2 TlsSocket::read_some 三态;read() 保留
P2-D2 T2 write_allwait_writable
P2-F1 T6 bodyComplete,ok() 不变(方案 a,非破坏)
P2-F2 T6 statusText 不再被错误信息覆盖;原因进 bodyError
P3-A T4 采纳
P3-B T3 采纳(head + body 都合)
P3-C 带缓冲的读 不做 见 §5
P3-D T7 采纳 curl 策略 + retryOnStaleConnection 开关(默认 true)

两个 review 决策点的裁定

  • P2-F1 ok() 是否要求 bodyComplete不要求(方案 a)。目标写明「无感升级」;让 ok() 变严会让现有代码在升级后行为改变,与之直接冲突。截断可见性由新字段提供。
  • P3-D 是否限制到幂等方法不限制(curl 策略),加 retryOnStaleConnection 开关。重试窗口是「一个响应字节都没收到」,此时服务端已发 FIN、应用层没看到这个请求;限制到幂等方法等于对 LLM/API 客户端(POST 为主)这个修复基本无效。

4. 测试矩阵

4.1 纯单测(无需服务端)—— T8

parse_status_line / parse_chunk_size_line / parse_content_length / append_within_limit 表驱动。 其中 parse_status_line 必须覆盖 issue #15 的原始输入 "BBBB 999 XHTTP/1.1 200 OK" → 拒绝。

4.2 集成测试 —— T9

为什么必须自带 TLS 服务端:send/send_stream/download_to_file 三个入口开头都有 if (parsed.scheme != "https") return "Only HTTPS is supported",本地明文 listener 根本进不去。 所以测试自带一个进程内 mbedtls TLS listener(自签证书,verifySsl = false), MBEDTLS_SSL_SRV_C / MBEDTLS_PEM_PARSE_C 在依赖的 mbedtls 3.6.1 配置里已开启(已核验)。

# 用例 断言 覆盖
T1 服务端发一半 CL body 后挂住 请求 2 statusCode == 200 P1-A1
T2 同上,并断言服务端 accept 次数 == 2 唯一能抓住「看起来对了」的断言 P1-A1
T3 chunked 版本同上 同 T1+T2 P1-A3
T4 服务端发一半后关连接 不崩;进程未被信号杀死 #16
T5 SIGPIPE 默认处置下跑 T4(子进程) WIFSIGNALED == false,退出码 ≠ 141 P0
T6 Content-Length: 4294967296 不得当成 0;连接不得回池 P1-D1
T7 302 + CL body + keep-alive → 同 host 内容正确 + accept 次数正确 P1-C3
T8 404 + error body + keep-alive,同 client 再发一个 第二个请求正确 P1-C4
T9 chunk 头是 zz / 空行 报错,不得当成终止块 P1-B2
T10 状态行 BBBB 999 XHTTP/1.1 200 OK Invalid status line,不是 999 P2-C1
T11 截断的 body bodyComplete == false P2-F1
T12 正常 keep-alive 两请求 accept 次数 == 1(不能因为修得太狠而丢掉复用) 回归护栏
T13 服务端在两请求之间关闭空闲连接 请求 2 成功,accept 次数 == 2 P3-D

T12 与 T2 是一对:T2 保证脏连接被丢弃,T12 保证干净连接被保留。只有 T2 会让「关掉连接池」这种过度修复也通过。


5. 明确不做的,和为什么

不做的理由
P3-C 带缓冲的读 原方案自陈「不要当成性能修复来卖」,其主要理由是「让『这条连接干净吗』可回答」——而 T4 的 guard + T3 的严格 framing 已经把这个问题回答了:干净 = read_body 返回 Complete。缓冲区是第二种表达,不是必需的一种。单独立项。
openkal 原生 socket 后端 见 §6。

6. openkal 体系

调研 T10 输出决定落点;先记录已核验的事实:

  • openkal-musl/port/src/okm_net.c:546-550:MSG_NOSIGNAL接受并忽略(注释原文:"asks that a signal not be raised. There are no signals here"),rest = flags & ~(MSG_NOSIGNAL | MSG_DONTWAIT),其余 flag 返回 -ENOSYS。 ⇒ P0-1 在 openkal 体系上是正确且无副作用的,而且必须写成 #ifdef MSG_NOSIGNAL(否则 macOS 分支编不过,见 5e7d66f)。
  • SO_NOSIGPIPE 是 BSD/macOS 的拼法,musl 头文件不定义 ⇒ P0-2 在 openkal-musl 上自动不编入。

7. 交付顺序

  1. T1 → T2 → T3 → T4 → T5/T6/T7(实现)
  2. T8 / T9(测试)
  3. T10 结论落地
  4. T11 文档 + release note
  5. 本地 mcpp build && mcpp test 全绿 → 推 PR → CI 全绿 → 自审 → 合并
  6. tag + GitHub release → gtc 本地补 gitcode 资源(不等 release CI)
  7. mcpp-index 增加 0.3.0 条目 → 生态真实验证