ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Erlang/OTP .appup 文件实战指南:运行时应用升级与降级指令详解

Erlang/OTP .appup 文件实战指南:运行时应用升级与降级指令详解 编程语言语言运行时标准库编译器并发编程【免费下载链接】otpErlang/OTP项目地址https://gitcode.com/gh_mirrors/ot/otp点击查看免费下载导读在 Erlang/OTP 中SASL 应用提供的 release handling 框架允许系统在运行时runtime对应用进行升级与降级其核心是每个应用目录下的.appup文件——它描述从旧版本升级到新版本、以及从新版本降级到旧版本需要执行哪些指令。本文以官方文档 Appup Cookbook 为骨架结合仓库中的 release handling 文档 与 SASL 源码systools、release_handler逐一讲解功能模块替换、内部状态变更、supervisor 属性与子进程规格变更、特殊进程、被包含应用included application以及非 Erlang 代码升级等典型场景并给出可直接复制使用的.appup文件示例。读完本文你将能够为自己的应用编写正确的.appup文件、理解relup的生成原理并掌握release_handler在目标系统上的安装流程。.appup文件的基础结构在深入各种场景之前先明确.appupapplication upgrade file的语法。它定义如何在新版本与旧版本之间升级/降级文件名为Application.appup其中Application是应用名通常位于应用的ebin目录{Vsn, [{UpFromVsn1, InstructionsU1}, ..., {UpFromVsnK, InstructionsUK}], [{DownToVsn1, InstructionsD1}, ..., {DownToVsnK, InstructionsDK}]}.Vsn字符串是当前版本与.app文件中的{vsn, ...}一致每个UpFromVsn是要从其升级的旧版本每个DownToVsn是要降级到的旧版本每个Instructions是一组 release handling 指令instructionUpFromVsn与DownToVsn也支持正则表达式例如 sasl.appup.src 中就用{^4\\.3\\.1(?:\\.[0-9])*$,[restart_new_emulator]}匹配 OTP 27/28/29 一系列历史版本。指令分为两类高层指令high-level由systools:make_relup/3,4翻译为低层指令low-level后者才是release_handler直接理解的指令集合详见 release_handling.md。.appup文件中书写的是高层指令而生成的relup文件全部是低层指令。systools:make_relup/3,4的实现位于 lib/sasl/src/systools.erl#L343-L435它会读取新老版本的.rel、.app与.appup文件推导出需要新增、删除、升级或降级的应用然后把所有指令合并为一条有序的低层指令列表。先记住两个关键概念驻留模块residence module进程尾递归循环函数所在的模块。对于按 OTP behaviour 实现的进程behaviour 模块如gen_server就是驻留模块回调模块则属于功能模块。功能模块functional module不是任何进程驻留模块的模块。更换功能模块简单的代码替换当一个功能模块被修改例如新增函数或修复 bug时简单代码替换simple code replacement就够了即把新版本模块加载进系统并移除旧版本。.appup文件只需一条load_module指令{2, [{1, [{load_module, m}]}], [{1, [{load_module, m}]}] }.升级与降级的指令相同因为降级时加载旧版本同样只是简单的模块加载。更换回调模块也是简单的代码替换回调模块callback module本质上是功能模块因此扩展代码同样用简单代码替换。文档中的经典例子是 Release Handling 里给ch3增加available/0函数新版本ch_app从1升级到2只需加载新版本的ch3回调模块{2, [{1, [{load_module, ch3}]}], [{1, [{load_module, ch3}]}] }.注意load_module只加载模块不会让已经运行中的 behaviour 进程切换到新代码进程只有在下次函数调用时才会自然进入新版本代码。如果新版本代码改变了进程内部状态格式就必须使用下一节讲的同步代码替换。变更内部状态同步代码替换与 code_change/3如果新版本改变了 behaviour 进程的内部状态格式简单代码替换就不够了。进程必须在切换到新代码之前通过回调函数code_change/3显式完成状态转换这称为同步代码替换synchronized code replacement对应指令{update, Module, {advanced, Extra}}它让 release handler 依次完成挂起suspend使用该模块的进程 → 让进程调用code_change/3转换状态并切换到新代码 → 移除旧版本代码 → 恢复resume进程。这一过程在 release_handling.md 中有详细描述其底层调用链是sys:suspend/1,2、sys:change_code/4,5与sys:resume/1,2。示例给 ch3 增加计数器考虑 gen_server Behaviour 中的ch3模块其内部状态Chs表示可用频道集合。假设要为alloc请求增加计数器N状态格式要变为{Chs,N}{2, [{1, [{update, ch3, {advanced, []}}]}], [{1, [{update, ch3, {advanced, []}}]}] }.update指令的第三个元素{advanced,Extra}表示受影响的进程在加载新模块版本之前要做状态转换——即调用回调函数code_change/3。Extra此处为[]会被原样传给该函数-module(ch3). ... -export([code_change/3]). ... code_change({down, _Vsn}, {Chs, N}, _Extra) - {ok, Chs}; code_change(_Vsn, Chs, _Extra) - {ok, {Chs, 0}}.第一个参数在降级时是{down,Vsn}在升级时是VsnVsn取自原始版本即你正在升级的旧版本或正在降级到的新版本的模块。版本由模块属性vsn决定ch3没有该属性因此版本就是 beam 文件的校验和一个大整数实践中通常不关心这个值。其余回调函数也需要相应修改比如新增接口函数这里不再展开。模块依赖DepMods假设m1模块调用新加的ch3:available/0。如果升级时先加载新版本m1而新版本ch3尚未加载m1调用ch3:available/0就会在运行时出错。因此ch3必须先于m1加载升级方向降级方向则相反。这就是模块依赖通过指令中的DepMods元素表达{load_module, Module, DepMods} {update, Module, {advanced, Extra}, DepMods}DepMods是Module所依赖的模块列表。例如应用myapp的m1依赖ch3myapp.appup: {2, [{1, [{load_module, m1, [ch3]}]}], [{1, [{load_module, m1, [ch3]}]}] }. ch_app.appup: {2, [{1, [{load_module, ch3}]}], [{1, [{load_module, ch3}]}] }.如果m1和ch3属于同一个应用可以写在一个.appup里{2, [{1, [{load_module, ch3}, {load_module, m1, [ch3]}]}], [{1, [{load_module, ch3}, {load_module, m1, [ch3]}]}] }.m1在降级时也依赖ch3。systools懂得升级与降级的方向差异会自动生成正确的relup升级时先加载ch3再加载m1降级时先加载m1再加载ch3。更换特殊进程Special Process的代码特殊进程是使用proc_lib和sys手动实现、不完全遵循 behaviour 的进程。当特殊进程的驻留模块换新版本时进程必须对循环函数做一次全限定调用fully qualified call才能切入新代码因此必须使用同步代码替换{update, ch4, {advanced, []}}注意特殊进程的驻留模块名必须列在子进程规格child specification的Modules部分否则 release handler 无法找到该进程。以文档 sys and proc_lib 中的ch4为例由 supervisor 启动时的子进程规格{ch4, {ch4, start_link, []}, permanent, brutal_kill, worker, [ch4]}ch4属于应用sp_app升级1→2时sp_app.appup可写为{2, [{1, [{update, ch4, {advanced, []}}]}], [{1, [{update, ch4, {advanced, []}}]}] }.update指令必须包含{advanced,Extra}它让特殊进程调用用户实现的system_code_change/4回调函数Extra原样传入-module(ch4). ... -export([system_code_change/4]). ... system_code_change(Chs, _Module, _OldVsn, _Extra) - {ok, Chs}.各参数含义第一个参数是内部状态State来自特殊进程收到系统消息时调用的sys:handle_system_msg/6在ch4中即可用频道集合Chs。第二个参数是模块名ch4。第三个参数是Vsn或{down,Vsn}规则与前面code_change/3相同。如果代码只是扩展返回原状态即可如果内部状态格式也变了类似变更内部状态一节的例子就在这个函数里完成转换并返回{ok,Chs2}。更换 Supervisor属性与子进程规格supervisor behaviour 支持修改内部状态即修改重启策略、最大重启频率以及现有子进程规格。子进程可以新增或删除但不会自动处理必须在.appup中显式给出指令。修改属性Properties修改 supervisor 属性属于内部状态变更需要同步代码替换但必须使用专用的update指令形式{update, Module, supervisor}其执行逻辑是先加载新版本回调模块升级、降级都要加载然后检查新的init/1返回值并据此变更内部状态。示例把ch_sup来自 Supervisor Behaviour的重启策略从one_for_one改为one_for_all先修改ch_sup.erl中的init/1-module(ch_sup). ... init(_Args) - {ok, {#{strategy one_for_all, ...}, ...}}.ch_app.appup{2, [{1, [{update, ch_sup, supervisor}]}], [{1, [{update, ch_sup, supervisor}]}] }.修改子进程规格Child Specifications修改现有子进程规格时指令与.appup文件与修改属性完全相同{2, [{1, [{update, ch_sup, supervisor}]}], [{1, [{update, ch_sup, supervisor}]}] }.需要注意修改不影响已经存在的子进程。例如修改 start 函数只决定该子进程以后如果需要重启时如何启动。子进程规格的id不能被修改。修改Modules字段会影响 release handling 过程本身因为 release handler 正是靠Modules字段识别哪些进程参与同步代码替换在 release_handling.md 中进程使用某模块即表示该模块名列在其子进程规格的Modules中若Modulesdynamic如事件管理器则由gen_event进程向 release handler 上报当前安装的事件处理器列表。新增与删除子进程新子进程规格会被自动添加但不会自动删除子进程也不会被自动启动或终止必须借助apply指令显式调用supervisor的函数。示例升级ch_app从1到2时给ch_sup新增子进程m1降级时删除它{2, [{1, [{update, ch_sup, supervisor}, {apply, {supervisor, restart_child, [ch_sup, m1]}} ]}], [{1, [{apply, {supervisor, terminate_child, [ch_sup, m1]}}, {apply, {supervisor, delete_child, [ch_sup, m1]}}, {update, ch_sup, supervisor} ]}] }.指令的顺序很重要升级时先更新 supervisor 的子进程规格再启动新子进程降级时先终止子进程、删除规格再更新 supervisor。apply指令的形式是{apply, {M, F, A}}由 release handler 直接求值apply(M, F, A)见 release_handling.md。这里要求 supervisor 已注册为ch_sup如果 supervisor 未注册脚本无法直接访问它就必须写一个辅助函数先找到 supervisor 的 pid 再调用supervisor:restart_child等并通过apply调用这个辅助函数。如果模块m1是在版本2中才引入的升级时还要加载、降级时还要删除{2, [{1, [{add_module, m1}, {update, ch_sup, supervisor}, {apply, {supervisor, restart_child, [ch_sup, m1]}} ]}], [{1, [{apply, {supervisor, terminate_child, [ch_sup, m1]}}, {apply, {supervisor, delete_child, [ch_sup, m1]}}, {update, ch_sup, supervisor}, {delete_module, m1} ]}] }.升级方向先add_module加载m1、更新子进程规格再启动子进程降级方向先终止并删除子进程再更新规格、最后delete_module卸载模块。delete_module会杀死任何以该模块为驻留模块的进程所以删除前必须确保这类进程已全部终止见 release_handling.md。新增或删除模块新增一个功能模块m到ch_app{2, [{1, [{add_module, m}]}], [{1, [{delete_module, m}]}] }.add_module加载模块。在嵌入式模式embedded mode下必须显式加载交互模式interactive mode下代码服务器会自动查找并加载未加载的模块所以并非严格必需。delete_module卸载模块。启动或终止进程在按 OTP 设计原则组织的系统中任何进程都是某个 supervisor 的子进程所以启动/终止进程本质上就是上一节新增与删除子进程的操作直接复用那里的指令即可。新增或移除应用新增或移除一个**主应用primary application**时不需要.appup文件。生成relup时systools会对比新旧.rel文件自动加入add_application与remove_application指令add_application先按.app文件中的modules键用多条add_module加载模块然后启动应用remove_application停止应用、用多条delete_module卸载模块、再从 application controller 卸载应用规格。重启一个应用当变更过于复杂、无法不重启进程就完成时例如 supervisor 层级被重构restart_application指令非常有用它等价于依次执行移除应用 添加应用见 release_handling.md。示例上一节新增子进程m1的场景也可以不更新 supervisor而是直接重启整个应用{2, [{1, [{restart_application, ch_app}]}], [{1, [{restart_application, ch_app}]}] }.变更应用规格与应用配置安装 release 时应用规格会自动更新发生在relup脚本求值之前因此.appup中不需要任何指令{2, [{1, []}], [{1, []}] }.更新.app文件中的env键从而修改应用配置属于变更应用规格的一种。除此之外也可以在sys.config中新增或更新应用配置参数。值得补充的是应用配置的更新优先级见 release_handling.md安装新 release 后应用配置参数按以下优先级递增的顺序自动更新启动脚本boot script中的数据来自新版本的App.app新的sys.config命令行参数-App Par Val。这意味着其他系统配置文件里设置的值以及通过application:set_env/3设置的值会被忽略。安装完成后application controller 会比较新旧配置并调用可选回调Module:config_change(Changed, New, Removed)其中Changed/New是变更/新增的{Par,Val}列表Removed是被移除的参数列表。变更被包含应用Included Applications关于新增、移除、重启应用的指令只适用于主应用没有针对被包含应用included application的对应指令。但由于被包含应用本质上是一棵以顶层 supervisor 为根的监督树、作为包含应用某 supervisor 的子进程启动因此可以手工创建relup来实现。示例release 中有应用prim_app监督树里有 supervisorprim_sup。新版本要把ch_app包含进prim_app即把ch_app的顶层 supervisorch_sup作为prim_sup的子进程启动。Step 1)修改prim_sup的代码init(...) - {ok, {...supervisor flags..., [..., {ch_sup, {ch_sup,start_link,[]}, permanent,infinity,supervisor,[ch_sup]}, ...]}}.Step 2)修改prim_app的.app文件{application, prim_app, [..., {vsn, 2}, ..., {included_applications, [ch_app]}, ... ]}.Step 3)创建新的.rel文件把ch_app加进去{release, ..., [..., {prim_app, 2}, {ch_app, 1}]}.被包含应用可以用两种方式启动。方式一应用重启Application RestartStep 4a)一种方式是重启整个prim_app通常使用prim_app.appup中的restart_application指令。但如果这样做了再生成relup它不但包含重启移除并添加prim_app的指令还会因为新.rel里有ch_app而自动加入启动/停止ch_app的指令——这正是我们不想要的被包含应用应随包含应用启动。正确做法是手工创建relup从头写或在生成版本上修改把启动/停止ch_app的指令替换为加载/卸载应用的指令{B, [{A, [], [{load_object_code,{ch_app,1,[ch_sup,ch3]}}, {load_object_code,{prim_app,2,[prim_app,prim_sup]}}, point_of_no_return, {apply,{application,stop,[prim_app]}}, {remove,{prim_app,brutal_purge,brutal_purge}}, {remove,{prim_sup,brutal_purge,brutal_purge}}, {purge,[prim_app,prim_sup]}, {load,{prim_app,brutal_purge,brutal_purge}}, {load,{prim_sup,brutal_purge,brutal_purge}}, {load,{ch_sup,brutal_purge,brutal_purge}}, {load,{ch3,brutal_purge,brutal_purge}}, {apply,{application,load,[ch_app]}}, {apply,{application,start,[prim_app,permanent]}}]}], [{A, [], [{load_object_code,{prim_app,1,[prim_app,prim_sup]}}, point_of_no_return, {apply,{application,stop,[prim_app]}}, {apply,{application,unload,[ch_app]}}, {remove,{ch_sup,brutal_purge,brutal_purge}}, {remove,{ch3,brutal_purge,brutal_purge}}, {purge,[ch_sup,ch3]}, {remove,{prim_app,brutal_purge,brutal_purge}}, {remove,{prim_sup,brutal_purge,brutal_purge}}, {purge,[prim_app,prim_sup]}, {load,{prim_app,brutal_purge,brutal_purge}}, {load,{prim_sup,brutal_purge,brutal_purge}}, {apply,{application,start,[prim_app,permanent]}}]}] }.这段脚本展示了低层指令的组合load_object_code把模块代码读入系统但不立即切换、point_of_no_return提交点其前的指令失败可回滚、其后失败则不可回滚见 release_handler.erl、remove/load带brutal_purge的卸载/加载、purge、apply调用application:stop/1、application:load/1、application:unload/1、application:start/2等。方式二Supervisor 变更Supervisor ChangeStep 4b)另一种方式是对prim_sup做新增/删除子进程的指令同时配合加载/卸载ch_app的全部代码及其应用规格。同样手工创建relup升级时先加载ch_app全部代码与应用规格再更新prim_sup降级时先更新prim_sup再卸载ch_app的代码与应用规格。{B, [{A, [], [{load_object_code,{ch_app,1,[ch_sup,ch3]}}, {load_object_code,{prim_app,2,[prim_sup]}}, point_of_no_return, {load,{ch_sup,brutal_purge,brutal_purge}}, {load,{ch3,brutal_purge,brutal_purge}}, {apply,{application,load,[ch_app]}}, {suspend,[prim_sup]}, {load,{prim_sup,brutal_purge,brutal_purge}}, {code_change,up,[{prim_sup,[]}]}, {resume,[prim_sup]}, {apply,{supervisor,restart_child,[prim_sup,ch_sup]}}]}], [{A, [], [{load_object_code,{prim_app,1,[prim_sup]}}, point_of_no_return, {apply,{supervisor,terminate_child,[prim_sup,ch_sup]}}, {apply,{supervisor,delete_child,[prim_sup,ch_sup]}}, {suspend,[prim_sup]}, {load,{prim_sup,brutal_purge,brutal_purge}}, {code_change,down,[{prim_sup,[]}]}, {resume,[prim_sup]}, {remove,{ch_sup,brutal_purge,brutal_purge}}, {remove,{ch3,brutal_purge,brutal_purge}}, {purge,[ch_sup,ch3]}, {apply,{application,unload,[ch_app]}}]}] }.注意升级方向的顺序先load_object_code加载ch_app与prim_sup新代码 →point_of_no_return→ 加载ch_sup、ch3→application:load(ch_app)→suspend(prim_sup)→ 加载新prim_sup→code_change,up→resume(prim_sup)→supervisor:restart_child(prim_sup, ch_sup)启动被包含应用。降级方向则完全镜像先终止/删除子进程再更新prim_sup最后卸载ch_app代码并application:unload(ch_app)。变更非 Erlang 代码如 Port 程序OTP 对非 Erlang 语言编写的程序例如 port 程序的代码升级没有专门支持属于应用相关的问题。典型做法是让控制 port 的 Erlang 进程在code_change/3中关闭旧 port 并打开新 port。示例假设控制 port 的进程是gen_serverportcport 在init/1中打开init(...) - ..., PortPrg filename:join(code:priv_dir(App), portc), Port open_port({spawn,PortPrg}, [...]), ..., {ok, #state{portPort, ...}}.为portc增加code_change/3关闭旧 port、打开新 port必要时可先从旧 port 请求需保存的数据再传给新 portcode_change(_OldVsn, State, port) - State#state.port ! close, receive {Port,close} - true end, PortPrg filename:join(code:priv_dir(App), portc), Port open_port({spawn,PortPrg}, [...]), {ok, #state{portPort, ...}}.更新.app中的应用版本号并编写.appup文件[2, [{1, [{update, portc, {advanced,port}}]}], [{1, [{update, portc, {advanced,port}}]}] ].{advanced,port}中的port会作为Extra传给code_change/3。还要确保 C 程序所在的priv目录被打进新 release 包1 systools:make_tar(my_release, [{dirs,[priv]}]). ...运行时系统重启与升级两条升级指令会重启运行时系统restart_new_emulator用于 ERTS、Kernel、STDLIB 或 SASL 升级。由systools:make_relup/3,4生成relup时自动加入且必须是relup的第一条指令。遇到该指令时release handler 先生成临时启动脚本新版本运行时系统与核心应用、旧版本的其他应用调用init:reboot/0优雅终止所有进程由heart程序按临时脚本重启重启后继续执行relup剩余指令。它要求系统以心跳监控heartbeat方式启动即嵌入式系统 heart见 release_handling.md。在 release_handler.erl 中可以看到如果脚本里遇到restart_new_emulatorupgrade_app/2会返回{error,restart_new_emulator}因为该指令需要启动新版本模拟器。警告该机制会让新版本运行时系统与核心应用在启动时与旧版本的其他应用共存必须格外小心兼容性。核心应用如确有破坏性变更通常会先在两个大版本中走弃用deprecation流程同时要尽快移除对已弃用函数的调用避免应用被不兼容变更弄崩。restart_emulator与 ERTS/核心应用升级无关用于在任何应用需要时、在所有其他升级指令执行完之后强制重启运行时系统。relup中只能有一条且必须放在末尾systools:make_relup生成时自动满足。遇到它时 release handler 调用init:reboot/0优雅关闭系统由heart用新release 版本重启重启后不再执行任何升级指令。如果只需要重启运行时系统、不需要任何升级指令即重启本身足以让新版本应用生效可以手工写一个极简的relup{B, [{A, [], [restart_emulator]}], [{A, [], [restart_emulator]}] }.这种情况下可以只使用 release handler 框架的自动打包/解包、自动路径更新等能力而完全不需要编写.appup文件。从.appup到relup再到在线安装完整工作流把上面的知识串起来一个完整的升级周期是详见 release_handling.md按 Releases 创建 release安装到目标环境嵌入式系统配heart。在开发环境修改代码更新相关.app文件版本号并编写新.rel文件。为每个被修改的应用编写.appup文件本文全部场景。用systools:make_relup/3,4基于新旧.rel、.app、.appup生成relup1 systools:make_relup(ch_rel-2, [ch_rel-1], [ch_rel-1]). ok如果新旧版本的.app/.rel文件不在同一代码路径用path选项扩展1 systools:make_relup(ch_rel-2, [ch_rel-1], [ch_rel-1], [{path,[../ch_rel-1, ../ch_rel-1/lib/ch_app-1/ebin]}]). ok生成启动脚本与 release 包记得包含relup、sys.config必要时用{dirs,[priv]}包含 priv 目录拷到目标机$ROOT/releases1 systools:make_script(ch_rel-2). ok 2 systools:make_tar(ch_rel-2). ok在运行中的目标系统里解包并安装1 release_handler:unpack_release(ch_rel-2). {ok,B} 2 release_handler:install_release(B). {ok,A,[]}安装会逐条执行relup中的指令失败则用旧版本重启成功则新版本成为当前版本但尚未成为默认版本。 7. 让新版本成为默认版本permanent否则系统重启后仍会回到旧版本3 release_handler:make_permanent(B). ok系统在$ROOT/releases/RELEASES与$ROOT/releases/start_erl.data中记录各版本的 old/permanent 状态。release_handler:remove_release(Vsn)可移除已安装但未 permanent 的 release。常见注意事项小结指令顺序至关重要新增模块要先加载再使用删除模块前要先终止相关进程升级与降级方向完全相反。简单 vs 同步代码替换只加函数用load_module改内部状态/驻留模块用update{advanced,Extra}behaviour 进程回调code_change/3特殊进程回调system_code_change/4改 supervisor 用updatesupervisor。DepMods表达模块依赖防止升级瞬间新旧代码混用导致的undef运行时错误systools会根据升级/降级方向自动排好加载顺序。被包含应用没有专属指令要么重启包含应用要么手工编写relup组合加载/卸载与 supervisor 子进程操作。relup只需低层指令手工编写时只使用load_object_code、point_of_no_return、suspend/resume、code_change、remove/load/purge、apply、restart_new_emulator/restart_emulator等低层指令。保持向后兼容、小步改动release handling 期间未受影响的进程照常运行新进程可能短暂执行旧代码release_handling.md因此代码改动尽量小且向后兼容。以上所有指令的完整清单与语法可查阅 SASL 应用手册中的appup、relup页面本仓库对应源码在 lib/sasl/src 的systools*.erl与release_handler*.erl而.appup的官方示例合集正是本文依据的 appup_cookbook.md。赞分享编程语言语言运行时标准库编译器并发编程【免费下载链接】otpErlang/OTP项目地址https://gitcode.com/gh_mirrors/ot/otp点击查看免费下载相关推荐Erlang/OTP 应用升级文件 appup 完全指南语法、指令详解与实战示例Erlang/OTP 应用升级文件 appup 完全指南语法、指令详解与实战示例 导读 本文深入讲解 Erlang/OTP 中 应用升级文件Applicat编程语言语言运行时标准库编译器并发编程Erlang/OTP relup 文件完全指南运行时热升级指令的生成、语法与执行原理Erlang/OTP relup 文件完全指南运行时热升级指令的生成、语法与执行原理 导读 relup Release Upgrade File发布升级文编程语言语言运行时标准库编译器并发编程Erlang/OTP Release Handling 实战指南基于 SASL 的运行时升级与回滚Erlang/OTP Release Handling 实战指南基于 SASL 的运行时升级与回滚 Erlang 语言最突出的能力之一是在运行时替换模块代码编程语言语言运行时标准库编译器并发编程上一篇Hoppscotch 开源 API 开发工具零基础 3 步跑起来的完整指南下一篇react-use 中 useRafState基于 requestAnimationFrame 的高性能状态 Hook 实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表