From 66ebb85c14cfcc7d5bc3d9447078d505124245ff Mon Sep 17 00:00:00 2001 From: Anders McCarthy Date: Tue, 24 Sep 2024 11:17:43 +0200 Subject: [PATCH] Add error counter (#7) --- doc.go | 2 +- monitor.go | 21 ++++++++++++++++++++- monitor_test.go | 21 +++++++++++++++++++++ nested.go | 8 ++++++-- 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/doc.go b/doc.go index 6df2863..6393cd5 100644 --- a/doc.go +++ b/doc.go @@ -9,7 +9,7 @@ // The state machine has the following states: // - Initializing. The service is not ready yet. // - Ready. The service is running normally. -// - Not ready. The service is temporarily unavailable. +// - Error. The service is temporarily unavailable. // - Stopped. The service is permanently unavailable. // // The state machine begins in the initializing state. Once it transitions to one of the other states, it can never diff --git a/monitor.go b/monitor.go index 2f671e9..609dc0e 100644 --- a/monitor.go +++ b/monitor.go @@ -1,6 +1,7 @@ package nested import ( + "math" "math/rand" "sync" ) @@ -12,6 +13,7 @@ type Monitor struct { sync.Mutex state State // current state err error // current error state, if the state is not ready + errCount int // number of consecutive errors callbacks map[Token]func(Event) } @@ -34,6 +36,14 @@ func (m *Monitor) Err() error { return m.err } +// ErrCount returns the number of consective errors recorded for this service. If the service is not in Error state, +// ErrCount returns 0. +func (m *Monitor) ErrCount() int { + m.Lock() + defer m.Unlock() + return m.errCount +} + // Stop sets the service to stopped. If there are registered observers, all observers are called before returning. func (m *Monitor) Stop() { m.setState(Stopped, nil) @@ -88,10 +98,18 @@ func (m *Monitor) setState(newState State, newErr error) { m.Lock() defer m.Unlock() - if newState == m.state && !(newState == Error && newErr != m.err) { + if newState == m.state && newState != Error { return // nothing to do } + if newState == Error { + if m.errCount < math.MaxInt { + m.errCount++ + } + } else { + m.errCount = 0 + } + if m.state == Stopped { panic("cannot transition from stopped state") } @@ -100,6 +118,7 @@ func (m *Monitor) setState(newState State, newErr error) { OldState: m.state, NewState: newState, Error: newErr, + ErrCount: m.errCount, } m.state = newState diff --git a/monitor_test.go b/monitor_test.go index 5d9f20b..4de7902 100644 --- a/monitor_test.go +++ b/monitor_test.go @@ -39,22 +39,32 @@ func TestMonitor(t *testing.T) { mon := Monitor{} assertEqual(t, Initializing, mon.GetState()) assertEqual(t, nil, mon.Err()) + assertEqual(t, 0, mon.ErrCount()) // Set to Ready. mon.SetReady() assertEqual(t, Ready, mon.GetState()) assertEqual(t, nil, mon.Err()) + assertEqual(t, 0, mon.ErrCount()) // Set to Error. reason := errors.New("some reason") mon.SetError(reason) assertEqual(t, Error, mon.GetState()) assertEqual(t, reason, mon.Err()) + assertEqual(t, 1, mon.ErrCount()) + + // Two consecutive errors. + mon.SetError(reason) + assertEqual(t, Error, mon.GetState()) + assertEqual(t, reason, mon.Err()) + assertEqual(t, 2, mon.ErrCount()) // Set Ready again. Previous error can still be retrieved. mon.SetReady() assertEqual(t, Ready, mon.GetState()) assertEqual(t, reason, mon.Err()) + assertEqual(t, 0, mon.ErrCount()) // Stop. mon.Stop() @@ -77,6 +87,7 @@ func TestMonitorNotifications(t *testing.T) { assertEqual(t, Initializing, n.OldState) assertEqual(t, Ready, n.NewState) assertEqual(t, nil, n.Error) + assertEqual(t, 0, n.ErrCount) // Set to Ready again, and there's not an additional notification. mon.SetReady() @@ -91,6 +102,14 @@ func TestMonitorNotifications(t *testing.T) { assertEqual(t, Ready, n.OldState) assertEqual(t, Error, n.NewState) assertEqual(t, reason, n.Error) + assertEqual(t, 1, n.ErrCount) + + // Two consecutive errors. + mon.SetError(reason) + n = assertReceived(t, ch) + assertEqual(t, Error, mon.GetState()) + assertEqual(t, reason, mon.Err()) + assertEqual(t, 2, n.ErrCount) // Set ready again. mon.SetReady() @@ -98,6 +117,7 @@ func TestMonitorNotifications(t *testing.T) { assertEqual(t, Error, n.OldState) assertEqual(t, Ready, n.NewState) assertEqual(t, nil, n.Error) + assertEqual(t, 0, n.ErrCount) // Stop. mon.Stop() @@ -105,6 +125,7 @@ func TestMonitorNotifications(t *testing.T) { assertEqual(t, Ready, n.OldState) assertEqual(t, Stopped, n.NewState) assertEqual(t, nil, n.Error) + assertEqual(t, 0, n.ErrCount) // Stop again, and there's not an additional notification. mon.Stop() diff --git a/nested.go b/nested.go index 1538d95..e5f93d4 100644 --- a/nested.go +++ b/nested.go @@ -20,19 +20,23 @@ func (s State) String() string { return names[s] } -// An event is a single notification of a state change. +// An event is a single notification of a state change. If the current state is Error, an event is issued for +// every error encountered, since the error count will increase. type Event struct { OldState State NewState State Error error // error condition if the new state is Error, nil otherwise + ErrCount int } // The Service interface defines the behavior of a nested service. type Service interface { // GetState returns the current state of the service. GetState() State - // Err returns the most recent error condition. Returns nil if the service has never been in the Err state. + // Err returns the most recent error condition. Returns nil if the service has never been in the Error state. Err() error + // ErrCount returns the number of consecutive Error states. Returns 0 if the service is not in the Error state. + ErrCount() int // Stop stops the service and releases all resources. Stop should not return until the service shutdown is complete. Stop() // RegisterCallback registers a function which will be called any time there is a state change. Returns a token