Add Periodic config to job

This commit is contained in:
Alex Dadgar 2015-11-30 16:51:56 -08:00
parent b039f963f0
commit a9b95bca2d
5 changed files with 203 additions and 1 deletions

View file

@ -90,6 +90,7 @@ func parseJob(result *structs.Job, list *ast.ObjectList) error {
delete(m, "constraint")
delete(m, "meta")
delete(m, "update")
delete(m, "periodic")
// Set the ID and name to the object key
result.ID = obj.Keys[0].Token.Value().(string)
@ -127,6 +128,13 @@ func parseJob(result *structs.Job, list *ast.ObjectList) error {
}
}
// If we have a periodic definition, then parse that
if o := listVal.Filter("periodic"); len(o.Items) > 0 {
if err := parsePeriodic(&result.Periodic, o); err != nil {
return err
}
}
// Parse out meta fields. These are in HCL as a list so we need
// to iterate over them and merge them.
if metaO := listVal.Filter("meta"); len(metaO.Items) > 0 {
@ -666,3 +674,43 @@ func parseUpdate(result *structs.UpdateStrategy, list *ast.ObjectList) error {
}
return dec.Decode(m)
}
func parsePeriodic(result *structs.PeriodicConfig, list *ast.ObjectList) error {
list = list.Elem()
if len(list.Items) > 1 {
return fmt.Errorf("only one 'periodic' block allowed per job")
}
// Get our resource object
o := list.Items[0]
var m map[string]interface{}
if err := hcl.DecodeObject(&m, o.Val); err != nil {
return err
}
// Enabled by default if the periodic block exists.
if value, ok := m["enabled"]; !ok {
m["Enabled"] = true
} else {
enabled, err := parseBool(value)
if err != nil {
return fmt.Errorf("periodic.enabled should be set to true or false; %v", err)
}
m["Enabled"] = enabled
}
// If "cron_spec" is provided, set the type to "cron" and store the spec.
if cron, ok := m["cron_spec"]; ok {
m["SpecType"] = structs.PeriodicSpecCron
m["Spec"] = cron
}
// Build the constraint
var p structs.PeriodicConfig
if err := mapstructure.WeakDecode(m, &p); err != nil {
return err
}
*result = p
return nil
}

View file

