1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-03-20 14:31:09 +02:00

229 lines
4.6 KiB
Go
Raw Normal View History

2023-05-09 22:36:31 +03:00
// Package cron implements a crontab-like service to execute and schedule
// repeative tasks/jobs.
//
// Example:
//
// c := cron.New()
// c.MustAdd("dailyReport", "0 0 * * *", func() { ... })
// c.Start()
package cron
import (
"errors"
"fmt"
2024-12-22 16:05:38 +02:00
"slices"
2023-05-09 22:36:31 +03:00
"sync"
"time"
)
// Cron is a crontab-like struct for tasks/jobs scheduling.
type Cron struct {
timezone *time.Location
ticker *time.Ticker
startTimer *time.Timer
2024-03-20 23:55:32 +02:00
tickerDone chan bool
2024-12-25 22:24:24 +02:00
jobs []*Job
2024-09-29 19:23:19 +03:00
interval time.Duration
mux sync.RWMutex
2023-05-09 22:36:31 +03:00
}
// New create a new Cron struct with default tick interval of 1 minute
// and timezone in UTC.
//
// You can change the default tick interval with Cron.SetInterval().
// You can change the default timezone with Cron.SetTimezone().
func New() *Cron {
return &Cron{
2024-03-20 23:55:32 +02:00
interval: 1 * time.Minute,
timezone: time.UTC,
2024-12-22 16:05:38 +02:00
jobs: []*Job{},
2024-03-20 23:55:32 +02:00
tickerDone: make(chan bool),
2023-05-09 22:36:31 +03:00
}
}
// SetInterval changes the current cron tick interval
// (it usually should be >= 1 minute).
func (c *Cron) SetInterval(d time.Duration) {
// update interval
2024-09-29 19:23:19 +03:00
c.mux.Lock()
2023-05-09 22:36:31 +03:00
wasStarted := c.ticker != nil
c.interval = d
2024-09-29 19:23:19 +03:00
c.mux.Unlock()
2023-05-09 22:36:31 +03:00
// restart the ticker
if wasStarted {
c.Start()
}
}
// SetTimezone changes the current cron tick timezone.
func (c *Cron) SetTimezone(l *time.Location) {
2024-09-29 19:23:19 +03:00
c.mux.Lock()
defer c.mux.Unlock()
2023-05-09 22:36:31 +03:00
c.timezone = l
}
// MustAdd is similar to Add() but panic on failure.
func (c *Cron) MustAdd(jobId string, cronExpr string, run func()) {
if err := c.Add(jobId, cronExpr, run); err != nil {
panic(err)
}
}
// Add registers a single cron job.
//
// If there is already a job with the provided id, then the old job
// will be replaced with the new one.
//
// cronExpr is a regular cron expression, eg. "0 */3 * * *" (aka. at minute 0 past every 3rd hour).
// Check cron.NewSchedule() for the supported tokens.
2024-12-22 16:05:38 +02:00
func (c *Cron) Add(jobId string, cronExpr string, fn func()) error {
if fn == nil {
return errors.New("failed to add new cron job: fn must be non-nil function")
2023-05-09 22:36:31 +03:00
}
schedule, err := NewSchedule(cronExpr)
if err != nil {
return fmt.Errorf("failed to add new cron job: %w", err)
}
2024-12-22 16:05:38 +02:00
c.mux.Lock()
defer c.mux.Unlock()
// remove previous (if any)
c.jobs = slices.DeleteFunc(c.jobs, func(j *Job) bool {
return j.Id() == jobId
})
// add new
c.jobs = append(c.jobs, &Job{
id: jobId,
fn: fn,
2023-05-09 22:36:31 +03:00
schedule: schedule,
2024-12-22 16:05:38 +02:00
})
2023-05-09 22:36:31 +03:00
return nil
}
// Remove removes a single cron job by its id.
func (c *Cron) Remove(jobId string) {
2024-09-29 19:23:19 +03:00
c.mux.Lock()
defer c.mux.Unlock()
2023-05-09 22:36:31 +03:00
2024-12-22 16:05:38 +02:00
if c.jobs == nil {
return // nothing to remove
}
c.jobs = slices.DeleteFunc(c.jobs, func(j *Job) bool {
return j.Id() == jobId
})
2023-05-09 22:36:31 +03:00
}
// RemoveAll removes all registered cron jobs.
func (c *Cron) RemoveAll() {
2024-09-29 19:23:19 +03:00
c.mux.Lock()
defer c.mux.Unlock()
2023-05-09 22:36:31 +03:00
2024-12-22 16:05:38 +02:00
c.jobs = []*Job{}
2023-05-09 22:36:31 +03:00
}
2023-07-08 13:51:00 +03:00
// Total returns the current total number of registered cron jobs.
func (c *Cron) Total() int {
2024-09-29 19:23:19 +03:00
c.mux.RLock()
defer c.mux.RUnlock()
2023-07-08 13:51:00 +03:00
return len(c.jobs)
}
2024-12-22 16:05:38 +02:00
// Jobs returns a shallow copy of the currently registered cron jobs.
func (c *Cron) Jobs() []*Job {
c.mux.RLock()
defer c.mux.RUnlock()
copy := make([]*Job, len(c.jobs))
for i, j := range c.jobs {
copy[i] = j
}
return copy
}
2023-05-09 22:36:31 +03:00
// Stop stops the current cron ticker (if not already).
//
// You can resume the ticker by calling Start().
func (c *Cron) Stop() {
2024-09-29 19:23:19 +03:00
c.mux.Lock()
defer c.mux.Unlock()
2023-05-09 22:36:31 +03:00
if c.startTimer != nil {
c.startTimer.Stop()
c.startTimer = nil
}
2023-05-09 22:36:31 +03:00
if c.ticker == nil {
return // already stopped
}
2024-03-20 23:55:32 +02:00
c.tickerDone <- true
2023-05-09 22:36:31 +03:00
c.ticker.Stop()
c.ticker = nil
}
// Start starts the cron ticker.
//
// Calling Start() on already started cron will restart the ticker.
func (c *Cron) Start() {
c.Stop()
// delay the ticker to start at 00 of 1 c.interval duration
now := time.Now()
next := now.Add(c.interval).Truncate(c.interval)
delay := next.Sub(now)
2024-09-29 19:23:19 +03:00
c.mux.Lock()
c.startTimer = time.AfterFunc(delay, func() {
2024-09-29 19:23:19 +03:00
c.mux.Lock()
c.ticker = time.NewTicker(c.interval)
2024-09-29 19:23:19 +03:00
c.mux.Unlock()
// run immediately at 00
c.runDue(time.Now())
// run after each tick
go func() {
2024-03-20 23:55:32 +02:00
for {
select {
case <-c.tickerDone:
return
case t := <-c.ticker.C:
c.runDue(t)
}
}
}()
})
2024-09-29 19:23:19 +03:00
c.mux.Unlock()
2023-05-09 22:36:31 +03:00
}
2023-05-13 22:10:14 +03:00
// HasStarted checks whether the current Cron ticker has been started.
func (c *Cron) HasStarted() bool {
2024-09-29 19:23:19 +03:00
c.mux.RLock()
defer c.mux.RUnlock()
2023-05-13 22:10:14 +03:00
return c.ticker != nil
}
2023-05-09 22:36:31 +03:00
// runDue runs all registered jobs that are scheduled for the provided time.
func (c *Cron) runDue(t time.Time) {
2024-09-29 19:23:19 +03:00
c.mux.RLock()
defer c.mux.RUnlock()
2023-05-09 22:36:31 +03:00
moment := NewMoment(t.In(c.timezone))
for _, j := range c.jobs {
if j.schedule.IsDue(moment) {
2024-12-22 16:05:38 +02:00
go j.Run()
2023-05-09 22:36:31 +03:00
}
}
}