Skip to content

Commit

Permalink
Add Jitter to "Update Go" action schedule
Browse files Browse the repository at this point in the history
Fixes #966
  • Loading branch information
phil9909 committed Jan 20, 2023
1 parent b37efd6 commit 55b1f52
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 2 deletions.
30 changes: 30 additions & 0 deletions octo/jitter/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package jitter_test

import (
"testing"

"github.com/sclevine/spec"
"github.com/sclevine/spec/report"
)

func TestUnit(t *testing.T) {
suite := spec.New("jitter", spec.Report(report.Terminal{}))
suite("Tests", testJitter)
suite.Run(t)
}
64 changes: 64 additions & 0 deletions octo/jitter/jitter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package jitter

import (
"crypto/md5"
"encoding/binary"
"math/rand"
"strconv"

"github.com/paketo-buildpacks/pipeline-builder/octo/actions/event"
)

type Jitterer struct {
rng *rand.Rand
}

func New(seedString string) Jitterer {
// use a string to calculate a deterministic seed for the random number generator
// the actual implementation does not really matter, we just need to condense a string into an 64-bit number
// also: cryptocraphic security is not important here, we just don't want all cron jobs to run at the same time
sum := md5.Sum([]byte(seedString))
seed := binary.LittleEndian.Uint64(sum[0:8]) ^ binary.BigEndian.Uint64(sum[8:16])
return Jitterer{
rng: rand.New(rand.NewSource(int64(seed))),
}
}

func (j Jitterer) jitter(min, max int) string {
return strconv.Itoa(min + j.rng.Intn(max-min+1))
}

func (j Jitterer) Jitter(cron event.Cron) event.Cron {
if cron.Minute == "" {
cron.Minute = j.jitter(0, 59)
}
if cron.Hour == "" {
cron.Hour = j.jitter(0, 23)
}
if cron.DayOfMonth == "" {
cron.DayOfMonth = j.jitter(1, 28)
}
if cron.Month == "" {
cron.Month = j.jitter(1, 12)
}
if cron.DayOfWeek == "" {
cron.DayOfWeek = j.jitter(0, 6)
}
return cron
}
79 changes: 79 additions & 0 deletions octo/jitter/jitter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2018-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package jitter_test

import (
"testing"

. "github.com/onsi/gomega"
"github.com/paketo-buildpacks/pipeline-builder/octo/actions/event"
"github.com/paketo-buildpacks/pipeline-builder/octo/jitter"
"github.com/sclevine/spec"
)

func testJitter(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
)

context("Jitterer", func() {
it("jittering should change unset values", func() {
jitterer := jitter.New("my-seed")
jittered := jitterer.Jitter(event.Cron{})

Expect(jittered.DayOfMonth).NotTo(BeEmpty())
Expect(jittered.DayOfWeek).NotTo(BeEmpty())
Expect(jittered.Hour).NotTo(BeEmpty())
Expect(jittered.Minute).NotTo(BeEmpty())
Expect(jittered.Month).NotTo(BeEmpty())
})

it("jittering should not change set values", func() {
jitterer := jitter.New("my-seed")
jittered := jitterer.Jitter(event.Cron{
DayOfMonth: "42",
DayOfWeek: "42",
Hour: "42",
Minute: "42",
Month: "42",
})

Expect(jittered.DayOfMonth).To(Equal("42"))
Expect(jittered.DayOfWeek).To(Equal("42"))
Expect(jittered.Hour).To(Equal("42"))
Expect(jittered.Minute).To(Equal("42"))
Expect(jittered.Month).To(Equal("42"))
})

it("jittering with the same seed should produce the same result", func() {
jitterer := jitter.New("my-seed")
jitteredA := jitterer.Jitter(event.Cron{})
jitterer = jitter.New("my-seed")
jitteredB := jitterer.Jitter(event.Cron{})
Expect(jitteredA).To(Equal(jitteredB))
})

it("jittering with different seeds should produce a different result (with high probability)", func() {
jitterer := jitter.New("my-seed")
jitteredA := jitterer.Jitter(event.Cron{})
jitterer = jitter.New("my-other-seed")
jitteredB := jitterer.Jitter(event.Cron{})
Expect(jitteredA).NotTo(Equal(jitteredB))
})

})
}
10 changes: 8 additions & 2 deletions octo/update_go.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2018-2020 the original author or authors.
* Copyright 2018-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,6 +22,7 @@ import (

"github.com/paketo-buildpacks/pipeline-builder/octo/actions"
"github.com/paketo-buildpacks/pipeline-builder/octo/actions/event"
"github.com/paketo-buildpacks/pipeline-builder/octo/jitter"
)

func ContributeUpdateGo(descriptor Descriptor) (*Contribution, error) {
Expand All @@ -41,10 +42,15 @@ func ContributeUpdateGo(descriptor Descriptor) (*Contribution, error) {
return nil, nil
}

seed := fmt.Sprintf("Update Go %s", os.Getenv("GITHUB_REPOSITORY"))
cron := jitter.
New(seed).
Jitter(event.Cron{Hour: "2", DayOfWeek: "1", Month: "*", DayOfMonth: "*"})

w := actions.Workflow{
Name: "Update Go",
On: map[event.Type]event.Event{
event.ScheduleType: event.Schedule{{Minute: "0", Hour: "2", DayOfWeek: "1"}},
event.ScheduleType: event.Schedule{cron},
event.WorkflowDispatchType: event.WorkflowDispatch{},
},
Jobs: map[string]actions.Job{
Expand Down

0 comments on commit 55b1f52

Please sign in to comment.