Public source validation / validate (push) Failing after 3m8s
161 lines
4.8 KiB
Go
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)
|
|
}
|
|
}
|