mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-09-15 10:19:13 +00:00
79 lines
2.6 KiB
Go
79 lines
2.6 KiB
Go
// Package cron runs module crons in-process for the self-hosted long-lived
|
|
// container. It reads each registered cron's Schedule field and fires its
|
|
// handler on time, in UTC.
|
|
package cron
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"runtime/debug"
|
|
"time"
|
|
|
|
"github.com/robfig/cron/v3"
|
|
|
|
"github.com/tiennm99/miti99bot/internal/log"
|
|
"github.com/tiennm99/miti99bot/internal/modules"
|
|
)
|
|
|
|
// cronTimeout caps a single in-process cron fire so a slow handler cannot
|
|
// wedge the scheduler. 60s is the budget; long crons must offload and return
|
|
// fast.
|
|
const cronTimeout = 60 * time.Second
|
|
|
|
// Run starts an in-process scheduler that fires every registered cron with a
|
|
// non-empty Schedule. Each fire is wrapped with a timeout, panic recovery, and
|
|
// structured logging — modules.DispatchScheduled provides none of these (it is
|
|
// only a registry lookup + handler call), so the wrapping lives here.
|
|
//
|
|
// A malformed schedule string is fatal (returned as an error) so a config typo
|
|
// fails fast at startup rather than silently never firing. The returned stop
|
|
// function halts the scheduler and waits for any in-flight job to finish; it is
|
|
// safe to call exactly once (e.g. via defer).
|
|
func Run(ctx context.Context, reg *modules.Registry) (stop func(), err error) {
|
|
c := cron.New(cron.WithLocation(time.UTC))
|
|
|
|
registered := 0
|
|
for _, cr := range reg.Crons() {
|
|
if cr.Schedule == "" {
|
|
log.Warn("cron has empty schedule; it will never fire", "name", cr.Name)
|
|
continue
|
|
}
|
|
name := cr.Name // capture per iteration
|
|
if _, err := c.AddFunc(cr.Schedule, func() { fire(ctx, name, reg) }); err != nil {
|
|
return func() {}, fmt.Errorf("cron %q has invalid schedule %q: %w", cr.Name, cr.Schedule, err)
|
|
}
|
|
registered++
|
|
}
|
|
|
|
c.Start()
|
|
log.Info("cron scheduler started", "registered", registered)
|
|
|
|
stop = func() {
|
|
// c.Stop() returns a context that is done once running jobs complete.
|
|
stopCtx := c.Stop()
|
|
<-stopCtx.Done()
|
|
}
|
|
return stop, nil
|
|
}
|
|
|
|
// fire dispatches one scheduled cron with its own timeout and panic recovery so
|
|
// a slow or panicking handler never wedges or kills the scheduler.
|
|
func fire(ctx context.Context, name string, reg *modules.Registry) {
|
|
runCtx, cancel := context.WithTimeout(ctx, cronTimeout)
|
|
defer cancel()
|
|
|
|
log.Info("cron triggered", "source", "scheduler", "name", name)
|
|
defer func() {
|
|
if rec := recover(); rec != nil {
|
|
log.Error("cron handler panic",
|
|
"source", "scheduler",
|
|
"name", name,
|
|
"panic", rec,
|
|
"stack", string(debug.Stack()))
|
|
}
|
|
}()
|
|
if err := modules.DispatchScheduled(runCtx, name, reg); err != nil {
|
|
log.Error("cron failed", "source", "scheduler", "name", name, "err", err)
|
|
}
|
|
}
|