Verified Commit 31646e53 authored by Tomasz Maczukin's avatar Tomasz Maczukin

Merge branch 'feature/off-peak-timezone' into 'master'

Add timezone support for OffPeak intervals

Closes #1826

See merge request !479
parents 611ca268 d2fa7a51
......@@ -81,6 +81,7 @@ type DockerMachine struct {
MachineOptions []string `long:"machine-options" env:"MACHINE_OPTIONS" description:"Additional machine creation options"`
OffPeakPeriods []string `long:"off-peak-periods" env:"MACHINE_OFF_PEAK_PERIODS" description:"Time periods when the scheduler is in the OffPeak mode"`
OffPeakTimezone string `long:"off-peak-timezone" env:"MACHINE_OFF_PEAK_TIMEZONE" description:"Timezone for the OffPeak periods (defaults to Local)"`
OffPeakIdleCount int `long:"off-peak-idle-count" env:"MACHINE_OFF_PEAK_IDLE_COUNT" description:"Maximum idle machines when the scheduler is in the OffPeak mode"`
OffPeakIdleTime int `long:"off-peak-idle-time" env:"MACHINE_OFF_PEAK_IDLE_TIME" description:"Minimum time after machine can be destroyed when the scheduler is in the OffPeak mode"`
......@@ -265,7 +266,7 @@ func (c *DockerMachine) isOffPeak() bool {
}
func (c *DockerMachine) CompileOffPeakPeriods() (err error) {
c.offPeakTimePeriods, err = timeperiod.TimePeriods(c.OffPeakPeriods)
c.offPeakTimePeriods, err = timeperiod.TimePeriods(c.OffPeakPeriods, c.OffPeakTimezone)
if err != nil {
err = errors.New(fmt.Sprint("Invalid OffPeakPeriods value: ", err))
}
......
......@@ -369,7 +369,8 @@ found in the separate [runners autoscale documentation](autoscale.md).
|---------------------|-------------|
| `IdleCount` | Number of machines, that need to be created and waiting in _Idle_ state. |
| `IdleTime` | Time (in seconds) for machine to be in _Idle_ state before it is removed. |
| `OffPeakPeriods` | Time periods when the scheduler is in the OffPeak mode. An array of cron-style patterns (described below) |
| `OffPeakPeriods` | Time periods when the scheduler is in the OffPeak mode. An array of cron-style patterns (described below). |
| `OffPeakTimezone` | Time zone for the times given in OffPeakPeriods. A timezone string like Europe/Berlin (defaults to the locale system setting of the host if omitted or empty). |
| `OffPeakIdleCount` | Like `IdleCount`, but for _Off Peak_ time periods. |
| `OffPeakIdleTime` | Like `IdleTime`, but for _Off Peak_ time mperiods. |
| `MaxBuilds` | Builds count after which machine will be removed. |
......@@ -387,6 +388,7 @@ Example:
"* * 0-10,18-23 * * mon-fri *",
"* * * * * sat,sun *"
]
OffPeakTimezone = "Europe/Berlin"
OffPeakIdleCount = 1
OffPeakIdleTime = 3600
MaxBuilds = 100
......
......@@ -276,10 +276,10 @@ periods.
**How it is working?**
Configuration of _Off Peak_ is done by three parameters: `OffPeakPeriods`,
`OffPeakIdleCount` and `OffPeakIdleTime`. The `OffPeakPeriods` setting
contains an array of cron-style patterns defining when the _Off Peak_ time
mode should be set on. For example:
Configuration of _Off Peak_ is done by four parameters: `OffPeakPeriods`,
`OffPeakIdleCount`, `OffPeakIdleCount` and `OffPeakIdleTime`. The
`OffPeakPeriods` setting contains an array of cron-style patterns defining
when the _Off Peak_ time mode should be set on. For example:
```toml
[runners.machine]
......@@ -294,6 +294,10 @@ from 12am to 9am and from 6pm to 11pm and whole weekend days. Machines
scheduler is checking all patterns from the array and if at least one of
them describes current time, then the _Off Peak_ time mode is enabled.
You can specify the `OffPeakTimezone` e.g. `"Australia/Sydney"`. If you don't,
the system setting of the host machine of every runner will be used. This
default can be stated as `OffPeakTimezone = "Local"` explicitly if you wish.
When the _Off Peak_ time mode is enabled machines scheduler use
`OffPeakIdleCount` instead of `IdleCount` setting and `OffPeakIdleTime`
instead of `IdleTime` setting. The autoscaling algorithm is not changed,
......
......@@ -8,14 +8,16 @@ import (
type TimePeriod struct {
expressions []*cronexpr.Expression
location *time.Location
GetCurrentTime func() time.Time
}
func (t *TimePeriod) InPeriod() bool {
now := t.GetCurrentTime().In(t.location)
for _, expression := range t.expressions {
nextIn := expression.Next(t.GetCurrentTime())
timeSince := time.Since(nextIn)
if timeSince < time.Second && timeSince > -time.Second {
nextIn := expression.Next(now)
timeSince := now.Sub(nextIn)
if -time.Second <= timeSince && timeSince <= time.Second {
return true
}
}
......@@ -23,7 +25,7 @@ func (t *TimePeriod) InPeriod() bool {
return false
}
func TimePeriods(periods []string) (*TimePeriod, error) {
func TimePeriods(periods []string, timezone string) (*TimePeriod, error) {
var expressions []*cronexpr.Expression
for _, period := range periods {
......@@ -35,8 +37,18 @@ func TimePeriods(periods []string) (*TimePeriod, error) {
expressions = append(expressions, expression)
}
// if not set, default to system setting (the empty string would mean UTC)
if timezone == "" {
timezone = "Local"
}
location, err := time.LoadLocation(timezone)
if err != nil {
return nil, err
}
timePeriod := &TimePeriod{
expressions: expressions,
location: location,
GetCurrentTime: func() time.Time { return time.Now() },
}
......
......@@ -19,14 +19,13 @@ var daysOfWeek = map[time.Weekday]string{
}
func newTimePeriods(t *testing.T) (time.Time, *TimePeriod) {
now := time.Now()
minute := now.Minute()
hour := now.Hour()
day := now.Weekday()
dayName := daysOfWeek[day]
periodPattern := fmt.Sprintf("* %d %d * * %s *", minute, hour, dayName)
timePeriods, err := TimePeriods([]string{periodPattern})
location, _ := time.LoadLocation("Local")
now := time.Date(2017, time.February, 21, 14, 0, 0, 0, location)
dayName := daysOfWeek[now.Weekday()]
periodPattern := fmt.Sprintf("* 0 14 * * %s *", dayName)
timePeriods, err := TimePeriods([]string{periodPattern}, location.String())
assert.NoError(t, err)
return now, timePeriods
......@@ -37,7 +36,6 @@ func TestInPeriod(t *testing.T) {
timePeriods.GetCurrentTime = func() time.Time {
return now
}
assert.True(t, timePeriods.InPeriod())
}
......@@ -60,3 +58,42 @@ func TestPeriodOut(t *testing.T) {
}
assert.False(t, timePeriods.InPeriod())
}
func TestInvalidTimezone(t *testing.T) {
_, err := TimePeriods([]string{}, "InvalidTimezone/String")
assert.Error(t, err)
}
func testTimeperiodsWithTimezone(t *testing.T, period, timezone string, month time.Month, day, hour, minute int, inPeriod bool) {
timePeriods, _ := TimePeriods([]string{period}, timezone)
timePeriods.GetCurrentTime = func() time.Time {
return time.Date(2017, month, day, hour, minute, 0, 0, time.UTC)
}
now := timePeriods.GetCurrentTime()
nowInLocation := now.In(timePeriods.location)
t.Log(fmt.Sprintf("Checking timeperiod '%s' in timezone '%s' for %s (%s)", period, timezone, now, nowInLocation))
if inPeriod {
assert.True(t, timePeriods.InPeriod(), "It should be inside of the period")
} else {
assert.False(t, timePeriods.InPeriod(), "It should be outside of the period")
}
}
func TestTimeperiodsWithTimezone(t *testing.T) {
period := "* * 10-17 * * * *"
timezone := "Europe/Berlin"
// inside or outside of the timeperiod, basing on DST status
testTimeperiodsWithTimezone(t, period, timezone, time.January, 1, 16, 30, true)
testTimeperiodsWithTimezone(t, period, timezone, time.July, 1, 16, 30, false)
// always inside of the timeperiod
testTimeperiodsWithTimezone(t, period, timezone, time.January, 1, 14, 30, true)
testTimeperiodsWithTimezone(t, period, timezone, time.July, 1, 14, 30, true)
// always outside of the timeperiod
testTimeperiodsWithTimezone(t, period, timezone, time.January, 1, 20, 30, false)
testTimeperiodsWithTimezone(t, period, timezone, time.July, 1, 20, 30, false)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment