Skip to content

Commit

Permalink
added heartbeat tests
Browse files Browse the repository at this point in the history
  • Loading branch information
gferraro committed Nov 24, 2021
1 parent 3701974 commit 74facfc
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 23 deletions.
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/alexflint/go-arg v1.4.2
github.com/c9s/goprocinfo v0.0.0-20190309065803-0b2ad9ac246b
github.com/godbus/dbus v4.1.0+incompatible
github.com/stretchr/testify v1.7.0
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1
periph.io/x/periph v3.6.8+incompatible
)
Expand All @@ -19,13 +20,15 @@ require (
github.com/TheCacophonyProject/lepton3 v0.0.0-20211005194419-22311c15d6ee // indirect
github.com/alexflint/go-scalar v1.1.0 // indirect
github.com/boltdb/bolt v1.3.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mitchellh/mapstructure v1.4.2 // indirect
github.com/nathan-osman/go-sunrise v0.0.0-20171121204956-7c449e7c690b // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
Expand All @@ -37,6 +40,7 @@ require (
gopkg.in/ini.v1 v1.64.0 // indirect
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)

replace periph.io/x/periph => github.com/TheCacophonyProject/periph v2.1.1-0.20200615222341-6834cd5be8c1+incompatible
71 changes: 48 additions & 23 deletions heartbeat.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,40 @@ const attemptDelay = 5 * time.Second
const heartBeatDelay = 30 * time.Minute

type Heartbeat struct {
api *api.CacophonyAPI
window *window.Window
nextEvent time.Time
end time.Time
api *api.CacophonyAPI
window *window.Window
nextEvent time.Time
end time.Time
penultimate bool
}

func heartBeatLoop(window *window.Window) {
// Used to test
type Clock interface {
Sleep(d time.Duration)
Now() time.Time
}

type HeartBeatClock struct {
}

func (h *HeartBeatClock) Sleep(d time.Duration) {
time.Sleep(d)
}
func (h *HeartBeatClock) Now() time.Time {
return time.Now()
}

var clock Clock = &HeartBeatClock{}

func heartBeatLoop(window *window.Window) {
hb, err := NewHeartbeat(window)
if err != nil {
log.Printf("Error starting up heart beat %v", err)
return
}
sendBeats(hb, window)
}
func sendBeats(hb *Heartbeat, window *window.Window) {
initialDelay := heartBeatDelay

if !window.Active() {
Expand All @@ -37,40 +58,34 @@ func heartBeatLoop(window *window.Window) {
}
}
log.Printf("Sending initial heart beat in %v", initialDelay)
time.Sleep(initialDelay)
penultimate := true
done := false
clock.Sleep(initialDelay)
for {
attempt := 0
if !penultimate {
penultimate = hb.updateNextBeat()
} else {
done = true
}
done := hb.updateNextBeat()

for attempt < maxAttempts {
err := sendHeartbeat(hb.api, hb.nextEvent)
if err == nil {
break
}
log.Printf("Error sending heart beat sleeping and trying again: %v", err)
time.Sleep(attemptDelay)
clock.Sleep(attemptDelay)
}
if done {
log.Printf("Sent penultimate heartbeat")
return
}

nextEventIn := hb.nextEvent.Sub(time.Now())
if !penultimate && nextEventIn >= 2*time.Hour {
nextEventIn := hb.nextEvent.Sub(clock.Now())
if !hb.penultimate && nextEventIn >= 2*time.Hour {
nextEventIn = nextEventIn - 1*time.Hour
} else {
// 5 minutes to give a bit of leeway
nextEventIn = nextEventIn - 5*time.Minute

}
log.Printf("Heartbeat sleeping until %v", time.Now().Add(nextEventIn))
time.Sleep(nextEventIn)
log.Printf("Heartbeat sleeping until %v", clock.Now().Add(nextEventIn))
clock.Sleep(nextEventIn)
}
}

Expand All @@ -83,29 +98,39 @@ func NewHeartbeat(window *window.Window) (*Heartbeat, error) {
apiClient, err := api.New()
if err != nil {
log.Printf("Error connecting to api %v", apiClient)
return nil, err
}

h := &Heartbeat{api: apiClient, end: nextEnd, window: window}
return h, nil
return h, err
}

//updates next heart beat time, returns true if will be the final event
func (h *Heartbeat) updateNextBeat() bool {
h.nextEvent = time.Now().Add(interval)
if h.penultimate {
h.nextEvent = h.end
return true
}
h.nextEvent = clock.Now().Add(interval)
if !h.window.NoWindow && h.nextEvent.After(h.end.Add(-time.Hour)) {
// always wwant an event 1 hour before end
h.nextEvent = h.end.Add(-time.Hour)
return true
if clock.Now().After(h.nextEvent) {
h.nextEvent = h.end
return true
}
h.penultimate = true
}
return false
}

func sendHeartbeat(api *api.CacophonyAPI, nextBeat time.Time) error {
if api == nil {
return nil
}
_, err := api.Heartbeat(nextBeat)
if err == nil {
log.Printf("Sent heart, valid until %v", nextBeat)
}
log.Printf("Sent heart, valid until %v", nextBeat)
return err
}

Expand Down
93 changes: 93 additions & 0 deletions heartbeat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2018 The Cacophony Project. All rights reserved.
// Use of this source code is governed by the Apache License Version 2.0;
// see the LICENSE file for further details.

package main

import (
"testing"
"time"

"github.com/TheCacophonyProject/window"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const dateFormat = "15:04"

type TestClock struct {
now time.Time
expectedSleeps []time.Time

sleepCount int
t *testing.T
hb *Heartbeat
}

func (h *TestClock) Sleep(d time.Duration) {
// nextBeat gets updated after sleep skip first
if h.sleepCount > 0 {
if h.sleepCount == len(h.expectedSleeps)-1 {
// penutimate sleep is only valid for 5 minutes after sleep
assert.Equal(h.t, h.expectedSleeps[h.sleepCount].Add(5*time.Minute).Format(dateFormat), h.hb.nextEvent.Format(dateFormat))

} else {
assert.Equal(h.t, h.expectedSleeps[h.sleepCount-1].Add(4*time.Hour).Format(dateFormat), h.hb.nextEvent.Format(dateFormat))
}
}
h.now = h.now.Add(d)
assert.Equal(h.t, h.now.Format(dateFormat), h.expectedSleeps[h.sleepCount].Format(dateFormat))
h.sleepCount += 1
}
func (h *TestClock) Now() time.Time {
return h.now
}

func TestSmallWindow(t *testing.T) {
clock := &TestClock{now: time.Now(), t: t}
w, err := window.New(clock.Now().Format(dateFormat), clock.Now().Add(time.Hour).Format(dateFormat), 0, 0)
sleeps := make([]time.Time, 1)
sleeps[0] = clock.now.Add(30 * time.Minute)

clock.expectedSleeps = sleeps
require.NoError(t, err)
heartBeatTestLoop(w, clock)
}

func TestLongDelay(t *testing.T) {
clock := &TestClock{now: time.Now(), t: t}
w, err := window.New(clock.Now().Add(time.Hour).Format(dateFormat), clock.Now().Add(4*time.Hour).Format(dateFormat), 0, 0)
sleeps := make([]time.Time, 2, 2)
sleeps[0] = clock.Now().Add(w.Until())
sleeps[1] = w.NextEnd().Add(-65 * time.Minute)

clock.expectedSleeps = sleeps
require.NoError(t, err)
heartBeatTestLoop(w, clock)
}

func TestWindow(t *testing.T) {
clock := &TestClock{now: time.Now(), t: t}
w, err := window.New(clock.Now().Format(dateFormat), clock.Now().Add(9*time.Hour).Format(dateFormat), 0, 0)
sleeps := make([]time.Time, 4, 4)
sleeps[0] = clock.now.Add(30 * time.Minute)
sleeps[1] = sleeps[0].Add(3 * time.Hour)
sleeps[2] = sleeps[1].Add(3 * time.Hour)
sleeps[3] = w.NextEnd().Add(-65 * time.Minute)

clock.expectedSleeps = sleeps
require.NoError(t, err)
heartBeatTestLoop(w, clock)
}

func heartBeatTestLoop(window *window.Window, timer *TestClock) {
clock = timer
heartBeatLoop(window)
hb, _ := NewHeartbeat(window)
timer.hb = hb
sendBeats(hb, window)
// assert last beat is at end
assert.Equal(timer.t, timer.sleepCount, len(timer.expectedSleeps), "Missing sleep events")

assert.Equal(timer.t, window.NextEnd().Format(dateFormat), hb.nextEvent.Format(dateFormat))
}

0 comments on commit 74facfc

Please sign in to comment.