Skip to content

Commit

Permalink
Merge pull request #44 from fopina/upstream/4.0.0-rc1.2
Browse files Browse the repository at this point in the history
add support for hash scheduling (distribworks#1260)
  • Loading branch information
fopina authored Sep 16, 2024
2 parents 060a046 + 4ff9416 commit 5e02981
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 31 deletions.
73 changes: 48 additions & 25 deletions dkron/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ const (
ConcurrencyAllow = "allow"
// ConcurrencyForbid forbids a job from executing concurrency.
ConcurrencyForbid = "forbid"

// HashSymbol is the "magic" character used in scheduled to be replaced with a value based on job name
HashSymbol = "~"
)

var (
Expand Down Expand Up @@ -305,38 +308,58 @@ func (j *Job) GetTimeLocation() *time.Location {
return loc
}

// nameHash returns hash code of the job name
func (j *Job) nameHash() int {
hash := 0
for _, c := range j.Name {
hash += int(c)
}
return hash
}

// scheduleHash replaces H in the cron spec by a value derived from job Name
// such as "0 0 H * * *"
// such as "0 0 ~ * * *"
func (j *Job) scheduleHash() string {
spec := j.Schedule
if strings.Contains(spec, "H") && strings.Count(strings.TrimSpace(spec), " ") == 5 {
h := 0
for _, c := range j.Name {
h += int(c)

if !strings.Contains(spec, HashSymbol) {
return spec
}

hash := j.nameHash()
parts := strings.Split(spec, " ")
partIndex := 0
for index, part := range parts {
if strings.HasPrefix(part, "@") {
// this is a pre-defined scheduled, ignore everything
return spec
}
parts := strings.Split(spec, " ")
for index, part := range parts {
if strings.Contains(part, "H") {
// mods taken in accordance with https://dkron.io/docs/usage/cron-spec/#cron-expression-format
ph := h
switch index {
case 2:
ph %= 24
case 3:
ph = (ph % 31) + 1
case 4:
ph = (ph % 12) + 1
case 5:
ph %= 7
default:
ph %= 60
}
parts[index] = strings.ReplaceAll(part, "H", strconv.Itoa(ph))
if strings.HasPrefix(part, "TZ=") || strings.HasPrefix(part, "CRON_TZ=") {
// do not increase partIndex
continue
}

if strings.Contains(part, HashSymbol) {
// mods taken in accordance with https://dkron.io/docs/usage/cron-spec/#cron-expression-format
partHash := hash
switch partIndex {
case 2:
partHash %= 24
case 3:
partHash = (partHash % 28) + 1
case 4:
partHash = (partHash % 12) + 1
case 5:
partHash %= 7
default:
partHash %= 60
}
parts[index] = strings.ReplaceAll(part, HashSymbol, strconv.Itoa(partHash))
}
return strings.Join(parts, " ")

partIndex++
}
return spec
return strings.Join(parts, " ")
}

// GetNext returns the job's next schedule from now
Expand Down
12 changes: 12 additions & 0 deletions dkron/job_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,18 @@ func Test_isRunnable(t *testing.T) {
}
}

func Test_scheduleHash(t *testing.T) {
job := &Job{
Name: "test_job",
}
job.Schedule = "0 0 ~ * * *"
assert.Equal(t, "0 0 18 * * *", job.scheduleHash())
job.Schedule = "TZ=Europe/Madrid 0 0 1 * ~ *"
assert.Equal(t, "TZ=Europe/Madrid 0 0 1 * 7 *", job.scheduleHash())
job.Schedule = "TZ=Europe/Madrid @at something with ~"
assert.Equal(t, "TZ=Europe/Madrid @at something with ~", job.scheduleHash())
}

type gRPCClientMock struct {
}

Expand Down
16 changes: 10 additions & 6 deletions website/docs/usage/cron-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ A cron expression represents a set of times, using 6 space-separated fields.

Field name | Mandatory? | Allowed values | Allowed special characters
---------- | ---------- | -------------- | --------------------------
Seconds | Yes | 0-59 | * / , -
Minutes | Yes | 0-59 | * / , -
Hours | Yes | 0-23 | * / , -
Day of month | Yes | 1-31 | * / , - ?
Month | Yes | 1-12 or JAN-DEC | * / , -
Day of week | Yes | 0-6 or SUN-SAT | * / , - ?
Seconds | Yes | 0-59 | * / , - ~
Minutes | Yes | 0-59 | * / , - ~
Hours | Yes | 0-23 | * / , - ~
Day of month | Yes | 1-31 | * / , - ? ~
Month | Yes | 1-12 or JAN-DEC | * / , - ~
Day of week | Yes | 0-6 or SUN-SAT | * / , - ? ~

Note: Month and Day-of-week field values are case insensitive. "SUN", "Sun",
and "sun" are equally accepted.
Expand Down Expand Up @@ -51,6 +51,10 @@ Question mark ( ? )
Question mark may be used instead of '*' for leaving either day-of-month or
day-of-week blank.

Tilde ( ~ )

Tilde will be replaced by a numeric value valid for the range where it is used. It allows periodically scheduled tasks to produce even load on the system. For example, scheduling multiple hourly jobs to "0 ~ * * * *" rather than "0 0 * * * *" will run the jobs at different minutes of every hour. It can be thought of as a random value over a range, but it actually is a hash of the job name, not a random function, so that the value remains stable for any given job.

### Predefined schedules

You may use one of several pre-defined schedules in place of a cron expression.
Expand Down

0 comments on commit 5e02981

Please sign in to comment.