第 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 重建
xrt 延伸阅读