Files
ITWorx-Pulse-Public/internal/probe/scheduler_test.go
T
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

161 lines
4.8 KiB
Go

package probe
import (
"context"
"errors"
"fmt"
"sync"
"testing"
"time"
)
type executorFunc func(context.Context, Definition) (Result, error)
func (f executorFunc) Execute(ctx context.Context, definition Definition) (Result, error) {
return f(ctx, definition)
}
func schedulerDefinition(id string) Definition {
return Definition{ID: id, ServiceID: "service", Name: id, Type: TypeHTTP, Target: Target{Scheme: "https", Host: "example.internal", Port: 443}, Interval: 30 * time.Second, Timeout: 5 * time.Second, Enabled: true, Revision: 1}
}
func TestSchedulerRetriesTimeoutsAndNormalizesResults(t *testing.T) {
var mu sync.Mutex
attempts := 0
executor := executorFunc(func(ctx context.Context, definition Definition) (Result, error) {
if definition.ID != "probe-1" {
t.Errorf("definition=%+v", definition)
}
mu.Lock()
attempts++
current := attempts
mu.Unlock()
if current == 1 {
return Result{}, RetryableError{Err: errors.New("temporary")}
}
return Result{State: "up"}, nil
})
scheduler, err := NewScheduler(executor, SchedulerConfig{MaxConcurrent: 2, MaxAttempts: 2, RetryBackoff: 0})
if err != nil {
t.Fatal(err)
}
report, err := scheduler.Run(context.Background(), []Definition{schedulerDefinition("probe-1")})
if err != nil || len(report.Results) != 1 {
t.Fatalf("report=%+v err=%v", report, err)
}
if report.Results[0].State != "up" || report.Results[0].Attempts != 2 || report.Metrics.Retried != 1 {
t.Fatalf("report=%+v", report)
}
timeoutExecutor := executorFunc(func(ctx context.Context, _ Definition) (Result, error) { <-ctx.Done(); return Result{}, ctx.Err() })
timeoutScheduler, err := NewScheduler(timeoutExecutor, SchedulerConfig{MaxAttempts: 2, AttemptTimeout: 5 * time.Millisecond, RetryBackoff: 0})
if err != nil {
t.Fatal(err)
}
timeoutReport, err := timeoutScheduler.Run(context.Background(), []Definition{schedulerDefinition("timeout")})
if err != nil || timeoutReport.Results[0].ErrorClass != "timeout" || timeoutReport.Results[0].Attempts != 2 || timeoutReport.Metrics.TimedOut != 1 {
t.Fatalf("timeout=%+v err=%v", timeoutReport, err)
}
}
func TestSchedulerPreventsOverlappingRunsAndShutsDown(t *testing.T) {
started := make(chan struct{})
release := make(chan struct{})
executor := executorFunc(func(ctx context.Context, _ Definition) (Result, error) {
closeOnce(started)
select {
case <-release:
return Result{State: "up"}, nil
case <-ctx.Done():
return Result{}, ctx.Err()
}
})
scheduler, err := NewScheduler(executor, SchedulerConfig{MaxAttempts: 1, AttemptTimeout: time.Second})
if err != nil {
t.Fatal(err)
}
first := make(chan RunReport, 1)
go func() {
report, _ := scheduler.Run(context.Background(), []Definition{schedulerDefinition("same")})
first <- report
}()
<-started
second, err := scheduler.Run(context.Background(), []Definition{schedulerDefinition("same")})
if err != nil || len(second.Results) != 0 || second.Metrics.SkippedOverlap < 1 {
t.Fatalf("overlap=%+v err=%v", second, err)
}
shutdownDone := make(chan error, 1)
go func() { shutdownDone <- scheduler.Shutdown(context.Background()) }()
select {
case err := <-shutdownDone:
if err != nil {
t.Fatal(err)
}
case <-time.After(time.Second):
t.Fatal("shutdown did not cancel active probe")
}
close(release)
select {
case <-first:
case <-time.After(time.Second):
t.Fatal("run did not finish after shutdown")
}
if err := scheduler.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}
func TestSchedulerTargetScale300IsBoundedAndDeterministic(t *testing.T) {
var mu sync.Mutex
active, maximum := 0, 0
executor := executorFunc(func(ctx context.Context, definition Definition) (Result, error) {
mu.Lock()
active++
if active > maximum {
maximum = active
}
mu.Unlock()
select {
case <-ctx.Done():
return Result{}, ctx.Err()
default:
}
mu.Lock()
active--
mu.Unlock()
return Result{State: "up"}, nil
})
scheduler, err := NewScheduler(executor, SchedulerConfig{MaxConcurrent: 16, MaxAttempts: 1})
if err != nil {
t.Fatal(err)
}
definitions := make([]Definition, 300)
for index := range definitions {
definitions[index] = schedulerDefinition(fmt.Sprintf("probe-%03d", 300-index))
}
started := time.Now()
report, err := scheduler.Run(context.Background(), definitions)
if err != nil || len(report.Results) != 300 {
t.Fatalf("count=%d err=%v", len(report.Results), err)
}
if maximum > 16 {
t.Fatalf("maximum concurrency=%d", maximum)
}
if time.Since(started) > 2*time.Second {
t.Fatalf("300-probe run exceeded budget: %s", time.Since(started))
}
for index := 1; index < len(report.Results); index++ {
if report.Results[index-1].ProbeID > report.Results[index].ProbeID {
t.Fatal("results are not deterministic")
}
}
}
func closeOnce(channel chan struct{}) {
select {
case <-channel:
default:
close(channel)
}
}