Alternatives considered
保留引导写入处的 /* v8 ignore */,只修复顺序。 已否决:正是那处 ignore 让这个 TDZ 回归得以未被发现地进入代码库。移除它使该 catch 成为被度量的分支,因此按文件计的 100% 覆盖率现在能证明该失败路径确实被执行。
通过捕获更多绑定方法来修复 flush 竞态。 已否决:这正是已经失败过的做法。绑定一个可调用对象解决的是引用解析,而不是对该可调用对象所读取的可变状态的并发访问。只有对共享账本施加互斥才能消除该竞态。
用大小阈值来保护残余数据(只复制大帧)。 已否决:该分支在每次包含换行符的读取时运行一次,复制的规模受残余数据自身长度约束(始终是一个不完整行),而阈值会引入一个可调参数和第二条代码路径,却换不来任何可度量的节省。无条件地做大小恰当的复制更简单,且始终正确。
通过 seam 断言残余数据的内存效应。 已否决:被保留的分配透过 CodeRunResult 不可观测,因此黑盒测试无法区分已修复与未修复。转而抽取出 detachResidual,把底层存储的不变量变成一个确定性的单元测试。
仅用一个发后不理的 unref SIGKILL 定时器来回收同进程组存活者。 已否决:unref 的定时器不会让宿主保持存活,因此一个在宽限窗口内退出的宿主(一次性运行、一个配置子进程)永远不会触发 SIGKILL,存活者被 reparent 给 init,这是同一个"没有子进程存活得比 fiber 更久"的违规换了个形态,而且 teardown 的"await 每个子进程退出"的 JSDoc 会变为不实。在一个 ref 的轮询上 await 进程组的消亡,让宿主恰好保持存活足够长以完成回收,在常见的空进程组情形下代价为零。
用 process.kill(pid, 0) 抛出 ESRCH 来断言回收。 已否决:一个被 SIGKILL 的进程会作为僵尸进程滞留,直到它的父进程 wait() 它,而在一个 PID 1 不回收孤儿进程的容器里,signal-0 探测会持续成功,因此该断言会在跨环境时误报失败。一个停止推进的心跳文件检测的是"不再执行",而被回收的进程和僵尸进程都满足这一点。
用一个普通的 set_result 完成跨事件循环的 Future 并依赖 GIL。 已否决:GIL 序列化字节码,但并不使 asyncio.Future 跨事件循环安全:从一个并非其事件循环所属的线程完成一个 Future,不会调度它的回调,也不会唤醒该事件循环。在拥有该 Future 的事件循环上调用 call_soon_threadsafe 才是有文档记载的机制。
在结算之后让 SIGKILL 定时器继续处于装设状态(早先的同进程组修复)。 已否决:一个被留待在 leader 被回收后长达 graceMs 才触发的 unref 定时器,可能 kill(-pid) 一个被回收(recycled)的 pgid,击中一个无关的进程组;危险是那次成功执行的 kill,而 killGroup 吞掉 ESRCH 无法阻止它。在进程组被确认为空后清除该定时器,把复用窗口收窄到真正存在存活者的情形,此时进程组不为空以供复用。
只用继承而来的硬限制来约束 rlimit。 已否决:那会静默地抬高一个比请求更严格的继承软限制,放松了该约束本应保持的那种收束。用每一侧各自继承而来的界来约束该侧(随后把 soft 钉在 hard 之下),在 soft 和 hard 两者上都保持配置值与继承值中的最严格者。
按序列化开销对宿主侧的 capMessage 兜底做计费,与子进程的 _cap_message 相符。 已否决:这两处上限守护的是不同的东西。_cap_message 的输出会作为一个 JSON 字符串再次穿过 fd 3,因此帧上限约束的是它转义后的宽度——那里必须按序列化计费。capMessage 的输出直接进入 CodeRunResult.error.message,绝不会再次穿过一个受帧上限约束的通道,因此对它所保留内容的诚实度量是模型可见字符串的原始字节长度。一个诚实的子进程已经按序列化开销设过上限,而原始长度 ≤ 序列化开销,因此一条格式良好的消息会原样通过;一条伪造的、控制字符密集的消息可能序列化到其原始长度约 6 倍,但由于它不经过任何受上限约束的通道,按那个被抬高的传输宽度对它计费只会截断一条尺寸合法的诊断,而换不来任何收束上的收益。每一侧的 JSDoc 都记录了这一区分,并指向另一侧。
每来一个 data 分片就把散逸的管道输出推入一条条目。 已否决:logs 条目在下游会用 \n 拼接,因此一个传输分片边界会变成一个模型可见的换行符——一次被拆散在多次管道读取中的原生写入会带着无端的换行回读。按真正的换行符聚合(原始分片缓冲 + 在 0x0a 处切分)与子进程的按行粒度的 log 帧相符;账本仍然通过在残余数据将要越过预算时把它准入并截断,来约束一场不含换行符的洪泛。
逐帧强制 fd-3 帧上限(在计数器检查之前先切分)以避免一次批次边缘的误拒。 已否决:帧上限检查在任何 Buffer.concat 之前读取字节计数器,正是为了让一个敌意程序无法迫使宿主内存达到 64 MiB 帧上限的约 2 倍(计数器与那次拼接是所持全部内容的第二份副本)。先切分以对单个帧计费,会在拒绝一个超上限的帧之前就 Buffer.concat 它,从而重新引入那种翻倍——正是出于这个原因,有两个回归测试断言了先计数后拼接的顺序。逐帧顺序本会修复的那次批次边缘误拒(一个合法的接近上限的帧,其携带换行符的分片同时也带上了下一帧的起始字节,在一次管道读取中把计数器推过上限)只有当 maxLogBytes/maxValueBytes 被配置到距 64 MiB 帧上限一次管道读取以内时才可达——比 32/64 KiB 的默认值高出好几个数量级。在任何配置下都抵御敌意输入的内存安全边界,优先于一个仅在病态的接近上限配置下才可达的误拒;计数器的超额计数与这一权衡都记录在该检查处。
当合并预算被越过时,按残余数据到达顺序冲刷两个散逸管道。 已否决:stdout 与 stderr 是相互独立的 OS 流,它们的 data 事件本就彼此之间、以及与子进程自己的 fd-3 log 帧之间不确定地交错。seam 的 CodeRunResult.logs JSDoc 写着「in order」,周围的文字把它限定为一条流之内的程序发出顺序——跨并发流的顺序在这里本质上是尽力而为,因为没有任何宿主侧的冲刷顺序能够重建内核已经丢失的真实交错,因此在冲刷处保留一条残余数据的到达顺序换不来任何东西。一个固定的排空顺序与任何顺序一样有效。跟踪一个逐残余数据的到达计次以先排空较早的管道,会增加一个分支,它的两侧只在两个 OS 管道的相对时机上才触发,而 os.sched_yield 并不使之具有确定性,因此该分支无法在不写一个不稳定测试的情况下被覆盖——有成本却没有可观测的契约收益。
在运行时按地址空间对子进程日志账本计量,而不是在加载期拒绝该配置。 已否决:对每次子进程写入做一次精确的序列化开销检查,要么是一次完整的 encode——正是一次超大写入负担不起、而账本那处廉价预检本就为规避它而存在的那次分配——要么是一个逐字符的 Python 循环,它会烧掉 CPU 预算(一次 10 MB 的合法写入会在 cpuSeconds: 1 之下触发 SIGXCPU)。每一种运行时做法都是在热路径上拿内存界限换另一种资源界限。地址空间的突破是 maxLogBytes/addressSpaceMb 组合的属性,而不是任何一次具体写入的属性,因此在加载期一次性拒绝这个不兼容的组合,能在没有任何逐次写入代价的情况下消除整类问题,并保留 _LogStream 原有的按字符计数的缓冲,它一旦预算放进地址空间就是内存安全的。