From 32087d8b60bc72bb75bfd49b93ce5990123385dc Mon Sep 17 00:00:00 2001 From: Markus Opolka Date: Thu, 18 Jun 2026 12:31:44 +0200 Subject: [PATCH 1/6] Make Overall concurrency-safe --- README.md | 2 + result/overall.go | 53 +++++++++++---- result/overall_test.go | 142 ++++++++++++++++++++++++++++++++++------- 3 files changed, 163 insertions(+), 34 deletions(-) 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..a3f407c 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,38 @@ 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 + + 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. +// Add 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. +// Add 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 +84,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. +// Add 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 +144,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 +159,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) } @@ -170,12 +193,18 @@ func (s *PartialResult) SetDefaultState(state check.Status) { // 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..08d83aa 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,100 @@ 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() +} From ed7720777b9c7a6ac0c43378f30d03d9a26a5b84 Mon Sep 17 00:00:00 2001 From: Markus Opolka Date: Thu, 18 Jun 2026 15:03:31 +0200 Subject: [PATCH 2/6] [squash] --- result/overall.go | 3 +++ result/overall_test.go | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/result/overall.go b/result/overall.go index a3f407c..3f6deb2 100644 --- a/result/overall.go +++ b/result/overall.go @@ -187,6 +187,9 @@ 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 } diff --git a/result/overall_test.go b/result/overall_test.go index 08d83aa..998073a 100644 --- a/result/overall_test.go +++ b/result/overall_test.go @@ -670,3 +670,17 @@ func TestPartialResult_AddSubcheck_WithRace(t *testing.T) { } 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() +} From b0d37816e98acd35e6aeec1ddad238a22dc5d5b0 Mon Sep 17 00:00:00 2001 From: Markus Opolka Date: Thu, 18 Jun 2026 15:29:08 +0200 Subject: [PATCH 3/6] Update result/overall.go --- result/overall.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/result/overall.go b/result/overall.go index 3f6deb2..55a3d02 100644 --- a/result/overall.go +++ b/result/overall.go @@ -85,7 +85,7 @@ func (o *Overall) GetStatus() check.Status { } // GetOutput returns a text representation of the current outputs of the Overall. -// Add is concurrency-safe +// GetOutput is concurrency-safe func (o *Overall) GetOutput() string { o.mu.RLock() defer o.mu.RUnlock() From 2a6642537837266ac4460f63e24c6a441ee87ccf Mon Sep 17 00:00:00 2001 From: Markus Opolka Date: Thu, 18 Jun 2026 15:29:17 +0200 Subject: [PATCH 4/6] Update result/overall.go --- result/overall.go | 1 + 1 file changed, 1 insertion(+) diff --git a/result/overall.go b/result/overall.go index 55a3d02..0900ea4 100644 --- a/result/overall.go +++ b/result/overall.go @@ -36,6 +36,7 @@ type Overall struct { // 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 } From bc278d47e57389b3a54c5a81c72753b95fcc62ce Mon Sep 17 00:00:00 2001 From: Markus Opolka Date: Thu, 18 Jun 2026 15:29:26 +0200 Subject: [PATCH 5/6] Update result/overall.go --- result/overall.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/result/overall.go b/result/overall.go index 0900ea4..cddc31f 100644 --- a/result/overall.go +++ b/result/overall.go @@ -59,7 +59,7 @@ func (o *Overall) AddSubcheck(subcheck *PartialResult) { } // GetStatus returns the current state (ok, warning, critical, unknown) of the Overall. -// Add is concurrency-safe +// GetStatus is concurrency-safe func (o *Overall) GetStatus() check.Status { o.mu.RLock() defer o.mu.RUnlock() From 0bafaec5c4bb33f7adcd60e8b7f7f8c57f4706c8 Mon Sep 17 00:00:00 2001 From: Markus Opolka Date: Thu, 18 Jun 2026 15:40:34 +0200 Subject: [PATCH 6/6] [squash] --- result/overall.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/result/overall.go b/result/overall.go index cddc31f..1227836 100644 --- a/result/overall.go +++ b/result/overall.go @@ -36,7 +36,7 @@ type Overall struct { // The results that are associated with this overall PartialResults []*PartialResult - // We use a Mutex to make sure PartialResults can be added and evaluated concurrently + // We use a Mutex to make sure PartialResults can be added and evaluated concurrently mu sync.RWMutex } @@ -50,7 +50,7 @@ func (o *Overall) Add(state check.Status, output string) { } // AddSubcheck adds a PartialResult to the Overall. -// Add is concurrency-safe +// AddSubcheck is concurrency-safe func (o *Overall) AddSubcheck(subcheck *PartialResult) { o.mu.Lock() defer o.mu.Unlock()