Revel框架不提供原生定时任务支持,因其定位为全栈Web框架,专注HTTP生命周期管理,缺乏任务队列、持久化触发器等调度模块;需自行集成如robfig/cron/v3或外挂Redis任务队列。

Revel 框架本身不内置定时任务调度能力,必须自行集成第三方库或系统级方案;直接用 time.Ticker 启动 goroutine 是最轻量、最可控的做法,但要注意生命周期与应用重启问题。
为什么 Revel 不提供原生定时任务支持
Revel 定位是全栈 Web 框架,核心关注请求路由、拦截器、模板渲染等 HTTP 生命周期管理。它没有设计任务队列、持久化触发器、集群协调等调度系统所需模块。官方文档和源码中找不到 JobScheduler、CronManager 或类似接口。
常见误操作是试图在 app.Init() 里调用 quartz.Start() 或 jobrunner.Start() 后就认为“已集成”,结果发现进程重启后任务丢失、goroutine 泄漏、或 panic 报 panic: close of closed channel。
- Revel 的
AppInit是单次执行,不保证 goroutine 持续存活 - Web 服务器热重载(如
revel run)会 kill 原进程,所有未托管的 goroutine 被强制终止 - 无内置钩子监听进程退出,无法优雅 stop ticker 或 flush pending jobs
用 time.Ticker 实现可靠单机定时任务
适用于日志轮转、缓存刷新、心跳上报等低频、非关键、可丢失的任务。关键是把 ticker 绑定到 Revel 应用生命周期里,并避免重复启动。
推荐在 app/init.go 的 init() 函数之后、app.Run() 之前启动:
var ticker *time.Ticker
<p>func init() {
revel.InterceptFunc(checkAndStartTicker, revel.BEFORE)
}</p><p>func checkAndStartTicker(c <em>revel.Controller) revel.Result {
if ticker == nil {
ticker = time.NewTicker(5 </em> time.Minute)
go func() {
for range ticker.C {
// 执行你的任务逻辑,例如:
// cleanExpiredSessions()
// syncConfigFromRemote()
}
}()
revel.INFO.Printf("ticker started: 5m interval")
}
return nil
}
</p>
- 用
revel.InterceptFunc+BEFORE确保只初始化一次,避免多 controller 并发触发 - 不要在
Controller.Action里启动 ticker,会导致每次请求都新建 goroutine - 若需停止(如测试或 graceful shutdown),保存
ticker全局变量并显式调用ticker.Stop() - 注意:该方式无法跨实例同步,集群部署时每个节点都会独立执行
需要持久化/集群/精确 cron 时怎么选库
当任务不能丢、需按 0 0 * * * 执行、或要支持失败重试时,time.Ticker 就不够用了。此时应外挂成熟调度器,而非强行塞进 Revel 流程。
-
robfig/cron/v3:最常用,支持标准 crontab 语法,但仍是单机内存型,进程挂掉即丢失任务 -
go-co-op/gocron:API 更现代,支持Every(2).Hour().Do()链式调用,也默认不持久化 - 真正生产级选择是解耦:用
Redis+asynq或machinery做任务队列,再由外部 cron(如 Linuxcron或 k8sCronJob)触发curl http://localhost:9000/api/v1/schedule-job接口入队 - 绝对避免在 Revel 中启动
quartz(Java)或直接调用 shellcrontab -e—— 权限、路径、环境变量均不可控
最容易被忽略的坑:SIGTERM 与 goroutine 清理
Revel 进程收到 SIGTERM(如 docker stop、k8s rollout)时,默认不等待 goroutine 结束就退出,导致正在执行的定时任务被粗暴中断。
如果你的任务含数据库写入、文件写入或网络调用,必须加 shutdown hook:
func onAppShutdown() {
if ticker != nil {
ticker.Stop()
revel.INFO.Println("ticker stopped on shutdown")
}
// 等待正在运行的任务结束(如有 context 控制)
}
// 在 init() 末尾注册
revel.OnAppEnd(onAppShutdown)
更严谨的做法是给每个任务加上 context.WithTimeout,并在 onAppShutdown 中 cancel context,否则一个卡住的 http.Post 可能拖住整个进程退出长达 30 秒。











