Skip to content

feat: add crontab package for supporting autostart/stop. #844

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 4, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: add crontab package for supporting autostart/stop.
This is basically a small wrapper around robfig/cron/v3.

Fixes #817.
  • Loading branch information
johnstcn committed Apr 4, 2022
commit 50a9b26055c9ace125e1cc6a675a291fad2bc1ce
64 changes: 64 additions & 0 deletions coderd/crontab/crontab.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package crontab

import (
"time"

"github.com/robfig/cron/v3"
"golang.org/x/xerrors"
)

const parserFormat = cron.Minute | cron.Hour | cron.Dow

var defaultParser = cron.NewParser(parserFormat)

// WeeklySchedule represents a weekly cron schedule serializable to and from a string.
//
// Example Usage:
// local_sched, _ := cron.Parse("59 23 *")
// fmt.Println(sched.Next(time.Now().Format(time.RFC3339)))
// // Output: 2022-04-04T23:59:00Z
// us_sched, _ := cron.Parse("TZ=US/Central 30 9 1-5")
// fmt.Println(sched.Next(time.Now()).Format(time.RFC3339))
// // Output: 2022-04-04T14:30:00Z
type WeeklySchedule interface {
String() string
Next(time.Time) time.Time
}

// cronSchedule is a thin wrapper for cron.SpecSchedule that implements Stringer.
type cronSchedule struct {
sched *cron.SpecSchedule
// XXX: there isn't any nice way for robfig/cron to serialize
spec string
}

var _ WeeklySchedule = (*cronSchedule)(nil)

// String serializes the schedule to its original human-friendly format.
func (s cronSchedule) String() string {
return s.spec
}

// Next returns the next time in the schedule relative to t.
func (s cronSchedule) Next(t time.Time) time.Time {
return s.sched.Next(t)
}

func Parse(spec string) (WeeklySchedule, error) {
s, err := defaultParser.Parse(spec)
if err != nil {
return nil, xerrors.Errorf("parse schedule: %w", err)
}

schedule, ok := s.(*cron.SpecSchedule)
if !ok {
return nil, xerrors.Errorf("expected cron.SpecSchedule but got %T", s)
}

cs := &cronSchedule{
sched: schedule,
spec: spec,
}
return cs, nil

}
65 changes: 65 additions & 0 deletions coderd/crontab/crontab_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package crontab

import (
"testing"
"time"

"github.com/stretchr/testify/require"
)

func Test_Parse(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
spec string
at time.Time
expectedNext time.Time
expectedError string
}{
{
name: "with timezone",
spec: "TZ=US/Central 30 9 1-5",
at: time.Date(2022, 4, 1, 14, 29, 0, 0, time.UTC),
expectedNext: time.Date(2022, 4, 1, 14, 30, 0, 0, time.UTC),
expectedError: "",
},
{
name: "without timezone",
spec: "30 9 1-5",
at: time.Date(2022, 4, 1, 9, 29, 0, 0, time.Local),
expectedNext: time.Date(2022, 4, 1, 9, 30, 0, 0, time.Local),
expectedError: "",
},
{
name: "invalid schedule",
spec: "asdfasdfasdfsd",
at: time.Time{},
expectedNext: time.Time{},
expectedError: "parse schedule: expected exactly 3 fields, found 1: [asdfasdfasdfsd]",
},
{
name: "invalid location",
spec: "TZ=Fictional/Country 30 9 1-5",
at: time.Time{},
expectedNext: time.Time{},
expectedError: "parse schedule: provided bad location Fictional/Country: unknown time zone Fictional/Country",
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
actual, err := Parse(tc.spec)
if tc.expectedError == "" {
nextTime := actual.Next(tc.at)
require.NoError(t, err)
require.Equal(t, tc.expectedNext, nextTime)
require.Equal(t, tc.spec, actual.String())
} else {
require.EqualError(t, err, tc.expectedError)
require.Nil(t, actual)
}
})
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ require (
github.com/prometheus/procfs v0.7.3 // indirect
github.com/rivo/tview v0.0.0-20200712113419-c65badfc3d92 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/robfig/cron/v3 v3.0.1
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/afero v1.8.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1501,6 +1501,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qq
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
Expand Down