Files
Alex ShtinandGitHub 8e8f1b0f7c Remove license header from every file (#7689)
## What changed?
<!-- Describe what has changed in this PR -->
Remove license header from every file. Because it is really hard to
follow in this PR here is the summary:
1. License header is removed from all `.go` and `.proto` files
:fireworks::fireworks:🎆.
2. `LICENSE` file in the root directory has only Temporal and Uber
copyrights.
3. 5 other `LICENSE` files added to the packages which have copyrights
different from Temporal and Uber: Datadog, Xargin, "Mat Ryer, Tyler
Bunnell and contributors".
4. `license_file` flag is removed from all code generation tools.
5. `copyright_file` flag is removed from `go:generate mockgen`
directive.
6. All copyright related targets are removed from `Makefile`.
7. Updated Temporal copyright year to 2025 everywhere.

## Why?
<!-- Tell your future self why have you made these changes -->
I double checked with legal department that it is not needed to have
license header in every file. One file per repo is enough. I put all
copyrights to the root `LICENSE` file and removed header from all other
files. Also updated tools and `Makefile`.
2025-05-01 18:50:21 -07:00

535 lines
14 KiB
Go

package scheduler
import (
"errors"
"fmt"
"strconv"
"strings"
"time"
schedulepb "go.temporal.io/api/schedule/v1"
"go.temporal.io/server/common/primitives/timestamp"
"google.golang.org/protobuf/types/known/durationpb"
)
type (
parseMode int
compiledCalendar struct {
// Time zone that this calendar spec is interpreted in
tz *time.Location
// Matching predicates for each field (two for date). As in Go's time
// library, months start on 1 == January, day of month starts at 1, day
// of week starts at 0 == Sunday. A time matches this compiled calendar
// when all fields match.
year, month, dayOfMonth, dayOfWeek, hour, minute, second func(int) bool
}
)
const (
// minCalendarYear is the smallest year that can appear in a calendar spec.
minCalendarYear = 2000
// maxCalendarYear is the latest year that will be recognized for calendar dates.
// If you're still using Temporal in 2100 please change this constant and rebuild.
maxCalendarYear = 2100
// max length of one calendar comment field
maxCommentLen = 200
)
const (
// Modes for parsing range strings: all modes accept decimal integers
parseModeInt parseMode = iota
// parseModeYear is like parseModeInt but returns an empty range for the default
parseModeYear
// parseModeMonth also accepts month name prefixes (at least three letters)
parseModeMonth
// parseModeDow also accepts day-of-week prefixes (at least two letters)
parseModeDow
)
var (
errOutOfRange = errors.New("out of range")
errConflictingTimezoneNames = errors.New("conflicting timezone names")
monthStrings = []string{
"january",
"february",
"march",
"april",
"may",
"june",
"july",
"august",
"september",
"october",
"november",
"december",
}
dowStrings = []string{
"sunday",
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
}
)
func newCompiledCalendar(cal *schedulepb.StructuredCalendarSpec, tz *time.Location) *compiledCalendar {
return &compiledCalendar{
tz: tz,
year: makeYearMatcher(cal.Year),
month: makeBitMatcher(cal.Month),
dayOfMonth: makeBitMatcher(cal.DayOfMonth),
dayOfWeek: makeBitMatcher(cal.DayOfWeek),
hour: makeBitMatcher(cal.Hour),
minute: makeBitMatcher(cal.Minute),
second: makeBitMatcher(cal.Second),
}
}
// Returns true if the given time matches this calendar spec.
func (cc *compiledCalendar) matches(ts time.Time) bool {
// set time zone
ts = ts.In(cc.tz)
// get ymdhms from ts
y, mo, d := ts.Date()
h, m, s := ts.Clock()
return cc.year(y) &&
cc.month(int(mo)) &&
cc.dayOfMonth(d) &&
cc.dayOfWeek(int(ts.Weekday())) &&
cc.hour(h) &&
cc.minute(m) &&
cc.second(s)
}
// Returns the earliest time that matches this calendar spec that is after the given time.
// All times are considered to have 1 second resolution.
func (cc *compiledCalendar) next(ts time.Time) time.Time {
// set time zone
ts = ts.In(cc.tz)
// get ymdhms from ts
y, mo, d := ts.Date()
h, m, s := ts.Clock()
dstoffset := 0 * time.Hour
if ts.Add(-time.Hour).Hour() == h {
// we're in the second copy of a dst repeated hour
dstoffset = 1 * time.Hour
}
// looking for first matching time after ts, so add 1 second
s++
Outer:
for {
// normalize after carries
if s >= 60 {
m, s = m+1, 0
}
if m >= 60 {
prev := time.Date(y, mo, d, h, 0, 0, 0, cc.tz)
h, m = h+1, 0
next := time.Date(y, mo, d, h, 0, 0, 0, cc.tz)
// if we moved to the next hour but it's two hours later, then we skipped over
// a dst repeated hour. try it again with an offset.
if dstoffset == 0 && next.Sub(prev) > time.Hour {
h = h - 1
dstoffset = 1 * time.Hour
} else {
dstoffset = 0
}
}
if h >= 24 {
d, h = d+1, 0
}
if d > daysInMonth(mo, y) {
mo, d = mo+1, 1
}
if mo > time.December {
y, mo = y+1, time.January
}
if y > maxCalendarYear {
break Outer
}
// try to match year, month, etc. from outside in
if !cc.year(y) {
y, mo, d, h, m, s = y+1, time.January, 1, 0, 0, 0
dstoffset = 0
continue Outer
}
for !cc.month(int(mo)) {
mo, d, h, m, s = mo+1, 1, 0, 0, 0
dstoffset = 0
if mo > time.December {
continue Outer
}
}
for !cc.dayOfMonth(d) || !cc.dayOfWeek(int(time.Date(y, mo, d, h, m, s, 0, cc.tz).Weekday())) {
d, h, m, s = d+1, 0, 0, 0
dstoffset = 0
if d > daysInMonth(mo, y) {
continue Outer
}
}
for !cc.hour(h) {
h, m, s = h+1, 0, 0
dstoffset = 0
if h >= 24 {
continue Outer
}
}
for !cc.minute(m) {
m, s = m+1, 0
if m >= 60 {
continue Outer
}
}
for !cc.second(s) {
s = s + 1
if s >= 60 {
continue Outer
}
}
// everything matches
nextTs := time.Date(y, mo, d, h, m, s, 0, cc.tz)
// we might have reached a nonexistent time that got jumped over by a dst transition.
// we can tell if the hour is different from what we think it should be.
if nextTs.Hour() != h {
h, m, s = h+1, 0, 0
continue Outer
}
return nextTs.Add(dstoffset)
}
// no more matching times (up to max we checked)
return time.Time{}
}
func parseCalendarToStructured(cal *schedulepb.CalendarSpec) (*schedulepb.StructuredCalendarSpec, error) {
var errs []string
makeRangeOrNil := func(s, field, def string, minVal, maxVal int, parseMode parseMode) []*schedulepb.Range {
r, err := makeRange(s, field, def, minVal, maxVal, parseMode)
if err != nil {
errs = append(errs, err.Error())
}
return r
}
ss := &schedulepb.StructuredCalendarSpec{
Second: makeRangeOrNil(cal.Second, "Second", "0", 0, 59, parseModeInt),
Minute: makeRangeOrNil(cal.Minute, "Minute", "0", 0, 59, parseModeInt),
Hour: makeRangeOrNil(cal.Hour, "Hour", "0", 0, 23, parseModeInt),
DayOfWeek: makeRangeOrNil(cal.DayOfWeek, "DayOfWeek", "*", 0, 7, parseModeDow),
DayOfMonth: makeRangeOrNil(cal.DayOfMonth, "DayOfMonth", "*", 1, 31, parseModeInt),
Month: makeRangeOrNil(cal.Month, "Month", "*", 1, 12, parseModeMonth),
Year: makeRangeOrNil(cal.Year, "Year", "*", minCalendarYear, maxCalendarYear, parseModeYear),
Comment: cal.Comment,
}
if len(errs) > 0 {
return nil, errors.New(strings.Join(errs, ", "))
}
return ss, nil
}
func parseCronString(c string) (*schedulepb.StructuredCalendarSpec, *schedulepb.IntervalSpec, string, error) {
var tzName string
var comment string
c = strings.TrimSpace(c)
// split out timezone
if strings.HasPrefix(c, "TZ=") || strings.HasPrefix(c, "CRON_TZ=") {
tz, rest, found := strings.Cut(c, " ")
if !found {
return nil, nil, "", errors.New("CronString has time zone but missing fields")
}
c = rest
_, tzName, _ = strings.Cut(tz, "=")
}
// split out comment
c, comment, _ = strings.Cut(c, "#")
c = strings.TrimSpace(c)
comment = strings.TrimSpace(comment)
// handle @every intervals
if strings.HasPrefix(c, "@every") {
iv, err := parseCronStringInterval(c)
return nil, iv, "", err
}
// handle @hourly, etc.
c = handlePredefinedCronStrings(c)
// split fields
cal := schedulepb.CalendarSpec{Comment: comment}
fields := strings.Fields(c)
switch len(fields) {
case 5:
cal.Minute, cal.Hour, cal.DayOfMonth, cal.Month, cal.DayOfWeek = fields[0], fields[1], fields[2], fields[3], fields[4]
case 6:
cal.Minute, cal.Hour, cal.DayOfMonth, cal.Month, cal.DayOfWeek, cal.Year = fields[0], fields[1], fields[2], fields[3], fields[4], fields[5]
case 7:
cal.Second, cal.Minute, cal.Hour, cal.DayOfMonth, cal.Month, cal.DayOfWeek, cal.Year = fields[0], fields[1], fields[2], fields[3], fields[4], fields[5], fields[6]
default:
return nil, nil, "", errors.New("CronString does not have 5-7 fields")
}
structured, err := parseCalendarToStructured(&cal)
if err != nil {
return nil, nil, "", err
}
return structured, nil, tzName, nil
}
func parseCronStringInterval(c string) (*schedulepb.IntervalSpec, error) {
// split after @every
_, interval, found := strings.Cut(c, " ")
if !found {
return nil, errors.New("CronString does not have interval after @every")
}
// allow @every 14h/3h
interval, phase, _ := strings.Cut(interval, "/")
intervalDuration, err := timestamp.ParseDuration(interval)
if err != nil {
return nil, err
}
if phase == "" {
return &schedulepb.IntervalSpec{Interval: durationpb.New(intervalDuration)}, nil
}
phaseDuration, err := timestamp.ParseDuration(phase)
if err != nil {
return nil, err
}
return &schedulepb.IntervalSpec{Interval: durationpb.New(intervalDuration), Phase: durationpb.New(phaseDuration)}, nil
}
func handlePredefinedCronStrings(c string) string {
switch c {
case "@yearly", "@annually":
return "0 0 1 1 *"
case "@monthly":
return "0 0 1 * *"
case "@weekly":
return "0 0 * * 0"
case "@daily", "@midnight":
return "0 0 * * *"
case "@hourly":
return "0 * * * *"
default:
return c
}
}
func makeBitMatcher(ranges []*schedulepb.Range) func(int) bool {
var bits uint64
add := func(i int) { bits |= 1 << i }
iterateRanges(ranges, add)
return func(v int) bool { return (1<<v)&bits != 0 }
}
func makeYearMatcher(ranges []*schedulepb.Range) func(int) bool {
if len(ranges) == 0 {
// special case for year: all is represented as empty range list
return func(int) bool { return true }
}
var values []int16
add := func(i int) { values = append(values, int16(i)) }
iterateRanges(ranges, add)
return func(v int) bool {
for _, value := range values {
if int(value) == v {
return true
}
}
return false
}
}
func iterateRanges(ranges []*schedulepb.Range, f func(i int)) {
for _, r := range ranges {
start, end, step := int(r.GetStart()), int(r.GetEnd()), int(r.GetStep())
if step == 0 {
step = 1
}
if end < start {
end = start
}
for ; start <= end; start += step {
f(start)
}
}
}
// Parses the string into a Range.
// Accepts strings of the form:
//
// - * matches always
// - x matches when the field equals x
// - x-z matches when the field is between x and z inclusive
// - x-z/y matches when the field is between x and z inclusive, skipping by y
// - x/y matches when the field is between x and max inclusive, skipping by y
// - j,k,l matches when the field is one of the listed values/ranges
//
// Each comma-separated value can be a range, and any range can have a skip value, e.g.:
//
// - 1-5 matches 1,2,3,4,5
// - 1-5/2 matches 1,3,5
// - 3/5 matches 3,8,13,18,23,28 (assuming max=30)
// - 1-5/2,8 matches 1,3,5,8
// - 1-5/2,8-11 matches 1,3,5,8,9,10,11
// - 1-5/2,8-16/3,2 matches 1,2,3,5,8,11,14
//
// Calls f for all values that should be considered matching. Values don't have to appear
// in order, and f may be called out of order as well.
// Handles day-of-week names or month names according to parseMode.
// min and max are the complete range of expected values.
//
//revive:disable-next-line:cognitive-complexity
func makeRange(s, field, def string, minVal, maxVal int, parseMode parseMode) ([]*schedulepb.Range, error) {
s = strings.TrimSpace(s)
if s == "" {
s = def
}
if s == "*" && parseMode == parseModeYear {
return nil, nil // special case for year: all is represented as empty range list
}
var ranges []*schedulepb.Range
for _, part := range strings.Split(s, ",") {
var err error
step := 1
hasStep := false
if strings.Contains(part, "/") {
skipParts := strings.Split(part, "/")
if len(skipParts) != 2 {
return nil, fmt.Errorf("%s has too many slashes", field)
}
part = skipParts[0]
step, err = strconv.Atoi(skipParts[1])
if err != nil {
return nil, err
}
if step < 1 {
return nil, fmt.Errorf("%s has invalid Step", field)
}
hasStep = true
}
start, end := minVal, maxVal
if part != "*" {
if strings.Contains(part, "-") {
rangeParts := strings.Split(part, "-")
if len(rangeParts) != 2 {
return nil, fmt.Errorf("%s has too many dashes", field)
}
if start, err = parseValue(rangeParts[0], minVal, maxVal, parseMode); err != nil {
return nil, fmt.Errorf("%s Start is not in range [%d-%d]", field, minVal, maxVal)
}
if end, err = parseValue(rangeParts[1], start, maxVal, parseMode); err != nil {
return nil, fmt.Errorf("%s End is before Start or not in range [%d-%d]", field, minVal, maxVal)
}
} else {
if start, err = parseValue(part, minVal, maxVal, parseMode); err != nil {
return nil, fmt.Errorf("%s is not in range [%d-%d]", field, minVal, maxVal)
}
if !hasStep {
// if / is present, a single value is treated as that value to the
// end. otherwise a single value is just the single value.
end = start
}
}
}
// Special handling for Sunday: Turn "7" into "0", which may require an extra range.
// Consider some cases:
// 0-7 or 1-7 can turn into 0-6
// 3-7 has to turn into 0,3-6
// 3-7/3 can turn into 3-6/3 (7 doesn't match)
// 1-7/2 has to turn into 0,1-6/2
// That is, we can use a single range and just turn the 7 into a 6 only if step == 1
// and start == 0 or 1. Or if 7 isn't actually included. In other cases, we can add a
// 0, and then turn the 7 into a 6 in whatever the original range was. If the original
// range was just 7-7, then we're done.
if parseMode == parseModeDow && end == 7 {
if (7-start)%step == 0 && (step > 1 || step == 1 && start > 1) {
ranges = append(ranges, &schedulepb.Range{Start: int32(0)})
if start == 7 {
continue
}
}
end = 6
}
if start == end {
end = 0 // use default value so proto is smaller
}
if step == 1 {
step = 0 // use default value so proto is smaller
}
ranges = append(ranges, &schedulepb.Range{Start: int32(start), End: int32(end), Step: int32(step)})
}
return ranges, nil
}
// Parses a single value (integer or day-of-week or month name).
func parseValue(s string, min, max int, parseMode parseMode) (int, error) {
if parseMode == parseModeMonth {
if len(s) >= 3 {
s = strings.ToLower(s)
for i, month := range monthStrings {
if strings.HasPrefix(month, s) {
i++
if i < min || i > max {
return i, errOutOfRange
}
return i, nil
}
}
}
} else if parseMode == parseModeDow {
if len(s) >= 2 {
s = strings.ToLower(s)
for i, dow := range dowStrings {
if strings.HasPrefix(dow, s) {
if i < min || i > max {
return i, errOutOfRange
}
return i, nil
}
}
}
}
i, err := strconv.Atoi(s)
if err != nil {
return i, err
}
if i < min || i > max {
return i, errOutOfRange
}
return i, nil
}
// same as Go's version
func isLeapYear(y int) bool {
return y%4 == 0 && (y%100 != 0 || y%400 == 0)
}
func daysInMonth(m time.Month, y int) int {
if m == time.February {
if isLeapYear(y) {
return 29
} else {
return 28
}
}
const bits = 0b1010110101010
return 30 + (bits>>m)&1
}