Files
EvoBGP/internal/httpapi/operation_limits_test.go
DenozordecandCursor f8e0520817 feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь.

Co-authored-by: Cursor <[email protected]>
2026-11-02 17:23:59 +07:00

144 lines
3.9 KiB
Go

package httpapi
import (
"context"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"evobgp/internal/store"
)
func TestOperationLimitsSharedCapacityAndWindows(t *testing.T) {
l := newOperationLimiter()
l.maxActive = 2
l.maxTenantActive = 1
l.rates = [3]int{1, 2, 2}
now := time.Now()
a, _ := l.acquire("a", lookupOperation, now)
if a == nil {
t.Fatal("first denied")
}
if r, _ := l.acquire("a", cdnPreviewOperation, now); r != nil {
t.Fatal("tenant concurrency bypass")
}
b, _ := l.acquire("b", revisionPreviewOperation, now)
if b == nil {
t.Fatal("other tenant denied")
}
if r, _ := l.acquire("c", lookupOperation, now); r != nil {
t.Fatal("global concurrency bypass")
}
a()
a()
b()
if l.active != 0 {
t.Fatal("release not idempotent")
}
if r, retry := l.acquire("a", lookupOperation, now.Add(time.Second)); r != nil || retry != 59 {
t.Fatalf("rate bypass/retry admitted=%t seconds=%d", r != nil, retry)
}
c, _ := l.acquire("a", cdnPreviewOperation, now.Add(time.Second))
if c == nil {
t.Fatal("independent operation denied")
}
c()
d, _ := l.acquire("a", lookupOperation, now.Add(time.Minute))
if d == nil {
t.Fatal("expired window not reset")
}
d()
}
func TestOperationTenantMapBoundWithoutActiveEviction(t *testing.T) {
l := newOperationLimiter()
l.maxTenants = 1
now := time.Now()
a, _ := l.acquire("a", lookupOperation, now)
if r, _ := l.acquire("b", lookupOperation, now.Add(2*time.Minute)); r != nil {
t.Fatal("active entry evicted")
}
a()
b, _ := l.acquire("b", lookupOperation, now.Add(3*time.Minute))
if b == nil || len(l.tenants) != 1 {
t.Fatal("idle expiry did not free bounded state")
}
b()
}
func TestOperationConcurrentAdmissionBound(t *testing.T) {
l := newOperationLimiter()
l.maxActive = 4
l.maxTenantActive = 4
l.rates = [3]int{100, 100, 100}
results := make(chan func(), 32)
var wg sync.WaitGroup
for range 32 {
wg.Add(1)
go func() { defer wg.Done(); r, _ := l.acquire("tenant", lookupOperation, time.Now()); results <- r }()
}
wg.Wait()
close(results)
accepted := 0
for r := range results {
if r != nil {
accepted++
r()
}
}
if accepted != 4 || l.active != 0 {
t.Fatalf("accepted=%d active=%d", accepted, l.active)
}
}
type waitingRevisionStore struct {
store.Backend
entered chan struct{}
unblock chan struct{}
}
func (s waitingRevisionStore) GetRevision(tenant, id string) (*store.Revision, error) {
s.entered <- struct{}{}
<-s.unblock
return s.Backend.GetRevision(tenant, id)
}
func TestRevisionQuotaHeldUntilWorkReturns(t *testing.T) {
t.Setenv("EVOBGP_API_EXPENSIVE_TENANT_CONCURRENT", "1")
t.Setenv("EVOBGP_API_REVISION_PREVIEW_PER_MINUTE", "30")
m := store.NewMemory()
m.SeedDemo()
tenant, _, _, revision, _ := m.DemoIDs()
entered := make(chan struct{}, 2)
unblock := make(chan struct{})
s := &Server{store: waitingRevisionStore{Backend: m, entered: entered, unblock: unblock}}
request := func() *http.Request {
r := httptest.NewRequest(http.MethodGet, "/v1/revisions/"+revision+"/preview", nil)
r.SetPathValue("revision_id", revision)
return r.WithContext(context.WithValue(r.Context(), authCtxKey, Auth{TenantID: tenant, Role: "operator"}))
}
ctx, cancel := context.WithCancel(request().Context())
defer cancel()
first := httptest.NewRecorder()
done := make(chan struct{})
go func() { defer close(done); s.handleRevisionPreview(first, request().WithContext(ctx)) }()
<-entered
cancel() // Backend deliberately ignores cancellation: capacity stays reserved.
second := httptest.NewRecorder()
s.handleRevisionPreview(second, request())
if second.Code != http.StatusTooManyRequests || second.Header().Get("Retry-After") == "" {
close(unblock)
<-done
t.Fatalf("quota: %d %s", second.Code, second.Body.String())
}
close(unblock)
<-done
third := httptest.NewRecorder()
s.handleRevisionPreview(third, request())
if third.Code != http.StatusOK {
t.Fatalf("release failed: %d %s", third.Code, third.Body.String())
}
}