Skip to content

Commit 50a9b26

Browse files
committed
feat: add crontab package for supporting autostart/stop.
This is basically a small wrapper around robfig/cron/v3. Fixes #817.
1 parent 315676b commit 50a9b26

File tree

4 files changed

+132
-0
lines changed

4 files changed

+132
-0
lines changed

coderd/crontab/crontab.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package crontab
2+
3+
import (
4+
"time"
5+
6+
"github.com/robfig/cron/v3"
7+
"golang.org/x/xerrors"
8+
)
9+
10+
const parserFormat = cron.Minute | cron.Hour | cron.Dow
11+
12+
var defaultParser = cron.NewParser(parserFormat)
13+
14+
// WeeklySchedule represents a weekly cron schedule serializable to and from a string.
15+
//
16+
// Example Usage:
17+
// local_sched, _ := cron.Parse("59 23 *")
18+
// fmt.Println(sched.Next(time.Now().Format(time.RFC3339)))
19+
// // Output: 2022-04-04T23:59:00Z
20+
// us_sched, _ := cron.Parse("TZ=US/Central 30 9 1-5")
21+
// fmt.Println(sched.Next(time.Now()).Format(time.RFC3339))
22+
// // Output: 2022-04-04T14:30:00Z
23+
type WeeklySchedule interface {
24+
String() string
25+
Next(time.Time) time.Time
26+
}
27+
28+
// cronSchedule is a thin wrapper for cron.SpecSchedule that implements Stringer.
29+
type cronSchedule struct {
30+
sched *cron.SpecSchedule
31+
// XXX: there isn't any nice way for robfig/cron to serialize
32+
spec string
33+
}
34+
35+
var _ WeeklySchedule = (*cronSchedule)(nil)
36+
37+
// String serializes the schedule to its original human-friendly format.
38+
func (s cronSchedule) String() string {
39+
return s.spec
40+
}
41+
42+
// Next returns the next time in the schedule relative to t.
43+
func (s cronSchedule) Next(t time.Time) time.Time {
44+
return s.sched.Next(t)
45+
}
46+
47+
func Parse(spec string) (WeeklySchedule, error) {
48+
s, err := defaultParser.Parse(spec)
49+
if err != nil {
50+
return nil, xerrors.Errorf("parse schedule: %w", err)
51+
}
52+
53+
schedule, ok := s.(*cron.SpecSchedule)
54+
if !ok {
55+
return nil, xerrors.Errorf("expected cron.SpecSchedule but got %T", s)
56+
}
57+
58+
cs := &cronSchedule{
59+
sched: schedule,
60+
spec: spec,
61+
}
62+
return cs, nil
63+
64+
}

coderd/crontab/crontab_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package crontab
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func Test_Parse(t *testing.T) {
11+
t.Parallel()
12+
testCases := []struct {
13+
name string
14+
spec string
15+
at time.Time
16+
expectedNext time.Time
17+
expectedError string
18+
}{
19+
{
20+
name: "with timezone",
21+
spec: "TZ=US/Central 30 9 1-5",
22+
at: time.Date(2022, 4, 1, 14, 29, 0, 0, time.UTC),
23+
expectedNext: time.Date(2022, 4, 1, 14, 30, 0, 0, time.UTC),
24+
expectedError: "",
25+
},
26+
{
27+
name: "without timezone",
28+
spec: "30 9 1-5",
29+
at: time.Date(2022, 4, 1, 9, 29, 0, 0, time.Local),
30+
expectedNext: time.Date(2022, 4, 1, 9, 30, 0, 0, time.Local),
31+
expectedError: "",
32+
},
33+
{
34+
name: "invalid schedule",
35+
spec: "asdfasdfasdfsd",
36+
at: time.Time{},
37+
expectedNext: time.Time{},
38+
expectedError: "parse schedule: expected exactly 3 fields, found 1: [asdfasdfasdfsd]",
39+
},
40+
{
41+
name: "invalid location",
42+
spec: "TZ=Fictional/Country 30 9 1-5",
43+
at: time.Time{},
44+
expectedNext: time.Time{},
45+
expectedError: "parse schedule: provided bad location Fictional/Country: unknown time zone Fictional/Country",
46+
},
47+
}
48+
49+
for _, tc := range testCases {
50+
tc := tc
51+
t.Run(tc.name, func(t *testing.T) {
52+
t.Parallel()
53+
actual, err := Parse(tc.spec)
54+
if tc.expectedError == "" {
55+
nextTime := actual.Next(tc.at)
56+
require.NoError(t, err)
57+
require.Equal(t, tc.expectedNext, nextTime)
58+
require.Equal(t, tc.spec, actual.String())
59+
} else {
60+
require.EqualError(t, err, tc.expectedError)
61+
require.Nil(t, actual)
62+
}
63+
})
64+
}
65+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ require (
222222
github.com/prometheus/procfs v0.7.3 // indirect
223223
github.com/rivo/tview v0.0.0-20200712113419-c65badfc3d92 // indirect
224224
github.com/rivo/uniseg v0.2.0 // indirect
225+
github.com/robfig/cron/v3 v3.0.1
225226
github.com/russross/blackfriday/v2 v2.1.0 // indirect
226227
github.com/sirupsen/logrus v1.8.1 // indirect
227228
github.com/spf13/afero v1.8.1 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1501,6 +1501,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qq
15011501
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
15021502
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
15031503
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
1504+
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
1505+
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
15041506
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
15051507
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
15061508
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=

0 commit comments

Comments
 (0)