多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Effect RPC 服务端故障隔离:`disableFatalDefects` 选项的设计与实践

Effect RPC 服务端故障隔离:`disableFatalDefects` 选项的设计与实践 Effect RPC 服务端故障隔离disableFatalDefects选项的设计与实践【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3codeEffect 的unstable/rpc模块是一套基于 Schema 定义、支持流式响应、覆盖 HTTP/WebSocket/Socket/Stdio 等多种传输协议的类型化 RPC 框架。本文围绕.changeset/pre/calm-panthers-nail.md记录的变更展开该变更将disableFatalDefects选项正式接入RpcServer.layerHttp、RpcServer.toHttpEffect与RpcServer.toHttpEffectWebsocket三个 HTTP 入口的选项类型使其与底层运行时早已具备的缺陷隔离能力对齐。读完本文将掌握致命缺陷fatal defect在 RPC 服务端如何传播、默认行为与隔离行为的分界、三个 API 的启用方式以及从源码与测试中可以验证的完整行为语义。变更背景一个把「运行时能力」补进「类型声明」的 patchchangeset 的原始信息该变更以 Changesets 的 pre-release 形式记录在 calm-panthers-nail.md原文如下--- effect: patch --- Add disableFatalDefects to RpcServer.layerHttp, RpcServer.toHttpEffect, and RpcServer.toHttpEffectWebsocket option types to match existing runtime support.可以提炼出三点核心信息变更性质是patch纯类型层面的增补不改变既有行为与二进制兼容性。目标 API 是三个 HTTP 相关的构造入口layerHttpLayer 形式、toHttpEffectHTTP 非 WebSocket 协议、toHttpEffectWebsocketWebSocket 升级协议。动机是「对齐」而非「新增能力」disableFatalDefects在底层运行时中早已生效这次只是把该选项暴露到 HTTP 入口的类型签名里让开发者可以在类型层面正确使用它。选项的运行时语义在 RpcServer.ts 的makeNoSerialization构造器中选项的读取逻辑如下const disableFatalDefects options.disableFatalDefects ?? false即未显式传入时默认值为false。这意味着默认情况下服务端仍走「致命缺陷全局处理」路径只有在开发者显式开启后才会切换为隔离模式。默认行为致命缺陷如何影响整条连接判定条件与处理分支在处理每个请求的onExit回调中RpcServer.ts存在一个关键分支} else if ( !disableFatalDefects Cause.hasDies(exit.cause) !Cause.hasInterrupts(exit.cause) ) { write sendDefect(client, Cause.squash(exit.cause)) } else { write options.onFromServer({ _tag: Exit, clientId: client.id, requestId: request.id, exit: exit as any }) }分支条件是三个条件同时成立!disableFatalDefects选项未开启默认情况Cause.hasDies(exit.cause)该请求的失败原因是Die致命缺陷例如 handler 内部抛出异常或显式调用Effect.die!Cause.hasInterrupts(exit.cause)该请求不是被中断导致的失败中断走Exit的正常失败路径。当三者同时成立服务端走sendDefect路径把缺陷折叠Cause.squash成顶层缺陷打包成Defect消息发回客户端。从 RpcMessage.ts 可以看到这种协议级消息的结构export interface ResponseDefectEncoded { readonly _tag: Defect readonly defect: unknown }这种Defect消息是连接级的终态信号它声明「该客户端连接上的通信已经失效」因此客户端收到后会认为这条连接不可复用。在sendDefect的实现里RpcServer.ts当客户端已结束且没有在途请求时发送完Defect还会顺带执行endClient收尾该连接。为什么默认如此激进这种设计的出发点是一旦某个 handler 发生了未捕获的致命缺陷说明服务端代码处于不可信状态继续复用同一条连接可能带来不确定的后续行为。因此默认策略选择「牺牲该连接确保缺陷信息无损地送达客户端」。这正是disableFatalDefects需要存在的理由——它给「连接复用」与「缺陷隔离」之间提供了一个显式的取舍开关。开启隔离disableFatalDefects: true的切换语义当选项被置为true时上文的else if分支整体被跳过任何失败——包括致命缺陷——都会走正常的Exit失败路径返回客户端该请求自身的失败被作为普通 RPC 失败Failureexit完整返回调用方可以通过Effect.exit观察到缺陷内容同一条连接上的其他请求不受影响包括正在持续推送数据的流式 RPC连接继续存活后续请求仍可正常处理。换句话说disableFatalDefects: true把「致命缺陷」从连接级事故降级为请求级失败这正是该选项名称的字面含义禁用「缺陷致命化」处理。从源码确认三处入口的选项透传三个入口在接收选项后都直接透传给底层make/layer运行时类型签名分别是layerHttpRpcServer.tsreadonly disableFatalDefects?: boolean | undefinedtoHttpEffectRpcServer.tsreadonly disableFatalDefects?: boolean | undefinedtoHttpEffectWebsocketRpcServer.tsreadonly disableFatalDefects?: boolean | undefinedlayerHttp的实现直接复用layer(group, options)RpcServer.ts而layer的选项类型同样包含该字段两个toHttpEffect*则通过make(group, options)RpcServer.ts将选项传入同一套运行时。因此本 patch 的本质是补齐了类型层面的覆盖使三个 HTTP 入口与make、makeNoSerialization、layer、layerNoSerialization等底层入口保持一致。三个 API 的启用方式与适用场景方式一RpcServer.layerHttpLayer 集成默认 WebSocketimport { RpcServer } from effect/unstable/rpc/RpcServer import * as HttpRouter from effect/unstable/http/HttpRouter const RpcLayer RpcServer.layerHttp({ group: MyGroup, path: /rpc, protocol: websocket, // 默认值传 http 可切换为非 WebSocket 协议 disableFatalDefects: true }) const App HttpRouter.empty.pipe( HttpRouter.mount(RpcLayer) )该入口适合将 RPC 端点直接挂载进既有HttpRouter的应用。disableFatalDefects与disableTracing、spanPrefix、spanAttributes、concurrency、streamBufferSize等选项并列详见 RpcServer.ts按需开启即可。方式二RpcServer.toHttpEffect手写 HTTP 非 WebSocket 协议const rpcHandler yield* RpcServer.toHttpEffect(MyGroup, { disableFatalDefects: true }) const app HttpRouter.empty.pipe( HttpRouter.get(/rpc, rpcHandler) )该入口启动服务端并返回「请求 → 响应」的 EffectRpcServer.ts适合需要自己拼装路由、或与其它中间件组合的场景。方式三RpcServer.toHttpEffectWebsocketWebSocket 升级协议const wsHandler yield* RpcServer.toHttpEffectWebsocket(MyGroup, { disableFatalDefects: true })该入口与方式二几乎一致但将请求升级为 WebSocket 连接来承载 RPC 协议RpcServer.ts适合需要长期复用连接、双工通信的场景。注意此时「连接级缺陷」往往更值得警惕因为 WebSocket 连接的重建成本更高是否开启隔离需要结合客户端重连策略综合判断。类型层面的验证typetest 目录下的 RpcServer.tst.ts 为本变更提供了类型级回归测试三个用例分别验证了三个入口对disableFatalDefects: true的接受RpcServer.layerHttp({ group: Group, path: /rpc, disableFatalDefects: true }) void RpcServer.toHttpEffect(Group, { disableFatalDefects: true }) void RpcServer.toHttpEffectWebsocket(Group, { disableFatalDefects: true })若选项类型缺失tstyche 类型测试会在编译期直接报错这正是本 patch 要保障的契约。行为验证测试用例揭示的真实语义测试脚手架assertTickerSurvivesplatform/node 的 RpcServer.test.ts 提供了一个精巧的隔离性验证器服务端同时提供Ticker一个每 60 毫秒推送一个数字的流式 RPCBoom一个立即Effect.die(boom)的缺陷 handler。测试先订阅Ticker流并等待至少两个数据点到达然后调用Boom触发致命缺陷再等待 300 毫秒最后断言tickerStatus为undefined——Ticker流对应的 Fiber 仍然存活ticksAfter ticksBefore——缺陷发生后流仍在持续产生数据。Effect.die产生的正是Cause.hasDies为真的致命缺陷因此这套脚手架精确覆盖了disableFatalDefects的判定分支。核心用例缺陷只影响自身请求const DefectServer RpcServer.layer(group, { disableFatalDefects: true }).pipe( Layer.provide(Handlers), Layer.provideMerge(RpcServer.layerProtocolSocketServer), Layer.provideMerge(NodeSocketServer.layer({ port: 0 })), Layer.provide(RpcSerialization.layerNdjson) ) it.live( with disableFatalDefects a handler defect fails only its own request, not other in-flight streams, ... )用例名把语义说得很直白「开启disableFatalDefects时handler 缺陷只使自身请求失败不影响同连接上的在途流」。同时它还有一个关键断言RpcServer.test.tsconst boomExit yield* client.Boom().pipe(Effect.exit) if (!Exit.isFailure(boomExit)) { return assert.fail(Boom call must fail with the handler defect) } assert.include( String(Cause.squash(boomExit.cause)), boom, the caller must receive the handler defect )即Boom调用本身必须以失败结束且调用方拿到的缺陷内容正是boom——隔离不等于吞掉错误缺陷信息依然被完整回传只是不再殃及连接上的其它请求。这一点对生产排障至关重要开启隔离后你仍然能在客户端侧精确捕获到缺陷。作为对照同一测试文件中的unknown-tag isolation用例RpcServer.test.ts验证了「未知请求 tag」同样具备请求级隔离说明请求级故障隔离是框架层的通用策略disableFatalDefects只是把「致命缺陷」也纳入了这个策略。测试依赖的传输栈用例通过RpcServer.layerProtocolSocketServerNodeSocketServer.layer({ port: 0 })RpcSerialization.layerNdjson搭起真实的 Node socket 传输RpcServer.test.ts。这印证了一点disableFatalDefects的语义与具体传输协议无关它作用于 RPC 服务器核心make层HTTP、WebSocket、Socket、Stdio 等所有协议栈共用同一套缺陷判定逻辑。因此即使本 patch 只把选项暴露在三个 HTTP 入口上你同样可以在layer、layerNoSerialization、make等底层入口使用它。选项矩阵与选型建议API 入口选项位置适用形态备注RpcServer.layerHttpoptions对象挂载进HttpRouter的 Layerprotocol可选http/websocket默认 WebSocketRpcServer.toHttpEffectoptions?可选对象手写 HTTP 非 WebSocket 协议返回请求级 EffectRpcServer.toHttpEffectWebsocketoptions?可选对象WebSocket 升级协议返回请求级 EffectRpcServer.layer/make等底层入口同样支持任意协议运行时早已支持选型建议默认保持关闭false如果客户端侧对「连接死亡」有成熟的检测与重连机制缺陷即连接终结的默认行为更简单可预测。开启隔离true当单条连接承载多个独立请求尤其是长生命周期流且你希望单个 handler 的崩溃不影响其它在途任务时开启该选项并配合客户端侧Effect.exit捕获缺陷。无论哪种选择缺陷内容都不会丢失默认模式下通过连接级Defect消息送达隔离模式下通过请求级Failureexit 送达。变更影响与兼容性从 CHANGELOG.md 可以看到该变更随 PR #1613 合入作者 lucas-barake以patch级别发布。对使用者而言编译期升级后即可在三个 HTTP 入口的选项对象中使用disableFatalDefects无需任何类型断言运行期行为无任何变化未传该选项的应用与升级前完全一致可回退选项独立、默认关闭逐服务灰度开启即可无需改动协议或客户端。小结disableFatalDefects是 Effect RPC 服务端「缺陷隔离」能力在类型系统层面的补齐底层make运行时早已支持该判定本变更让layerHttp、toHttpEffect、toHttpEffectWebsocket三个 HTTP 入口的选项类型与运行时对齐。理解它的关键在于分清两个层次——默认模式下handler 的致命缺陷会把整条连接标记为不可用缺陷以连接级Defect消息回传开启后缺陷降级为请求级失败仅影响自身请求同连接的在途流不受干扰缺陷仍以Failureexit 完整回传。测试用例fatal-defect isolationRpcServer.test.ts与类型用例RpcServer.tst.ts分别从行为与类型两个维度锁定了这一契约是理解与回归验证该特性的最佳入口。【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表