Cell 启动可达性(INV-SERVE-REACHABLE-01)

wesgine serve 到达"可被探活"的时间是有界常数,不随租户数或数据库体积增长。


核心契约

router.Start(绑端口)    ← 先
sdnotify.Ready()          ← 先
ActivatePersisted(热身) ← 后,在 goroutine 里

为什么顺序承重

每一个盯着这个进程的监督者都用固定超时判死:

监督者超时
systemd TimeoutStartSec120s
teleclaw watchdog5s 探针 × 3 次
负载均衡器配置固定

它们都无法区分"正在启动"和"已经死了"。

把一个随数据增长的操作放在可达性之前 → 部署跑够久之后必然穿过某个固定阈值 → 被判死、重启、把已完成的热身全部作废。


三条派生纪律

① 热身可被取消

ActivatePersisted 的循环每轮查 ctx.Err():

func (h *Hypervisor) ActivatePersisted(ctx context.Context) error {
    for _, cellID := range registered {
        if ctx.Err() != nil {
            return ctx.Err() // SIGTERM 后不继续 boot
        }
        h.activateCell(ctx, cellID)
    }
    return nil
}

不查就会在 SIGTERM 后继续 boot 那些 Stop 已经排空的 Cell,而每一个 boot 都要等它跑完进程才能退。

② SIGKILL 是自我强化的

TimeoutStopSec 升级成 SIGKILL → 每个打开的 Cell 留下未 checkpoint 的 WAL → 下次 boot 先付恢复 → 一轮比一轮深的重启循环。

③ 跳过热身不损失正确性

Cell 中间件对不同温度的处理:

温度首请求行为
Cool同步 WarmUp
Cold503 + Retry-After + 后台热

sweep 没走到的 Cell 只是慢,不是坏。

代价说清楚:这些 Cell 的 Cron/IMAP 推迟到下次热身才恢复——用可预期的延迟换掉不可预期的不可达。


与 systemd 的配合

[Service]
Type=notify
TimeoutStartSec=120
TimeoutStopSec=30

脚本形态对比

systemd脚本 (wes2.1_start.sh)
就绪判定READY=1 信号sleep 2 后看进程还在
崩溃重启Restart=on-failure无
停止超时TimeoutStopSec=305 秒
重复保护systemd 单实例查端口占用

脚本形态下,INV-SERVE-REACHABLE-01 的 systemd 相关纪律不生效,但可达性契约本身仍成立。


反模式

禁止正确做法
ActivatePersisted 放在 router.Start 前先绑端口,后热身
热身循环不查 ctx.Err()每轮查,SIGTERM 后退出
用 WatchdogSec 做活性检测/health 端点
热身失败阻塞整个进程单 Cell 失败跳过,不阻塞