Skip to content

Commit

Permalink
Add error counter (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
ders authored Sep 24, 2024
1 parent 5b4825c commit 66ebb85
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 4 deletions.
2 changes: 1 addition & 1 deletion doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 20 additions & 1 deletion monitor.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package nested

import (
"math"
"math/rand"
"sync"
)
Expand All @@ -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)
}

Expand All @@ -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)
Expand Down Expand Up @@ -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")
}
Expand All @@ -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
Expand Down
21 changes: 21 additions & 0 deletions monitor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -91,20 +102,30 @@ 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()
n = assertReceived(t, ch)
assertEqual(t, Error, n.OldState)
assertEqual(t, Ready, n.NewState)
assertEqual(t, nil, n.Error)
assertEqual(t, 0, n.ErrCount)

// Stop.
mon.Stop()
n = assertReceived(t, ch)
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()
Expand Down
8 changes: 6 additions & 2 deletions nested.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 66ebb85

Please sign in to comment.