feat(11-02): run plugin schedules as River periodic jobs through bonfire.Call
- pact.HasSchedule with ScheduledCommand and Daily/DailyAt/Every cadences (no River import) - bonfire.Call, Catalog and ErrUnknownCommand for in-process command runs - conga Daily/Every wall-clock schedules in app.timezone, periodic jobs on every worker, scheduled queue (MaxAttempts 1, unique by args within the cadence period) - scheduled worker runs only entries matching the compiled table; unregistered commands are skipped with a Warn log - generated app main publishes bonfire.NewCatalog(commands); hello main regenerated
This commit is contained in:
104
modules/conga/schedule.go
Normal file
104
modules/conga/schedule.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package conga
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"github.com/riverqueue/river"
|
||||
)
|
||||
|
||||
// Daily is a river.PeriodicSchedule that fires once a day at Hour:Minute:00
|
||||
// wall-clock time in Loc (UTC when nil). Unlike PeriodicInterval(24h) it
|
||||
// does not drift when the scheduler restarts.
|
||||
type Daily struct {
|
||||
Hour, Minute int
|
||||
Loc *time.Location
|
||||
}
|
||||
|
||||
// Next returns the first Hour:Minute:00 in Loc strictly after now. On a DST
|
||||
// day a time inside the gap resolves the way time.Date normalizes it.
|
||||
func (d Daily) Next(now time.Time) time.Time {
|
||||
loc := orUTC(d.Loc)
|
||||
n := now.In(loc)
|
||||
t := time.Date(n.Year(), n.Month(), n.Day(), d.Hour, d.Minute, 0, 0, loc)
|
||||
if !t.After(n) {
|
||||
t = time.Date(n.Year(), n.Month(), n.Day()+1, d.Hour, d.Minute, 0, 0, loc)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// Every is a river.PeriodicSchedule that fires at every multiple of Interval
|
||||
// since local midnight in Loc (UTC when nil). Interval must be positive.
|
||||
type Every struct {
|
||||
Interval time.Duration
|
||||
Loc *time.Location
|
||||
}
|
||||
|
||||
// Next returns local midnight plus the smallest multiple of Interval
|
||||
// strictly after now, and the next local midnight when that multiple falls
|
||||
// on or after it.
|
||||
func (e Every) Next(now time.Time) time.Time {
|
||||
loc := orUTC(e.Loc)
|
||||
n := now.In(loc)
|
||||
midnight := time.Date(n.Year(), n.Month(), n.Day(), 0, 0, 0, 0, loc)
|
||||
nextMidnight := time.Date(n.Year(), n.Month(), n.Day()+1, 0, 0, 0, 0, loc)
|
||||
if e.Interval <= 0 {
|
||||
return nextMidnight
|
||||
}
|
||||
k := n.Sub(midnight)/e.Interval + 1
|
||||
t := midnight.Add(k * e.Interval)
|
||||
if !t.Before(nextMidnight) {
|
||||
return nextMidnight
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func orUTC(loc *time.Location) *time.Location {
|
||||
if loc == nil {
|
||||
return time.UTC
|
||||
}
|
||||
return loc
|
||||
}
|
||||
|
||||
// scheduleFor returns the River schedule of a cadence in loc and its period.
|
||||
func scheduleFor(c pact.Cadence, loc *time.Location) (river.PeriodicSchedule, time.Duration, error) {
|
||||
if c.IsZero() {
|
||||
return nil, 0, fmt.Errorf("cadence is zero")
|
||||
}
|
||||
if h, m, ok := c.At(); ok {
|
||||
if h < 0 || h > 23 || m < 0 || m > 59 {
|
||||
return nil, 0, fmt.Errorf("daily time %02d:%02d is out of range", h, m)
|
||||
}
|
||||
return Daily{Hour: h, Minute: m, Loc: loc}, 24 * time.Hour, nil
|
||||
}
|
||||
d := c.Interval()
|
||||
if d <= 0 {
|
||||
return nil, 0, fmt.Errorf("interval %s is not positive", d)
|
||||
}
|
||||
if d < time.Second {
|
||||
return nil, 0, fmt.Errorf("interval %s is shorter than one second", d)
|
||||
}
|
||||
if (24*time.Hour)%d != 0 {
|
||||
return nil, 0, fmt.Errorf("interval %s does not divide 24h evenly", d)
|
||||
}
|
||||
return Every{Interval: d, Loc: loc}, d, nil
|
||||
}
|
||||
|
||||
// appLocation is the app.timezone location, UTC when unset.
|
||||
func appLocation(app *backpack.App) (*time.Location, error) {
|
||||
name := ""
|
||||
if app != nil && app.Config != nil {
|
||||
name = strings.TrimSpace(app.Config.String("app.timezone"))
|
||||
}
|
||||
if name == "" {
|
||||
return time.UTC, nil
|
||||
}
|
||||
loc, err := time.LoadLocation(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("conga: app.timezone %q: %w", name, err)
|
||||
}
|
||||
return loc, nil
|
||||
}
|
||||
Reference in New Issue
Block a user