File: cron.go

package info (click to toggle)
golang-github-dsnet-golib 0.0~git20171103.1ea1667-1
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 240 kB
  • sloc: makefile: 2
file content (220 lines) | stat: -rw-r--r-- 5,987 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
// Copyright 2017, Joe Tsai. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE.md file.

// Package cron provides functionality for parsing and running cron schedules.
package cron

import (
	"context"
	"errors"
	"strconv"
	"strings"
	"time"
)

var (
	monthNames = map[string]int{
		"JAN": 1, "FEB": 2, "MAR": 3, "APR": 4, "MAY": 5, "JUN": 6,
		"JUL": 7, "AUG": 8, "SEP": 9, "OCT": 10, "NOV": 11, "DEC": 12,
	}
	dayNames = map[string]int{
		"SUN": 0, "MON": 1, "TUE": 2, "WED": 3, "THU": 4, "FRI": 5, "SAT": 6,
	}
	scheduleMacros = map[string]string{
		"@yearly":   "0 0 1 1 *",
		"@annually": "0 0 1 1 *",
		"@monthly":  "0 0 1 * *",
		"@weekly":   "0 0 * * 0",
		"@daily":    "0 0 * * *",
		"@hourly":   "0 * * * *",
	}
)

// set64 represents a set of integers containing values in 0..63.
type set64 uint64

func (s *set64) set(i int)     { *s |= 1 << uint(i) }
func (s set64) has(i int) bool { return s&(1<<uint(i)) != 0 }

// Schedule represents a cron schedule.
type Schedule struct {
	str      string
	mins     set64 // 0-59
	hours    set64 // 0-23
	days     set64 // 1-31
	months   set64 // 1-12
	weekDays set64 // 0-6
}

// ParseSchedule parses a cron schedule, which is a space-separated list of
// five fields representing:
//	• minutes:       0-59
//	• hours:         0-23
//	• days of month: 1-31
//	• months:        1-12 or JAN-DEC
//	• days of week:  0-6 or SUN-SAT
//
// Each field may be a glob (e.g., "*"), representing the full range of values,
// or a comma-separated list, containing individual values (e.g., "JAN")
// or a dash-separated pair representing a range of values (e.g., "MON-FRI").
//
// The following macros are permitted:
//	• @yearly:   "0 0 1 1 *"
//	• @annually: "0 0 1 1 *"
//	• @monthly:  "0 0 1 * *"
//	• @weekly:   "0 0 * * 0"
//	• @daily:    "0 0 * * *"
//	• @hourly:   "0 * * * *"
//
// A given timestamp is in the schedule if the associated fields
// of the timestamp matches each field specified in the schedule.
//
// See https://wikipedia.org/wiki/cron
func ParseSchedule(s string) (Schedule, error) {
	s = strings.Join(strings.Fields(s), " ")
	sch := Schedule{str: s}
	if scheduleMacros[s] != "" {
		s = scheduleMacros[s]
	}
	var ok [5]bool
	if ss := strings.Fields(s); len(ss) == 5 {
		sch.mins, ok[0] = parseField(ss[0], 0, 59, nil)
		sch.hours, ok[1] = parseField(ss[1], 0, 23, nil)
		sch.days, ok[2] = parseField(ss[2], 1, 31, nil)
		sch.months, ok[3] = parseField(ss[3], 1, 12, monthNames)
		sch.weekDays, ok[4] = parseField(ss[4], 0, 6, dayNames)
	}
	if ok != [5]bool{true, true, true, true, true} {
		return Schedule{}, errors.New("cron: invalid schedule: " + s)
	}
	return sch, nil
}

func parseField(s string, min, max int, aliases map[string]int) (set64, bool) {
	var m set64
	for _, s := range strings.Split(s, ",") {
		var lo, hi int
		if i := strings.IndexByte(s, '-'); i >= 0 {
			lo = parseToken(s[:i], min, aliases)
			hi = parseToken(s[i+1:], max, aliases)
		} else {
			lo = parseToken(s, min, aliases)
			hi = parseToken(s, max, aliases)
		}
		if lo < min || max < hi || hi < lo {
			return m, false
		}
		for i := lo; i <= hi; i++ {
			m.set(i)
		}
	}
	return m, true
}

func parseToken(s string, wild int, aliases map[string]int) int {
	if n, ok := aliases[strings.ToUpper(s)]; ok {
		return n
	}
	if s == "*" {
		return wild
	}
	if n, err := strconv.Atoi(s); err == nil {
		return n
	}
	return -1
}

// NextAfter returns the next scheduled event, relative to the specified t,
// taking into account t.Location.
// This returns the zero value if unable to determine the next scheduled event.
func (s Schedule) NextAfter(t time.Time) time.Time {
	if s == (Schedule{}) {
		return time.Time{}
	}

	// Round-up to the nearest minute.
	t = t.Add(time.Minute).Truncate(time.Minute)

	// Increment min/hour first, then increment by days.
	// When incrementing by days, we do not need to verify the min/hour again.
	t100 := t.AddDate(100, 0, 0) // Sanity bounds of 100 years
	for !s.mins.has(t.Minute()) && t.Before(t100) {
		t = t.Add(time.Minute)
	}
	for !s.hours.has(t.Hour()) && t.Before(t100) {
		t = t.Add(time.Hour)
	}
	for !s.matchDate(t) && t.Before(t100) {
		t = t.AddDate(0, 0, 1)
	}

	// Check that the date truly matches.
	if !s.mins.has(t.Minute()) || !s.hours.has(t.Hour()) || !s.matchDate(t) {
		return time.Time{}
	}
	return t
}

func (s Schedule) matchDate(t time.Time) bool {
	return s.days.has(t.Day()) && s.months.has(int(t.Month())) && s.weekDays.has(int(t.Weekday()))
}

func (s Schedule) String() string {
	return s.str
}

// A Cron holds a channel that delivers events based on the cron schedule.
type Cron struct {
	C <-chan time.Time // The channel on which events are delivered

	cancel context.CancelFunc
}

// NewCron returns a new Cron containing a channel that sends the time
// at every moment specified by the Schedule.
// The timezone the cron job is operating in must be specified.
// Stop Cron to release associated resources.
func NewCron(sch Schedule, tz *time.Location) *Cron {
	if tz == nil {
		panic("cron: unspecified time.Location; consider using time.Local")
	}
	ch := make(chan time.Time, 1)
	ctx, cancel := context.WithCancel(context.Background())

	// Start monitor goroutine.
	go func() {
		timer := time.NewTimer(0)
		<-timer.C
		for {
			// Schedule the next firing.
			now := time.Now().In(tz)
			next := sch.NextAfter(now)
			if next.IsZero() {
				return
			}
			timer.Reset(next.Sub(now))

			// Wait until either stopped or triggered.
			select {
			case <-ctx.Done():
				timer.Stop()
				return
			case t := <-timer.C:
				// Best-effort at forwarding the signal.
				select {
				case ch <- t:
				default:
				}
			}
		}
	}()
	return &Cron{C: ch, cancel: cancel}
}

// Stop turns off the cron job. After Stop, no more events will be sent.
// Stop does not close the channel, to prevent a read from the channel
// succeeding incorrectly.
func (c *Cron) Stop() {
	c.cancel()
}