Skip to content

Commit d0fb049

Browse files
add cron scheduler for recurring jobs
1 parent 71b3bc7 commit d0fb049

1 file changed

Lines changed: 112 additions & 0 deletions

File tree

scheduler.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package relay
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"os/signal"
8+
"sync"
9+
"syscall"
10+
"time"
11+
12+
"github.com/google/uuid"
13+
"github.com/robfig/cron/v3"
14+
)
15+
16+
type SchedulerOpts struct {
17+
Logger Logger
18+
Location *time.Location
19+
}
20+
21+
type schedEntry struct {
22+
id string
23+
spec string
24+
task *Task
25+
opts []Option
26+
}
27+
28+
type Scheduler struct {
29+
client *Client
30+
cron *cron.Cron
31+
logger Logger
32+
nowFunc func() time.Time
33+
34+
mu sync.Mutex
35+
entries map[cron.EntryID]*schedEntry
36+
}
37+
38+
func NewScheduler(r RedisConnOpt, opts *SchedulerOpts) *Scheduler {
39+
if opts == nil {
40+
opts = &SchedulerOpts{}
41+
}
42+
loc := opts.Location
43+
if loc == nil {
44+
loc = time.Local
45+
}
46+
logger := opts.Logger
47+
if logger == nil {
48+
logger = defaultLogger()
49+
}
50+
return &Scheduler{
51+
client: NewClient(r),
52+
cron: cron.New(cron.WithLocation(loc)),
53+
logger: logger,
54+
nowFunc: time.Now,
55+
entries: make(map[cron.EntryID]*schedEntry),
56+
}
57+
}
58+
59+
func (s *Scheduler) Register(cronspec string, task *Task, opts ...Option) (string, error) {
60+
if task == nil {
61+
return "", errors.New("relay: cannot register nil task")
62+
}
63+
entry := &schedEntry{id: uuid.NewString(), spec: cronspec, task: task, opts: opts}
64+
cid, err := s.cron.AddFunc(cronspec, func() { s.fire(entry) })
65+
if err != nil {
66+
return "", err
67+
}
68+
s.mu.Lock()
69+
s.entries[cid] = entry
70+
s.mu.Unlock()
71+
return entry.id, nil
72+
}
73+
74+
func (s *Scheduler) fire(e *schedEntry) {
75+
fireAt := s.nowFunc().Truncate(time.Minute)
76+
id := fmt.Sprintf("cron:%s:%d", e.id, fireAt.Unix())
77+
opts := append([]Option{TaskID(id)}, e.opts...)
78+
79+
_, err := s.client.Enqueue(e.task, opts...)
80+
switch {
81+
case err == nil:
82+
s.logger.Info(fmt.Sprintf("relay: scheduler enqueued %s (%s)", e.task.Type(), e.spec))
83+
case errors.Is(err, ErrTaskIDConflict), errors.Is(err, ErrDuplicateTask):
84+
return
85+
default:
86+
s.logger.Error(fmt.Sprintf("relay: scheduler failed to enqueue %s: %v", e.task.Type(), err))
87+
}
88+
}
89+
90+
func (s *Scheduler) Start() error {
91+
s.cron.Start()
92+
s.logger.Info("relay: scheduler started")
93+
return nil
94+
}
95+
96+
func (s *Scheduler) Run() error {
97+
if err := s.Start(); err != nil {
98+
return err
99+
}
100+
sig := make(chan os.Signal, 1)
101+
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
102+
<-sig
103+
s.Shutdown()
104+
return nil
105+
}
106+
107+
func (s *Scheduler) Shutdown() {
108+
ctx := s.cron.Stop()
109+
<-ctx.Done()
110+
_ = s.client.Close()
111+
s.logger.Info("relay: scheduler stopped")
112+
}

0 commit comments

Comments
 (0)