github.com/decred/dcrlnd@v0.7.6/healthcheck/healthcheck_test.go (about)

     1  package healthcheck
     2  
     3  import (
     4  	"errors"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/decred/dcrlnd/ticker"
     9  	"github.com/stretchr/testify/require"
    10  )
    11  
    12  var (
    13  	errNonNil = errors.New("non-nil test error")
    14  	timeout   = time.Second
    15  	testTime  = time.Unix(1, 2)
    16  )
    17  
    18  type mockedCheck struct {
    19  	t       *testing.T
    20  	errChan chan error
    21  }
    22  
    23  // newMockCheck creates a new mock.
    24  func newMockCheck(t *testing.T) *mockedCheck {
    25  	return &mockedCheck{
    26  		t:       t,
    27  		errChan: make(chan error),
    28  	}
    29  }
    30  
    31  // call returns our mock's error channel, which we can send responses on.
    32  func (m *mockedCheck) call() chan error {
    33  	return m.errChan
    34  }
    35  
    36  // sendError sends an error into our mock's error channel, mocking the sending
    37  // of a response from our check function.
    38  func (m *mockedCheck) sendError(err error) {
    39  	select {
    40  	case m.errChan <- err:
    41  	case <-time.After(timeout):
    42  		m.t.Fatalf("could not send error: %v", err)
    43  	}
    44  }
    45  
    46  // TestMonitor tests creation and triggering of a monitor with a health check.
    47  func TestMonitor(t *testing.T) {
    48  	intervalTicker := ticker.NewForce(time.Hour)
    49  
    50  	mock := newMockCheck(t)
    51  	shutdown := make(chan struct{})
    52  
    53  	// Create our config for monitoring. We will use a 0 back off so that
    54  	// out test does not need to wait.
    55  	cfg := &Config{
    56  		Checks: []*Observation{
    57  			{
    58  				Check:    mock.call,
    59  				Interval: intervalTicker,
    60  				Attempts: 2,
    61  				Backoff:  0,
    62  				Timeout:  time.Hour,
    63  			},
    64  		},
    65  		Shutdown: func(string, ...interface{}) {
    66  			shutdown <- struct{}{}
    67  		},
    68  	}
    69  	monitor := NewMonitor(cfg)
    70  
    71  	require.NoError(t, monitor.Start(), "could not start monitor")
    72  
    73  	// Tick is a helper we will use to tick our interval.
    74  	tick := func() {
    75  		select {
    76  		case intervalTicker.Force <- testTime:
    77  		case <-time.After(timeout):
    78  			t.Fatal("could not tick timer")
    79  		}
    80  	}
    81  
    82  	// Tick our timer and provide our error channel with a nil error. This
    83  	// mocks our check function succeeding on the first call.
    84  	tick()
    85  	mock.sendError(nil)
    86  
    87  	// Now we tick our timer again. This time send a non-nil error, followed
    88  	// by a nil error. This tests our retry logic, because we allow 2
    89  	// retries, so should recover without needing to shutdown.
    90  	tick()
    91  	mock.sendError(errNonNil)
    92  	mock.sendError(nil)
    93  
    94  	// Finally, we tick our timer once more, and send two non-nil errors
    95  	// into our error channel. This mocks our check function failing twice.
    96  	tick()
    97  	mock.sendError(errNonNil)
    98  	mock.sendError(errNonNil)
    99  
   100  	// Since we have failed within our allowed number of retries, we now
   101  	// expect a call to our shutdown function.
   102  	select {
   103  	case <-shutdown:
   104  	case <-time.After(timeout):
   105  		t.Fatal("expected shutdown")
   106  	}
   107  
   108  	require.NoError(t, monitor.Stop(), "could not stop monitor")
   109  }
   110  
   111  // TestRetryCheck tests our retry logic. It does not include a test for exiting
   112  // during the back off period.
   113  func TestRetryCheck(t *testing.T) {
   114  	tests := []struct {
   115  		name string
   116  
   117  		// errors provides an in-order list of errors that we expect our
   118  		// health check to respond with. The number of errors in this
   119  		// list indicates the number of times we expect our check to
   120  		// be called, because our test will fail if we do not consume
   121  		// every error.
   122  		errors []error
   123  
   124  		// attempts is the number of times we call a check before
   125  		// failing.
   126  		attempts int
   127  
   128  		// timeout is the time we allow our check to take before we
   129  		// fail them.
   130  		timeout time.Duration
   131  
   132  		// expectedShutdown is true if we expect a shutdown to be
   133  		// triggered because all of our calls failed.
   134  		expectedShutdown bool
   135  
   136  		// maxAttemptsReached specifies whether the max allowed
   137  		// attempts are reached from calling retryCheck.
   138  		maxAttemptsReached bool
   139  	}{
   140  		{
   141  			name:               "first call succeeds",
   142  			errors:             []error{nil},
   143  			attempts:           2,
   144  			timeout:            time.Hour,
   145  			expectedShutdown:   false,
   146  			maxAttemptsReached: false,
   147  		},
   148  		{
   149  			name:               "first call fails",
   150  			errors:             []error{errNonNil},
   151  			attempts:           1,
   152  			timeout:            time.Hour,
   153  			expectedShutdown:   true,
   154  			maxAttemptsReached: true,
   155  		},
   156  		{
   157  			name:               "fail then recover",
   158  			errors:             []error{errNonNil, nil},
   159  			attempts:           2,
   160  			timeout:            time.Hour,
   161  			expectedShutdown:   false,
   162  			maxAttemptsReached: false,
   163  		},
   164  		{
   165  			name:               "always fail",
   166  			errors:             []error{errNonNil, errNonNil},
   167  			attempts:           2,
   168  			timeout:            time.Hour,
   169  			expectedShutdown:   true,
   170  			maxAttemptsReached: true,
   171  		},
   172  		{
   173  			name:               "no calls",
   174  			errors:             nil,
   175  			attempts:           0,
   176  			timeout:            time.Hour,
   177  			expectedShutdown:   false,
   178  			maxAttemptsReached: false,
   179  		},
   180  		{
   181  			name:               "call times out",
   182  			errors:             nil,
   183  			attempts:           1,
   184  			timeout:            1,
   185  			expectedShutdown:   true,
   186  			maxAttemptsReached: true,
   187  		},
   188  	}
   189  
   190  	for _, test := range tests {
   191  		test := test
   192  
   193  		t.Run(test.name, func(t *testing.T) {
   194  			var shutdown bool
   195  			shutdownFunc := func(string, ...interface{}) {
   196  				shutdown = true
   197  			}
   198  
   199  			mock := newMockCheck(t)
   200  
   201  			// Create an observation that calls our call counting
   202  			// function. We set a zero back off so that the test
   203  			// will not wait.
   204  			observation := &Observation{
   205  				Check:    mock.call,
   206  				Attempts: test.attempts,
   207  				Timeout:  test.timeout,
   208  				Backoff:  0,
   209  			}
   210  			quit := make(chan struct{})
   211  
   212  			// Run our retry check in a goroutine because it blocks
   213  			// on us sending errors into the mocked caller's error
   214  			// channel.
   215  			done := make(chan struct{})
   216  			retryResult := false
   217  			go func() {
   218  				retryResult = observation.retryCheck(
   219  					quit, shutdownFunc,
   220  				)
   221  				close(done)
   222  			}()
   223  
   224  			// Prompt our mock caller to send responses for calls
   225  			// to our call function.
   226  			for _, err := range test.errors {
   227  				mock.sendError(err)
   228  			}
   229  
   230  			// Make sure that we have finished running our retry
   231  			// check function before we start checking results.
   232  			<-done
   233  
   234  			require.Equal(t, test.maxAttemptsReached, retryResult,
   235  				"retryCheck returned unexpected error")
   236  			require.Equal(t, test.expectedShutdown, shutdown,
   237  				"unexpected shutdown state")
   238  		})
   239  	}
   240  }