226 lines
5.5 KiB
Go
226 lines
5.5 KiB
Go
|
package healthcheck
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"testing"
|
||
|
"time"
|
||
|
|
||
|
"github.com/lightningnetwork/lnd/ticker"
|
||
|
"github.com/stretchr/testify/require"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
errNonNil = errors.New("non-nil test error")
|
||
|
timeout = time.Second
|
||
|
testTime = time.Unix(1, 2)
|
||
|
)
|
||
|
|
||
|
type mockedCheck struct {
|
||
|
t *testing.T
|
||
|
errChan chan error
|
||
|
}
|
||
|
|
||
|
// newMockCheck creates a new mock.
|
||
|
func newMockCheck(t *testing.T) *mockedCheck {
|
||
|
return &mockedCheck{
|
||
|
t: t,
|
||
|
errChan: make(chan error),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// call returns our mock's error channel, which we can send responses on.
|
||
|
func (m *mockedCheck) call() chan error {
|
||
|
return m.errChan
|
||
|
}
|
||
|
|
||
|
// sendError sends an error into our mock's error channel, mocking the sending
|
||
|
// of a response from our check function.
|
||
|
func (m *mockedCheck) sendError(err error) {
|
||
|
select {
|
||
|
case m.errChan <- err:
|
||
|
case <-time.After(timeout):
|
||
|
m.t.Fatalf("could not send error: %v", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// TestMonitor tests creation and triggering of a monitor with a health check.
|
||
|
func TestMonitor(t *testing.T) {
|
||
|
intervalTicker := ticker.NewForce(time.Hour)
|
||
|
|
||
|
mock := newMockCheck(t)
|
||
|
shutdown := make(chan struct{})
|
||
|
|
||
|
// Create our config for monitoring. We will use a 0 back off so that
|
||
|
// out test does not need to wait.
|
||
|
cfg := &Config{
|
||
|
Checks: []*Observation{
|
||
|
{
|
||
|
Check: mock.call,
|
||
|
Interval: intervalTicker,
|
||
|
Attempts: 2,
|
||
|
Backoff: 0,
|
||
|
Timeout: time.Hour,
|
||
|
},
|
||
|
},
|
||
|
Shutdown: func(string, ...interface{}) {
|
||
|
shutdown <- struct{}{}
|
||
|
},
|
||
|
}
|
||
|
monitor := NewMonitor(cfg)
|
||
|
|
||
|
require.NoError(t, monitor.Start(), "could not start monitor")
|
||
|
|
||
|
// Tick is a helper we will use to tick our interval.
|
||
|
tick := func() {
|
||
|
select {
|
||
|
case intervalTicker.Force <- testTime:
|
||
|
case <-time.After(timeout):
|
||
|
t.Fatal("could not tick timer")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Tick our timer and provide our error channel with a nil error. This
|
||
|
// mocks our check function succeeding on the first call.
|
||
|
tick()
|
||
|
mock.sendError(nil)
|
||
|
|
||
|
// Now we tick our timer again. This time send a non-nil error, followed
|
||
|
// by a nil error. This tests our retry logic, because we allow 2
|
||
|
// retries, so should recover without needing to shutdown.
|
||
|
tick()
|
||
|
mock.sendError(errNonNil)
|
||
|
mock.sendError(nil)
|
||
|
|
||
|
// Finally, we tick our timer once more, and send two non-nil errors
|
||
|
// into our error channel. This mocks our check function failing twice.
|
||
|
tick()
|
||
|
mock.sendError(errNonNil)
|
||
|
mock.sendError(errNonNil)
|
||
|
|
||
|
// Since we have failed within our allowed number of retries, we now
|
||
|
// expect a call to our shutdown function.
|
||
|
select {
|
||
|
case <-shutdown:
|
||
|
case <-time.After(timeout):
|
||
|
t.Fatal("expected shutdown")
|
||
|
}
|
||
|
|
||
|
require.NoError(t, monitor.Stop(), "could not stop monitor")
|
||
|
}
|
||
|
|
||
|
// TestRetryCheck tests our retry logic. It does not include a test for exiting
|
||
|
// during the back off period.
|
||
|
func TestRetryCheck(t *testing.T) {
|
||
|
tests := []struct {
|
||
|
name string
|
||
|
|
||
|
// errors provides an in-order list of errors that we expect our
|
||
|
// health check to respond with. The number of errors in this
|
||
|
// list indicates the number of times we expect our check to
|
||
|
// be called, because our test will fail if we do not consume
|
||
|
// every error.
|
||
|
errors []error
|
||
|
|
||
|
// attempts is the number of times we call a check before
|
||
|
// failing.
|
||
|
attempts int
|
||
|
|
||
|
// timeout is the time we allow our check to take before we
|
||
|
// fail them.
|
||
|
timeout time.Duration
|
||
|
|
||
|
// expectedShutdown is true if we expect a shutdown to be
|
||
|
// triggered because all of our calls failed.
|
||
|
expectedShutdown bool
|
||
|
}{
|
||
|
{
|
||
|
name: "first call succeeds",
|
||
|
errors: []error{nil},
|
||
|
attempts: 2,
|
||
|
timeout: time.Hour,
|
||
|
expectedShutdown: false,
|
||
|
},
|
||
|
{
|
||
|
name: "first call fails",
|
||
|
errors: []error{errNonNil},
|
||
|
attempts: 1,
|
||
|
timeout: time.Hour,
|
||
|
expectedShutdown: true,
|
||
|
},
|
||
|
{
|
||
|
name: "fail then recover",
|
||
|
errors: []error{errNonNil, nil},
|
||
|
attempts: 2,
|
||
|
timeout: time.Hour,
|
||
|
expectedShutdown: false,
|
||
|
},
|
||
|
{
|
||
|
name: "always fail",
|
||
|
errors: []error{errNonNil, errNonNil},
|
||
|
attempts: 2,
|
||
|
timeout: time.Hour,
|
||
|
expectedShutdown: true,
|
||
|
},
|
||
|
{
|
||
|
name: "no calls",
|
||
|
errors: nil,
|
||
|
attempts: 0,
|
||
|
timeout: time.Hour,
|
||
|
expectedShutdown: false,
|
||
|
},
|
||
|
{
|
||
|
name: "call times out",
|
||
|
errors: nil,
|
||
|
attempts: 1,
|
||
|
timeout: 1,
|
||
|
expectedShutdown: true,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
for _, test := range tests {
|
||
|
test := test
|
||
|
|
||
|
t.Run(test.name, func(t *testing.T) {
|
||
|
var shutdown bool
|
||
|
shutdownFunc := func(string, ...interface{}) {
|
||
|
shutdown = true
|
||
|
}
|
||
|
|
||
|
mock := newMockCheck(t)
|
||
|
|
||
|
// Create an observation that calls our call counting
|
||
|
// function. We set a zero back off so that the test
|
||
|
// will not wait.
|
||
|
observation := &Observation{
|
||
|
Check: mock.call,
|
||
|
Attempts: test.attempts,
|
||
|
Timeout: test.timeout,
|
||
|
Backoff: 0,
|
||
|
}
|
||
|
quit := make(chan struct{})
|
||
|
|
||
|
// Run our retry check in a goroutine because it blocks
|
||
|
// on us sending errors into the mocked caller's error
|
||
|
// channel.
|
||
|
done := make(chan struct{})
|
||
|
go func() {
|
||
|
observation.retryCheck(quit, shutdownFunc)
|
||
|
close(done)
|
||
|
}()
|
||
|
|
||
|
// Prompt our mock caller to send responses for calls
|
||
|
// to our call function.
|
||
|
for _, err := range test.errors {
|
||
|
mock.sendError(err)
|
||
|
}
|
||
|
|
||
|
// Make sure that we have finished running our retry
|
||
|
// check function before we start checking results.
|
||
|
<-done
|
||
|
|
||
|
require.Equal(t, test.expectedShutdown, shutdown,
|
||
|
"unexpected shutdown state")
|
||
|
})
|
||
|
}
|
||
|
}
|