This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user