第 27 章 / 共 51 章
卷四 · 热重载深潜
触发与查询:把重载暴露给该用的人
XServer 不规定"谁在什么时候重载"——它只提供 Submit/Query。本章设计触发面:运维用的管理路由、开发用的文件监听、以及结果的呈现。
本章导读
站位:上一章的模型落地成"产品"。管理路由是绝大多数部署的选择(ch40 会把它长成插件系统);文件监听给你 IDE 保存即生效的开发体验。
27.1 引入
触发设计要回答三个问题:谁能触发(权限)、以什么粒度触发(host / server / all)、怎么知道结果。有意思的是,XServer 把这三个问题全留给应用——因为"管理面属于应用层"(第 1 章的坑)。这章给出两个经过验证的答案。
27.2 管理路由模式
/admin/reload 管理路由(完整模式)
static void Handle_Admin_Reload(XS_HttpReq* pReq, const RouteParamHTTP* a, uint32 n)
{
xvalue* pOut = xrtValueObject();
XS_ReloadId iId;
/* 1. 鉴权:管理面必须挡在信任边界内
* (示例用固定令牌;生产可接任意鉴权体系)*/
if ( !CheckAdminToken(pReq) ) {
(void)ReplyText(pReq, 403, "forbidden");
goto Out;
}
/* 2. 粒度选择:scope=host|server|all */
if ( ScopeIsAll(pReq) ) iId = xsReloadAllSubmit();
else if ( ScopeIsServer(pReq) ) iId = xsReloadServerSubmit(sName);
else iId = xsReloadHostSubmit(pReq->host);
/* 3. 立即回应受理(不等待终态)*/
if ( iId == 0 ) {
(void)ReplyText(pReq, 503, "reload queue full");
goto Out;
}
/* out: {"id":123} —— 结果走 /admin/reload/status?id=123 */
Out:
xrtValueRelease(pOut);
}
刚才发生了什么——设计决策:
- 受理与结果分离:POST 立刻回 ID,结果查询独立成 GET 路由——不占用请求线程等终态(Submit 本来就不阻塞,等待更不该)
- 粒度三档:日常用 host(最小爆炸半径);结构变更用 server;配置全局改动用 all(整批事务语义)
- 鉴权放在应用层第一行:管理路由是"能改服务行为的路由",与业务路由同权不同险
27.3 自动触发:保存即重载
开发体验版触发器——监听 devfile 目录,变化即提交:
文件监听触发器(模式)
/* ServiceInit:启动监听 + 周期检查(xrt 文件监听 / 轮询 mtime 均可)*/
static void OnSrcChanged(void* pUserData)
{
XS_HostInfo* pHost = (XS_HostInfo*)pUserData;
/* 防抖:保存风暴只提交最后一次(latest-wins 兜底)*/
(void)xsReloadHostSubmit(pHost);
}
/* 注意:定时器随代作废——新代 Init 里重新挂监听 */
防抖有两层:应用层合并短窗口内的连续变化;就算漏了,latest-wins 也会把过期意图挤成 SUPERSEDED——这个触发器最坏情况是多编译几次,不会上线中间态。生产环境通常不开自动触发:重载是受控变更,不是文件系统事件。
27.4 查询呈现
/admin/reload/status(结果查询)
static void Handle_Admin_ReloadStatus(XS_HttpReq* pReq, const RouteParamHTTP* a, uint32 n)
{
XS_ReloadResult t;
xvalue* pOut = xrtValueObject();
if ( !xsReloadQuery(ParseId(pReq), &t) ) {
(void)ReplyText(pReq, 404, "no such id");
return;
}
xrtValueObjectSetNew(pOut, XRT_STR_LITERAL("id"),
xrtValueInt64((int64)t.Id));
xrtValueObjectSetNew(pOut, XRT_STR_LITERAL("state"),
xrtValueInt64((int64)t.State));
xrtValueObjectSetNew(pOut, XRT_STR_LITERAL("revision"),
xrtValueInt64((int64)t.Revision));
xrtValueObjectSetNew(pOut, XRT_STR_LITERAL("message"),
xrtValueString(xrtStrView(t.Message))); /* FAILED 的原因 */
(void)ReplyJSON(pReq, 200, pOut);
xrtValueRelease(pOut);
}
终态会在几十毫秒到秒级(取决于脚本体量)内到达;前端轮询或刷新均可——Query 幂等且无副作用。
27.5 契约
- 鉴权线:管理路由必须有应用层鉴权;它改变服务行为,不是普通业务端点。
- 粒度线:默认 host 级;all 仅用于全局配置变更(整批事务)。
- 呈现线:受理与结果分离;查询幂等。
常见错误
管理路由裸奔
- 症状
- 公网可访问的 /admin/reload——任何人都能让你的服务换代码。
- 原因
- 触发面就是变更面。最少做法:独立 vhost(内网绑定)+ 令牌;更好的做法见 ch44 的信任边界设计。
文件监听没考虑换代
- 症状
- 热重载一次后自动触发器失灵。
- 原因
- 监听挂载用的定时器/句柄随旧代作废(ch30)——新代 ServiceInit 里要重新挂。这是代隔离"免费安全"的另一面。
编程练习
基础三档管理路由
实现 scope=host/server/all 的提交与 status 查询,全流程 curl 走通。
验收:三档各成功一次;坏脚本时 status 返回 FAILED 与原因。
进阶令牌鉴权
给管理路由加常数时间比较的令牌校验;错误令牌限速(第 22 章的降级思路)。
验收:错令牌 403;暴力尝试被限速到可忽略的速率。
挑战开发模式触发器
实现 mtime 轮询的保存即重载(防抖 + 代际重建),测量从保存到生效的端到端延迟。
验收:连续保存 10 次只上线最终版;端到端延迟 < 2s。
小结
| 要点 | 结论 |
|---|---|
| 触发三问 | 谁能(鉴权)、粒度(host/server/all)、结果(Query) |
| 管理路由 | 受理与结果分离;鉴权在应用层第一行 |
| 自动触发 | 监听 + Submit;防抖两层(应用合并 + latest-wins) |
| 代际注意 | 触发器随代作废,新代 Init 重建 |
本章涉及的 API 参考
xrt 延伸阅读
- xrt 参考 · file — mtime 轮询与文件监听的实现材料
- xrt 参考 · hash — 令牌常数时间比较所需的原语