p-queue 任务一开始就 abort,队列却一直占着名额
旧 p-queue 在调用任务后才注册运行期 abort 监听,可能错过同步取消。核对正式10实现,同时保留底层操作的合作式取消。
任务函数刚开始就调用 controller.abort,queue.add 返回的 Promise 却仍然 pending。单并发队列里的下一项也没有启动,直到原任务对应的长等待自行结束。若异步调度同一次 abort 就正常,需要看取消监听器是在什么时候注册的。
这个现象有 p-queue 9.3.4 的具体报告。正式9.3.3的相关实现同样先调用任务函数,随后才给 signal 注册运行期 abort 监听。任务的同步前缀已经触发取消时,AbortSignal 不会向后来登记的监听器重放过去的事件。
signal 已取消,不代表队列已经观察到它
旧实现进入任务前检查过 signal.aborted,但该检查早于任务自己的同步初始化。初始化中同步触发取消后,仍然可以返回一个未完成 Promise;后加的监听器错过事件,队列就继续等它。
async 函数在第一个 await 前也有同步前缀,不能只按有没有 async 关键字判断。故障还要与任务根本不消费 signal 的情况区分:后者即使队列正确拒绝外层 Promise,底层工作也可能继续。
正式10.0.0在调用任务之前登记监听
10.0.0先建立 abort Promise,再调用任务函数并参加 Promise.race。这覆盖同步初始化期间的取消,不再依赖事件发生在函数返回之后。采用该版本需同时处理 Node 22以上要求,以及旧 .on()、.once() 到 EventTarget 的事件接口迁移。
暂不能升级时,应检查该任务的初始化顺序。如果初始化可能触发取消,可在初始化结束、启动真正工作之前调用 signal.throwIfAborted,让同步取消转为任务拒绝;真正的长任务仍需要自己的取消处理。这是局部业务调整,不能修复任意第三方任务或 thenable 的全部竞态。
不要把取消放进微任务当成通用补丁。那会改变取消时机,而且不保证所有来源都按这个顺序触发;也不要把 concurrency 改大来让后续任务绕过堵住的名额。
队列名额释放后仍要检查底层工作
AbortSignal 是通知协议,不会强行终止任意函数。队列若已经把外层 Promise 拒绝并释放名额,原任务仍可能继续进行文件写入或网络操作。因此应将队列传入的 signal 交给实际支持取消的操作,并清理自己登记的监听器。
回归应覆盖进入队列前取消、初始化同步取消、运行中取消和不取消的正常完成。分别核对 add 的结果、下一项开始时刻、底层操作是否确实停止以及关闭时的资源回收。不要捕获取消异常后继续执行同一项有副作用的任务,或把 Promise 拒绝当成远端操作已撤销。


