Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: implement common check methods by check base #135

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 86 additions & 3 deletions pkg/checks/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"time"

"github.com/caas-team/sparrow/internal/helper"
"github.com/caas-team/sparrow/internal/logger"
"github.com/getkin/kin-openapi/openapi3"
"github.com/prometheus/client_golang/prometheus"
)
Expand All @@ -36,7 +37,7 @@ var DefaultRetry = helper.RetryConfig{

// Check implementations are expected to perform specific monitoring tasks and report results.
//
//go:generate moq -out base_moq.go . Check
//go:generate moq -out base_check_moq.go . Check
type Check interface {
// Run is called once, to start running the check. The check should
// run until the context is canceled and handle problems itself.
Expand All @@ -58,16 +59,98 @@ type Check interface {
GetMetricCollectors() []prometheus.Collector
}

// CheckBase is a struct providing common fields used by implementations of the Check interface.
// Base is a struct providing common fields used by implementations of the Check interface.
// It serves as a foundational structure that should be embedded in specific check implementations.
type CheckBase struct {
type Base[T Runtime] struct {
// name is the name of the check
name string
// Config is the current configuration of the check
Config T
// Mutex for thread-safe access to shared resources within the check implementation
Mu sync.Mutex
// Signal channel used to notify about shutdown of a check
DoneChan chan struct{}
}

func NewBase[T Runtime](name string, config T) Base[T] {
return Base[T]{
name: name,
Config: config,
Mu: sync.Mutex{},
DoneChan: make(chan struct{}, 1),
}
}

// Name returns the name of the check
func (b *Base[T]) Name() string {
return b.name
}

// SetConfig sets the configuration of the check
func (b *Base[T]) SetConfig(config Runtime) error {
if cfg, ok := config.(T); ok {
b.Mu.Lock()
defer b.Mu.Unlock()
b.Config = cfg
return nil
}

return ErrConfigMismatch{
Expected: b.Name(),
Current: config.For(),
}
}

// GetConfig returns the current configuration of the check
func (b *Base[T]) GetConfig() Runtime {
b.Mu.Lock()
defer b.Mu.Unlock()
return b.Config
}

// SendResult sends the result of a check run to the provided channel
func (b *Base[T]) SendResult(channel chan ResultDTO, data any) {
channel <- ResultDTO{
Name: b.Name(),
Result: &Result{Data: data, Timestamp: time.Now()},
}
}

// CheckFunc is a function that performs a check and returns the result
type CheckFunc func(ctx context.Context) any

// StartCheck runs the check indefinitely, sending results to the provided channel at the specified interval
func (b *Base[T]) StartCheck(ctx context.Context, cResult chan ResultDTO, interval time.Duration, check CheckFunc) error {
ctx, cancel := logger.NewContextWithLogger(ctx)
defer cancel()
log := logger.FromContext(ctx).With("check", b.Name())

log.InfoContext(ctx, "Starting check", "interval", interval.String())
for {
select {
case <-ctx.Done():
log.ErrorContext(ctx, "Context canceled", "error", ctx.Err())
return ctx.Err()
case <-b.DoneChan:
log.InfoContext(ctx, "Shutdown signal received")
return nil
case <-time.After(interval):
res := check(ctx)
b.SendResult(cResult, res)
log.DebugContext(ctx, "Check run completed")
}
}
}

// Shutdown shuts down the check
func (b *Base[T]) Shutdown() {
b.DoneChan <- struct{}{}
close(b.DoneChan)
}

// Runtime is the interface that all check configurations must implement
//
//go:generate moq -out base_runtime_moq.go . Runtime
type Runtime interface {
// For returns the name of the check being configured
For() string
Expand Down
File renamed without changes.
104 changes: 104 additions & 0 deletions pkg/checks/base_runtime_moq.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

158 changes: 158 additions & 0 deletions pkg/checks/base_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package checks

import (
"sync"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
)

const (
name = "test"
)

func TestBase_Shutdown(t *testing.T) {
cDone := make(chan struct{}, 1)
c := &Base[*RuntimeMock]{
Mu: sync.Mutex{},
DoneChan: cDone,
}
c.Shutdown()

_, ok := <-cDone
if !ok {
t.Error("Shutdown() should be ok")
}

assert.Panics(t, func() {
cDone <- struct{}{}
}, "Channel is closed, should panic")

ch := NewBase(name, &RuntimeMock{})
ch.Shutdown()

_, ok = <-ch.DoneChan
if !ok {
t.Error("Channel should be done")
}

assert.Panics(t, func() {
ch.DoneChan <- struct{}{}
}, "Channel is closed, should panic")
}

func TestBase_SetConfig(t *testing.T) {
tests := []struct {
name string
input Runtime
want *mockConfig
wantErr bool
}{
{
name: "simple config",
input: &mockConfig{
Targets: []string{
"example.com",
"sparrow.com",
},
Interval: 10 * time.Second,
Timeout: 30 * time.Second,
},
want: &mockConfig{
Targets: []string{"example.com", "sparrow.com"},
Interval: 10 * time.Second,
Timeout: 30 * time.Second,
},
wantErr: false,
},
{
name: "empty config",
input: &mockConfig{},
want: &mockConfig{},
wantErr: false,
},
{
name: "wrong type",
input: &RuntimeMock{
ForFunc: func() string { return "mock" },
},
want: &mockConfig{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewBase(name, &mockConfig{})

if err := c.SetConfig(tt.input); (err != nil) != tt.wantErr {
t.Errorf("DNS.SetConfig() error = %v, wantErr %v", err, tt.wantErr)
}

if !cmp.Equal(c.Config, tt.want) {
t.Error(cmp.Diff(c.Config, tt.want))
}
})
}
}

func TestBase_Name(t *testing.T) {
c := NewBase(name, &RuntimeMock{})
if c.Name() != name {
t.Errorf("Name() should return %q", name)
}
}

func TestBase_GetConfig(t *testing.T) {
c := NewBase(name, &mockConfig{
Targets: []string{"example.com"},
Interval: 10 * time.Second,
Timeout: 30 * time.Second,
})

cfg := c.GetConfig().(*mockConfig)
if len(cfg.Targets) != 1 {
t.Error("Targets should contain 1 element")
}
if cfg.Interval != 10*time.Second {
t.Error("Interval should be 10 seconds")
}
if cfg.Timeout != 30*time.Second {
t.Error("Timeout should be 30 seconds")
}
}

func TestBase_SendResult(t *testing.T) {
cResult := make(chan ResultDTO, 1)
defer close(cResult)

c := NewBase(name, &RuntimeMock{})
c.SendResult(cResult, name)

r := <-cResult
if r.Name != name {
t.Error("Name should be 'test'")
}
if r.Result == nil {
t.Error("Result should not be nil")
}

if r.Result.Data != name {
t.Error("Data should be 'test'")
}
}

type mockConfig struct {
Targets []string
Interval time.Duration
Timeout time.Duration
}

func (m *mockConfig) For() string {
return "mock"
}

func (m *mockConfig) Validate() error {
return nil
}
Loading
Loading