@ -231,6 +231,23 @@ func TestParse(t *testing.T) {
false,
},
{
"periodic-cron.hcl",
&structs.Job{
ID: "foo",
Name: "foo",
Priority: 50,
Region: "global",
Type: "service",
Periodic: structs.PeriodicConfig{
Enabled: true,
SpecType: structs.PeriodicSpecCron,
Spec: "*/5 * * *",
},
},
false,
},
{
"specify-job.hcl",
&structs.Job{

View file

@ -0,0 +1,5 @@
job "foo" {
periodic {
cron_spec = "*/5 * * *"
}
}

View file

@ -11,6 +11,7 @@ import (
"strings"
"time"
"github.com/gorhill/cronexpr"
"github.com/hashicorp/go-msgpack/codec"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-version"
@ -760,6 +761,9 @@ type Job struct {
// Update is used to control the update strategy
Update UpdateStrategy
// Periodic is used to define the interval the job is run at.
Periodic PeriodicConfig
// Meta is used to associate arbitrary metadata with this
// job. This is opaque to Nomad.
Meta map[string]string
@ -841,6 +845,13 @@ func (j *Job) Validate() error {
mErr.Errors = append(mErr.Errors, outer)
}
}
// Validate periodic is only used with batch jobs.
if j.Periodic.Enabled && j.Type != JobTypeBatch {
mErr.Errors = append(mErr.Errors,
fmt.Errorf("Periodic can only be used with %q scheduler", JobTypeBatch))
}
return mErr.ErrorOrNil()
}
@ -895,6 +906,61 @@ func (u *UpdateStrategy) Rolling() bool {
return u.Stagger > 0 && u.MaxParallel > 0
}
const (
// PeriodicSpecCron is used for a cron spec.
PeriodicSpecCron = "cron"
)
// Periodic defines the interval a job should be run at.
type PeriodicConfig struct {
// Enabled determines if the job should be run periodically.
Enabled bool
// Spec specifies the interval the job should be run as. It is parsed based
// on the SpecType.
Spec string
// SpecType defines the format of the spec.
SpecType string
}
func (p *PeriodicConfig) Validate() error {
if !p.Enabled {
return nil
}
if p.SpecType == "" {
return fmt.Errorf("Must specify a spec type to be able to parse the interval")
}
switch p.SpecType {
case PeriodicSpecCron:
// Validate the cron spec
if _, err := cronexpr.Parse(p.Spec); err != nil {
return fmt.Errorf("Invalid cron spec %q: %v", p.Spec, err)
}
default:
return fmt.Errorf("Unknown specification type %q", p.SpecType)
}
return nil
}
// Next returns the closest time instant matching the spec that is after the
// passed time. If no matching instance exists, the zero value of time.Time is
// returned. The `time.Location` of the returned value matches that of the
// passed time.
func (p *PeriodicConfig) Next(fromTime time.Time) time.Time {
switch p.SpecType {
case PeriodicSpecCron:
if e, err := cronexpr.Parse(p.Spec); err == nil {
return e.Next(fromTime)
}
}
return time.Time{}
}
// RestartPolicy influences how Nomad restarts Tasks when they
// crash or fail.
type RestartPolicy struct {

View file

@ -1,11 +1,12 @@
package structs
import (
"github.com/hashicorp/go-multierror"
"reflect"
"strings"
"testing"
"time"
"github.com/hashicorp/go-multierror"
)
func TestJob_Validate(t *testing.T) {
@ -34,6 +35,18 @@ func TestJob_Validate(t *testing.T) {
t.Fatalf("err: %s", err)
}
j = &Job{
Type: JobTypeService,
Periodic: PeriodicConfig{
Enabled: true,
},
}
err = j.Validate()
mErr = err.(*multierror.Error)
if !strings.Contains(mErr.Error(), "Periodic") {
t.Fatalf("err: %s", err)
}
j = &Job{
Region: "global",
ID: GenerateUUID(),
@ -488,3 +501,56 @@ func TestJob_ExpandServiceNames(t *testing.T) {
}
}
func TestPeriodicConfig_EnabledInvalid(t *testing.T) {
// Create a config that is enabled but with no interval specified.
p := &PeriodicConfig{Enabled: true}
if err := p.Validate(); err == nil {
t.Fatal("Enabled PeriodicConfig with no spec or type shouldn't be valid")
}
// Create a config that is enabled, with a spec but no type specified.
p = &PeriodicConfig{Enabled: true, Spec: "foo"}
if err := p.Validate(); err == nil {
t.Fatal("Enabled PeriodicConfig with no spec type shouldn't be valid")
}
// Create a config that is enabled, with a spec type but no spec specified.
p = &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron}
if err := p.Validate(); err == nil {
t.Fatal("Enabled PeriodicConfig with no spec shouldn't be valid")
}
}
func TestPeriodicConfig_InvalidCron(t *testing.T) {
specs := []string{"foo", "* *", "@foo"}
for _, spec := range specs {
p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec}
if err := p.Validate(); err == nil {
t.Fatal("Invalid cron spec")
}
}
}
func TestPeriodicConfig_ValidCron(t *testing.T) {
specs := []string{"0 0 29 2 *", "@hourly", "0 0-15 * * *"}
for _, spec := range specs {
p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec}
if err := p.Validate(); err != nil {
t.Fatal("Passed valid cron")
}
}
}
func TestPeriodicConfig_NextCron(t *testing.T) {
from := time.Date(2009, time.November, 10, 23, 22, 30, 0, time.UTC)
specs := []string{"0 0 29 2 * 1980", "*/5 * * * *"}
expected := []time.Time{time.Time{}, time.Date(2009, time.November, 10, 23, 25, 0, 0, time.UTC)}
for i, spec := range specs {
p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec}
n := p.Next(from)
if expected[i] != n {
t.Fatalf("Next(%v) returned %v; want %v", from, n, expected[i])
}
}
}