Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <[email protected]>
119 lines
3.6 KiB
Go
119 lines
3.6 KiB
Go
package httpclient
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"evobgp/internal/observability"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
)
|
|
|
|
func TestBreakerCanonicalHostAndActiveRetention(t *testing.T) {
|
|
c := breakerCache{entries: make(map[string]*breakerEntry), maximum: 1, idle: time.Minute}
|
|
now := time.Now()
|
|
first, key, release, err := c.acquire("Cache-Test.Example.", now)
|
|
if err != nil || key != "cache-test.example" {
|
|
t.Fatalf("canonical key=%s err=%v", key, err)
|
|
}
|
|
second, _, release2, err := c.acquire("cache-test.example", now)
|
|
if err != nil || first != second || len(c.entries) != 1 {
|
|
t.Fatal("alias bypassed shared breaker")
|
|
}
|
|
observability.SetUpstreamBreakerOpen(key, true)
|
|
first.openUntil = now.Add(3 * time.Minute)
|
|
if _, _, _, err := c.acquire("other.example", now.Add(2*time.Minute)); !errors.Is(err, ErrBreakerCapacity) {
|
|
t.Fatalf("active entry evicted: %v", err)
|
|
}
|
|
release()
|
|
release()
|
|
release2()
|
|
if _, _, _, err := c.acquire("other.example", now.Add(2*time.Minute+2*time.Second)); !errors.Is(err, ErrBreakerCapacity) {
|
|
t.Fatalf("open breaker evicted: %v", err)
|
|
}
|
|
_, _, done, err := c.acquire("other.example", now.Add(4*time.Minute))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer done()
|
|
if len(c.entries) != 1 || c.entries[key] != nil {
|
|
t.Fatal("idle closed entry retained")
|
|
}
|
|
families, err := prometheus.DefaultGatherer.Gather()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, family := range families {
|
|
if strings.HasSuffix(family.GetName(), "upstream_breaker_open") {
|
|
for _, metric := range family.Metric {
|
|
for _, label := range metric.Label {
|
|
if label.GetName() == "host" && label.GetValue() == key {
|
|
t.Fatal("evicted host metric retained")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type breakerTransport func(*http.Request) (*http.Response, error)
|
|
|
|
func (f breakerTransport) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
|
|
|
|
func TestBreakerCapacityRejectsBeforeDial(t *testing.T) {
|
|
ResetHostBreakers()
|
|
var releases []func()
|
|
defer func() {
|
|
for _, release := range releases {
|
|
release()
|
|
}
|
|
ResetHostBreakers()
|
|
}()
|
|
for i := 0; i < hostBreakers.maximum; i++ {
|
|
_, _, release, err := hostBreakers.acquire(fmt.Sprintf("capacity-%d.example", i), time.Now())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
releases = append(releases, release)
|
|
}
|
|
called := false
|
|
hc := &http.Client{Transport: breakerTransport(func(*http.Request) (*http.Response, error) { called = true; return nil, errors.New("unexpected dial") })}
|
|
req, _ := http.NewRequest(http.MethodGet, "https://overflow.example/", nil)
|
|
_, err := DoWithBreaker(context.Background(), hc, req, 1)
|
|
if !errors.Is(err, ErrBreakerCapacity) || called {
|
|
t.Fatalf("capacity err=%v dial=%t", err, called)
|
|
}
|
|
if len(SnapshotBreakers()) != hostBreakers.maximum {
|
|
t.Fatal("cache capacity changed")
|
|
}
|
|
ResetHostBreakers()
|
|
if len(SnapshotBreakers()) != hostBreakers.maximum {
|
|
t.Fatal("reset discarded in-flight references")
|
|
}
|
|
}
|
|
|
|
func TestBreakerRejectsOversizedIdentity(t *testing.T) {
|
|
c := breakerCache{entries: make(map[string]*breakerEntry), maximum: 1, idle: time.Minute}
|
|
if _, _, _, err := c.acquire(strings.Repeat("x", 254), time.Now()); err == nil || len(c.entries) != 0 {
|
|
t.Fatal("oversized identity retained")
|
|
}
|
|
name := strings.Repeat("a", 253)
|
|
first, key, release, err := c.acquire(name, time.Now())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer release()
|
|
second, alias, release2, err := c.acquire(strings.ToUpper(name)+".", time.Now())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer release2()
|
|
if first != second || key != alias {
|
|
t.Fatal("maximum length trailing-dot alias differs")
|
|
}
|
|
}
|