diff --git a/README.md b/README.md index 24d9a96..53ef269 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,8 @@ fmt.Println(o.GetOutput()) // \_ [OK] My Subcheck ``` +Overall is concurrency-safe. + ## Human-readable bytes `ParseBytes` is a helper that can be used to parse string containering IEC or SI bytes into the number of bytes. diff --git a/result/overall.go b/result/overall.go index 5da42e5..1227836 100644 --- a/result/overall.go +++ b/result/overall.go @@ -4,6 +4,7 @@ package result import ( "fmt" "strings" + "sync" "github.com/NETWAYS/go-check" "github.com/NETWAYS/go-check/perfdata" @@ -30,25 +31,39 @@ type statusCount struct { // one suffices, but one fails, the whole check might be OK and only the subcheck // Warning or Critical. type Overall struct { - OKSummary string // default summary (first line of output) if everything is ok. Has to be set in a plugin - PartialResults []PartialResult + // default summary (first line of output) if everything is ok. Has to be set in a plugin + OKSummary string + // The results that are associated with this overall + PartialResults []*PartialResult + + // We use a Mutex to make sure PartialResults can be added and evaluated concurrently + mu sync.RWMutex } -// Add adds a return state explicitly +// Add adds a return state explicitly. +// Add is concurrency-safe func (o *Overall) Add(state check.Status, output string) { var result PartialResult result.SetState(state) result.Output = output - o.AddSubcheck(result) + o.AddSubcheck(&result) } -// AddSubcheck adds a PartialResult to the Overall -func (o *Overall) AddSubcheck(subcheck PartialResult) { +// AddSubcheck adds a PartialResult to the Overall. +// AddSubcheck is concurrency-safe +func (o *Overall) AddSubcheck(subcheck *PartialResult) { + o.mu.Lock() + defer o.mu.Unlock() + o.PartialResults = append(o.PartialResults, subcheck) } -// GetStatus returns the current state (ok, warning, critical, unknown) of the Overall +// GetStatus returns the current state (ok, warning, critical, unknown) of the Overall. +// GetStatus is concurrency-safe func (o *Overall) GetStatus() check.Status { + o.mu.RLock() + defer o.mu.RUnlock() + statuses := o.getStatusCount() if statuses.Critical > 0 { @@ -70,8 +85,12 @@ func (o *Overall) GetStatus() check.Status { return check.Unknown } -// GetOutput returns a text representation of the current outputs of the Overall +// GetOutput returns a text representation of the current outputs of the Overall. +// GetOutput is concurrency-safe func (o *Overall) GetOutput() string { + o.mu.RLock() + defer o.mu.RUnlock() + var output strings.Builder output.WriteString(o.getSummary() + "\n") @@ -126,7 +145,7 @@ func (o *Overall) getStatusCount() statusCount { // PartialResult represents a sub-result for an Overall struct type PartialResult struct { Perfdata perfdata.PerfdataList - PartialResults []PartialResult + PartialResults []*PartialResult Output string // Result state, either set explicitly or derived from partialResults @@ -141,19 +160,24 @@ type PartialResult struct { // and no PartialResults exist and no explicit state is set, GetStatus returns // s.defaultState instead of check.Unknown. defaultStateSetExplicitly bool + + mu sync.RWMutex } // NewPartialResult initializer with defaults. It is recommended to use NewPartialResult. // The default compared to the nil object is the default state is set to Unknown. -func NewPartialResult() PartialResult { - return PartialResult{ +func NewPartialResult() *PartialResult { + return &PartialResult{ stateSetExplicitly: false, defaultState: check.Unknown, } } // AddSubcheck adds a PartialResult to the PartialResult -func (s *PartialResult) AddSubcheck(subcheck PartialResult) { +func (s *PartialResult) AddSubcheck(subcheck *PartialResult) { + s.mu.Lock() + defer s.mu.Unlock() + s.PartialResults = append(s.PartialResults, subcheck) } @@ -164,18 +188,27 @@ func (s *PartialResult) String() string { // SetDefaultState sets a new default state for a PartialResult func (s *PartialResult) SetDefaultState(state check.Status) { + s.mu.Lock() + defer s.mu.Unlock() + s.defaultState = state s.defaultStateSetExplicitly = true } // SetState sets a state for a PartialResult func (s *PartialResult) SetState(state check.Status) { + s.mu.Lock() + defer s.mu.Unlock() + s.state = state s.stateSetExplicitly = true } // GetStatus returns the current state (ok, warning, critical, unknown) of the PartialResult func (s *PartialResult) GetStatus() check.Status { + s.mu.RLock() + defer s.mu.RUnlock() + if s.stateSetExplicitly { return s.state } diff --git a/result/overall_test.go b/result/overall_test.go index bc31558..998073a 100644 --- a/result/overall_test.go +++ b/result/overall_test.go @@ -4,6 +4,7 @@ import ( "fmt" "reflect" "strings" + "sync" "testing" "github.com/NETWAYS/go-check" @@ -153,7 +154,7 @@ func ExampleOverall_withSubchecks() { pd_list := perfdata.PerfdataList{} pd_list.Add(&example_perfdata) - subcheck := PartialResult{ + subcheck := &PartialResult{ Output: "Subcheck1 Test", Perfdata: pd_list, } @@ -194,14 +195,14 @@ func TestOverall_withEnhancedSubchecks(t *testing.T) { pd_list2.Add(&example_perfdata3) pd_list2.Add(&example_perfdata4) - subcheck := PartialResult{ + subcheck := &PartialResult{ Output: "Subcheck1 Test", Perfdata: pd_list, } subcheck.SetState(check.OK) - subcheck2 := PartialResult{ + subcheck2 := &PartialResult{ Output: "Subcheck2 Test", Perfdata: pd_list2, } @@ -231,13 +232,13 @@ func TestOverall_withEnhancedSubchecks(t *testing.T) { func TestOverall_withSubchecks_Simple_Output(t *testing.T) { var overall Overall - subcheck2 := PartialResult{ + subcheck2 := &PartialResult{ Output: "SubSubcheck", } subcheck2.SetState(check.OK) - subcheck := PartialResult{ + subcheck := &PartialResult{ Output: "PartialResult", } @@ -262,13 +263,13 @@ func TestOverall_withSubchecks_Simple_Output(t *testing.T) { func TestOverall_withSubchecks_Perfdata(t *testing.T) { var overall Overall - subcheck2 := PartialResult{ + subcheck2 := &PartialResult{ Output: "SubSubcheck", } subcheck2.SetState(check.OK) - subcheck := PartialResult{ + subcheck := &PartialResult{ Output: "PartialResult", } @@ -308,17 +309,17 @@ func TestOverall_withSubchecks_Perfdata(t *testing.T) { func TestOverall_withSubchecks_PartialResult(t *testing.T) { var overall Overall - subcheck3 := PartialResult{ + subcheck3 := &PartialResult{ Output: "SubSubSubcheck", } subcheck3.SetState(check.Critical) - subcheck2 := PartialResult{ + subcheck2 := &PartialResult{ Output: "SubSubcheck", } - subcheck := PartialResult{ + subcheck := &PartialResult{ Output: "PartialResult", } @@ -364,19 +365,19 @@ func TestOverall_withSubchecks_PartialResult(t *testing.T) { func TestOverall_withSubchecks_PartialResultStatus(t *testing.T) { var overall Overall - subcheck := PartialResult{ + subcheck := &PartialResult{ Output: "Subcheck", } subcheck.SetState(check.OK) - subsubcheck := PartialResult{ + subsubcheck := &PartialResult{ Output: "SubSubcheck", } subsubcheck.SetState(check.Warning) - subsubsubcheck := PartialResult{ + subsubsubcheck := &PartialResult{ Output: "SubSubSubcheck", } @@ -404,7 +405,7 @@ func TestOverall_withSubchecks_PartialResultStatus(t *testing.T) { func TestSubchecksPerfdata(t *testing.T) { var overall Overall - check1 := PartialResult{ + check1 := &PartialResult{ Output: "Check1", Perfdata: perfdata.PerfdataList{ &perfdata.Perfdata{ @@ -420,7 +421,7 @@ func TestSubchecksPerfdata(t *testing.T) { check1.SetState(check.OK) - check2 := PartialResult{ + check2 := &PartialResult{ Output: "Check2", Perfdata: perfdata.PerfdataList{ &perfdata.Perfdata{ @@ -445,7 +446,7 @@ func TestSubchecksPerfdata(t *testing.T) { func TestDefaultStates1(t *testing.T) { var overall Overall - subcheck := PartialResult{} + subcheck := &PartialResult{} subcheck.SetDefaultState(check.OK) @@ -459,7 +460,7 @@ func TestDefaultStates1(t *testing.T) { func TestDefaultStates2(t *testing.T) { var overall Overall - subcheck := PartialResult{} + subcheck := &PartialResult{} overall.AddSubcheck(subcheck) @@ -475,7 +476,7 @@ func TestDefaultStates2(t *testing.T) { func TestDefaultStates3(t *testing.T) { var overall Overall - subcheck := PartialResult{} + subcheck := &PartialResult{} subcheck.SetDefaultState(check.OK) subcheck.SetState(check.Warning) @@ -490,15 +491,15 @@ func TestDefaultStates3(t *testing.T) { func TestOverallOutputWithMultiLayerPartials(t *testing.T) { var overall Overall - subcheck1 := PartialResult{} + subcheck1 := &PartialResult{} subcheck1.SetState(check.Warning) - subcheck2 := PartialResult{} + subcheck2 := &PartialResult{} - subcheck2_1 := PartialResult{} + subcheck2_1 := &PartialResult{} subcheck2_1.SetState(check.OK) - subcheck2_2 := PartialResult{} + subcheck2_2 := &PartialResult{} subcheck2_2.SetState(check.Critical) subcheck2.AddSubcheck(subcheck2_1) @@ -572,3 +573,114 @@ func TestOverallGetOutput_WithMultipleStatesMultipleTimes(t *testing.T) { t.Fatalf("expected %s in first line, but output was %q", "WANT", output) } } + +func TestOverall_Add_WithRace(t *testing.T) { + o := &Overall{OKSummary: "unittest"} + + var wg sync.WaitGroup + + for range 5 { + wg.Add(1) + go func() { + defer wg.Done() + o.Add(check.OK, "goroutine") + }() + } + wg.Wait() +} + +func TestOverall_AddSubcheck_WithRace(t *testing.T) { + o := &Overall{} + + var wg sync.WaitGroup + + for range 5 { + wg.Add(1) + go func() { + defer wg.Done() + pr := NewPartialResult() + pr.SetState(check.OK) + pr.Output = "goroutine" + o.AddSubcheck(pr) + }() + } + wg.Wait() +} + +func TestOverall_Get_WithRace(t *testing.T) { + o := &Overall{OKSummary: "unittest"} + + for range 3 { + o.Add(check.OK, "OK") + } + + var wg sync.WaitGroup + + for range 3 { + wg.Add(1) + go func() { + defer wg.Done() + _ = o.GetStatus() + }() + } + + for range 3 { + wg.Add(1) + go func() { + defer wg.Done() + _ = o.GetOutput() + }() + } + + wg.Wait() +} + +func TestPartialResult_SetGet_WithRace(t *testing.T) { + pr := NewPartialResult() + + var wg sync.WaitGroup + + for range 3 { + wg.Add(2) + go func() { + defer wg.Done() + pr.SetState(check.Critical) + }() + go func() { + defer wg.Done() + _ = pr.GetStatus() + }() + } + wg.Wait() +} + +func TestPartialResult_AddSubcheck_WithRace(t *testing.T) { + parent := NewPartialResult() + parent.Output = "unittest" + + var wg sync.WaitGroup + for range 5 { + wg.Add(1) + go func() { + defer wg.Done() + child := NewPartialResult() + child.SetState(check.OK) + parent.AddSubcheck(child) + }() + } + wg.Wait() +} + +func TestPartialResult_SetDefaultState_WithRace(t *testing.T) { + pr := NewPartialResult() + + var wg sync.WaitGroup + for range 5 { + wg.Add(1) + go func() { + defer wg.Done() + pr.SetDefaultState(check.Warning) + }() + } + wg.Wait() +}