定时器运行时语义
本文档包含 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
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_errorhook。 - callback 内
shield.sleep/shield.call挂起回调协程,actor 立即回到 mailbox 继续处理其他消息;回调结束后由既有 resume 机制继续。 - timer 归属当前 service。
- service exit 时自动取消 owned timers。
TimerId是 opaque userdata,不暴露 CAF。
timer 时间语义
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-clockClock(LuaClock,测试可拨)。os.clock()不接入(保持真实 CPU 时间)。参见 架构决策 AD-07。 - 周期 timer 使用固定节拍(fixed-cadence)语义:下一拍按「上一拍触发时刻 + interval_ms」推进,与 callback 执行时长无关。
fixed-cadence:
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
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。
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 期间 stashfork_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。
local task_id = shield.fork(fn)当前契约不提供 task cancel、status 或 join,避免引入额外等待图和死锁语义。需要结果时应使用 service call 或业务 channel。