Skip to content

定时器运行时语义 ​

本文档包含 Shield 定时器、sleep 和 fork 相关的运行时语义决策。

当前实现状态:timer_once/timer/cancel_timer 已实现,每个 timer 由一个 CAF driver actor 驱动(delayed_send + tick_atom),到期向 owning service actor 发送 timer_fire_atom,callback 在 service actor 调度点通过 invoke_coroutine 以协程方式执行(可 shield.sleep / shield.call 挂起,actor 不被阻塞),错误路由到 on_error hook。shield.sleep 已实现协程感知(yield + _resume_after 定时器 resume)。shield.fork 已实现,task body 同样以协程方式执行。shield.call/call_timeout 已实现协程感知调用 + timeout。详细实现状态以 重构路线图 和 Lua API 测试用例 为准。

AD-07 分层时钟已实现:shield.now() 返回 wall-clock UTC ms(可注入 MockClock 测试可拨),shield.monotonic() 返回 real steady_clock ms(不可拨)。os.time()/os.date() 无参形式重定向到业务时钟,os.time(table)/os.date(fmt,t)/os.clock() 保持原生行为。LuaServiceManager::attach_clock() 注入时钟,Clock/SystemClock/MockClock 定义于 include/shield/lua/clock.hpp。test_lua_api_clock 覆盖 12 个用例。

timer API ​

lua
local id = shield.timer_once(1000, function()
end)

local id = shield.timer(1000, function()
end)

local ok = shield.cancel_timer(id)

规则:

  • timer callback 在当前 service 的 Lua VM 中以协程方式执行(invoke_coroutine 包裹),错误路由到 on_error hook。
  • callback 内 shield.sleep/shield.call 挂起回调协程,actor 立即回到 mailbox 继续处理其他消息;回调结束后由既有 resume 机制继续。
  • timer 归属当前 service。
  • service exit 时自动取消 owned timers。
  • TimerId 是 opaque userdata,不暴露 CAF。

timer 时间语义 ​

txt
timer_once(delay_ms)
timer(interval_ms)

规则:

  • delay_ms >= 0。
  • interval_ms > 0。
  • 时间分层(AD-07):定时器触发基于 monotonic clock(InfraClock,不可拨),与 CAF delayed_send 一致。Lua 业务读取的 shield.now() / os.time()(无参)/ os.date()(无参)走可注入的 wall-clock Clock(LuaClock,测试可拨)。os.clock() 不接入(保持真实 CPU 时间)。参见 架构决策 AD-07。
  • 周期 timer 使用固定节拍(fixed-cadence)语义:下一拍按「上一拍触发时刻 + interval_ms」推进,与 callback 执行时长无关。

fixed-cadence:

txt
driver actor 在派发本拍 fire 后立即续排下一拍 delayed_send,
下一拍时刻 = 本拍触发时刻 + interval_ms
  • callback 执行变慢不会平移节拍基准,也不会把落下的拍补发堆积——已经过去的节拍不追补。
  • 实际触发时刻相对理论节拍的漂移上限为 fire 消息的派发延迟(驱动侧记录的 next_fire_ms 每拍推进为「当前时刻 + interval_ms」,注释原话:drift equals the fire-dispatch lag)。
  • 不使用 fixed-rate,避免慢 callback 触发补发堆积——动机不变,只是节拍基准固定,不随 callback 结束时间平移。

timer 错误语义 ​

timer callback 抛错:

  • 错误通过 invoke_error_hook 路由到 service 的 on_error(err, context) hook,context.type = "timer",并记录一条 timer error: ... ERROR 日志。
  • timer_once 记录错误后结束(单次触发,fire 后条目即移除)。
  • timer 记录错误后继续按节拍触发,不会自动停止——实现里没有「出错即停」逻辑;能让它停下的是显式 cancel_timer、service exit(自动取消 owned timers)或下面的 panic 退出。
  • 连续错误达到阈值(kDefaultMaxErrorsBeforePanic,当前为 10 次;一次成功 dispatch 会把计数清零)时触发 on_panic 并 exit("panic")。
  • 错误进入 shield_ops 统计(Phase 2,尚未实现)。

由于周期 timer 出错不停,业务希望某些错误不触发 on_error 刷屏/计数时,才需要自己 pcall。

shield.sleep ​

lua
shield.sleep(1000)

语义:

  • 在协程上下文(message handler、timer/fork callback、on_init、on_shutdown)中只挂起当前 coroutine。
  • 仅在 module-level 代码等无协程上下文中退化为阻塞线程的 _block_sleep。
  • 协程路径不阻塞 runtime 线程。
  • 基于 timer 实现。
  • service exit 时 sleep coroutine 被取消。
  • 业务需要非阻塞等待时应在 service handler 内调用。

sleep(0) 表示让出当前 coroutine,回到 service ready queue 尾部。

shield.fork ​

shield.fork 将一个当前 service 归属的后台 task 投递到 owning service actor(经 fork_task_atom 在 actor 调度点执行),不创建新 service。

lua
local task_id = shield.fork(function()
  shield.send("player", "tick")
end)

规则:

  • fork task 与当前 service 共享 Lua VM 和 Lua 状态。
  • fork task 没有 service id,没有 mailbox,没有 name。
  • fork task 以协程方式执行(invoke_coroutine 包裹);shield.sleep / shield.call 在其中挂起 task 协程,不阻塞 actor。
  • on_init 内调度的 fork 在 init 完成后才串行执行:actor 在 init 期间 stash fork_task_atom,init_ready 时统一 unstash——fork 绝不与 spawning 线程并发进入同一 Lua VM。
  • fork unhandled error 记录日志和 ops 指标,不杀死 service。
  • service exit 时自动取消 owned fork tasks。

Task ID ​

shield.fork 当前返回 numeric task id。

lua
local task_id = shield.fork(fn)

当前契约不提供 task cancel、status 或 join,避免引入额外等待图和死锁语义。需要结果时应使用 service call 或业务 channel。

Apache License 2.0