Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/admission control multi layer support #2443

Merged
merged 1 commit into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions filters/shedder/admission.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,14 @@ func (m mode) String() string {
}

const (
counterPrefix = "shedder.admission_control."
admissionControlSpanName = "admission_control"
admissionControlKey = "shedder:admission_control"
admissionControlValue = "reject"
minWindowSize = 1
maxWindowSize = 100
counterPrefix = "shedder.admission_control."
admissionControlSpanName = "admission_control"
admissionSignalHeaderKey = "Admission-Control"
admissionSignalHeaderValue = "true"
admissionControlKey = "shedder:admission_control"
admissionControlValue = "reject"
minWindowSize = 1
maxWindowSize = 100
)

type Options struct {
Expand Down Expand Up @@ -422,18 +424,27 @@ func (ac *admissionControl) Request(ctx filters.FilterContext) {
if ac.mode != active {
return
}

header := make(http.Header)
header.Set(admissionSignalHeaderKey, admissionSignalHeaderValue)
ctx.Serve(&http.Response{
Header: header,
StatusCode: http.StatusServiceUnavailable,
})
}
}

func (ac *admissionControl) Response(ctx filters.FilterContext) {
// we don't want to count ourselves
// we don't want to count our short cutted responses as errors
if ctx.StateBag()[admissionControlKey] == admissionControlValue {
return
}

// we don't want to count other shedders in the call path as errors
if ctx.Response().Header.Get(admissionSignalHeaderKey) == admissionSignalHeaderValue {
return
}

if ctx.Response().StatusCode < 499 {
ac.successCounter.Add(1)
}
Expand Down
161 changes: 157 additions & 4 deletions filters/shedder/admission_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,30 @@ package shedder

import (
"fmt"
"io"
"math/rand"
"net/http"
"net/http/httptest"
"net/url"
"sort"
"sync"
"sync/atomic"
"testing"
"time"

"github.com/zalando/skipper/eskip"
"github.com/zalando/skipper/filters"
"github.com/zalando/skipper/filters/builtin"
"github.com/zalando/skipper/metrics"
"github.com/zalando/skipper/metrics/metricstest"
"github.com/zalando/skipper/net"
"github.com/zalando/skipper/proxy/proxytest"
"github.com/zalando/skipper/routing"
"github.com/zalando/skipper/routing/testdataclient"
"golang.org/x/time/rate"
)

func TestAdmissionControl(t *testing.T) {
// fixed rand to have non flaky tests
rand := rand.New(rand.NewSource(5))

for _, ti := range []struct {
msg string
mode string
Expand Down Expand Up @@ -92,7 +97,7 @@ func TestAdmissionControl(t *testing.T) {
exponent: 1.0,
N: 20,
pBackendErr: 0.2,
pExpectedAdmissionShedding: 0.1,
pExpectedAdmissionShedding: 0.15,
}, {
msg: "medium error rate and bigger than threshold will block some traffic",
mode: "active",
Expand All @@ -117,10 +122,37 @@ func TestAdmissionControl(t *testing.T) {
N: 20,
pBackendErr: 0.8,
pExpectedAdmissionShedding: 0.91,
}, {
msg: "inactive mode with large error rate and bigger than threshold will pass all traffic",
mode: "inactive",
d: 10 * time.Millisecond,
windowsize: 5,
minRequests: 10,
successThreshold: 0.9,
maxrejectprobability: 0.95,
exponent: 1.0,
N: 20,
pBackendErr: 0.8,
pExpectedAdmissionShedding: 0.0,
Comment on lines +129 to +136
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just trust these numbers make sense :)

Copy link
Member Author

@szuecs szuecs Aug 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I basically copied the last test that has pExpectedAdmissionShedding: 0.91, and for "inactive" I expect pExpectedAdmissionShedding: 0. Same for case "logInactive".

}, {
msg: "logInactive mode with large error rate and bigger than threshold will pass all traffic",
mode: "logInactive",
d: 10 * time.Millisecond,
windowsize: 5,
minRequests: 10,
successThreshold: 0.9,
maxrejectprobability: 0.95,
exponent: 1.0,
N: 20,
pBackendErr: 0.8,
pExpectedAdmissionShedding: 0.0,
}} {
t.Run(ti.msg, func(t *testing.T) {
var mu sync.Mutex
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
p := rand.Float64()
mu.Unlock()
if p < ti.pBackendErr {
w.WriteHeader(http.StatusInternalServerError)
} else {
Expand Down Expand Up @@ -208,6 +240,127 @@ func TestAdmissionControl(t *testing.T) {
}
}

func TestAdmissionControlChainOnlyBackendErrorPass(t *testing.T) {
dm := metrics.Default
t.Cleanup(func() { metrics.Default = dm })
m := &metricstest.MockMetrics{}
metrics.Default = m
defer m.Close()

// backend with 50% error rate
var cnt int64
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
i := atomic.AddInt64(&cnt, 1)
if i%2 == 0 {
w.WriteHeader(http.StatusInternalServerError)
} else {
w.WriteHeader(http.StatusOK)
}
}))
defer backend.Close()

spec := NewAdmissionControl(Options{}).(*AdmissionControlSpec)

argsLeaf := make([]interface{}, 0, 6)
argsLeaf = append(argsLeaf, "testmetric-leaf", "active", "5ms", 5, 5, 0.9, 0.95, 1.0)
fLeaf, err := spec.CreateFilter(argsLeaf)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think filter creation in this tests obscures the main test logic. Do we need it here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly we don't need it but creating invalid filters is easy to do with so many args, so I think it's better to bail out early.

if err != nil {
t.Fatalf("error creating leaf filter %q: %v", argsLeaf, err)
return
}
defer fLeaf.(*admissionControl).Close()

args := make([]interface{}, 0, 6)
args = append(args, "testmetric", "active", "50ms", 10, 10, 0.9, 0.95, 1.0)
f, err := spec.CreateFilter(args)
if err != nil {
t.Fatalf("error creating filter %q: %v", args, err)
return
}
defer f.(*admissionControl).Close()

fr := make(filters.Registry)
fr.Register(spec)

rLeaf := &eskip.Route{Filters: []*eskip.Filter{{Name: spec.Name(), Args: argsLeaf}}, Backend: backend.URL}
proxyLeaf := proxytest.New(fr, rLeaf)
defer proxyLeaf.Close()

r := &eskip.Route{Filters: []*eskip.Filter{{Name: spec.Name(), Args: args}}, Backend: proxyLeaf.URL}
proxy := proxytest.New(fr, r)
defer proxy.Close()

reqURL, err := url.Parse(proxy.URL)
if err != nil {
t.Fatalf("Failed to parse url %s: %v", proxy.URL, err)
}
Comment on lines +293 to +296
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proxy url is always valid, no need to parse it

Copy link
Member Author

@szuecs szuecs Aug 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but also does not hurt either.
I am used to always check the error.


rt := net.NewTransport(net.Options{})
defer rt.Close()
Comment on lines +298 to +299
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not proxy.Client() which is https://pkg.go.dev/net/http/httptest#Server.Client

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case I want to have a well configured connection pool. In cases which require a significant amount of requests to make sense we should rely on our configuration.


req, err := http.NewRequest("GET", reqURL.String(), nil)
if err != nil {
t.Error(err)
return
}

N := 1000
var wg sync.WaitGroup
wg.Add(N)
lim := rate.NewLimiter(50, 50)
now := time.Now()
for i := 0; i < N; i++ {
r := lim.Reserve()
for !r.OK() {
time.Sleep(10 * time.Microsecond)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes tests run longer. We need to think whether it is necessary and maybe we can mock the clock like in #2432 and #2453

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to have something that jitters the funcs, because otherwise you get errors from the runtime.

}
go func(i int) {
defer r.Cancel()
defer wg.Done()
rsp, err := rt.RoundTrip(req)
if err != nil {
t.Logf("roundtrip %d: %v", i, err)
} else {
io.Copy(io.Discard, rsp.Body)
rsp.Body.Close()
}
}(i)
}
wg.Wait()
t.Logf("test took: %s", time.Since(now))

var total, totalLeaf, rejects, rejectsLeaf int64
m.WithCounters(func(counters map[string]int64) {
t.Logf("counters: %+v", counters)
var ok bool

total, ok = counters["shedder.admission_control.total.testmetric"]
if !ok {
t.Error("Failed to find total shedder data")
}

rejectsLeaf, ok = counters["shedder.admission_control.reject.testmetric-leaf"]
if !ok {
t.Error("Failed to find rejectsLeaf shedder data")
}

totalLeaf, ok = counters["shedder.admission_control.total.testmetric-leaf"]
if !ok {
t.Error("Failed to find totalLeaf shedder data")
}
})

t.Logf("N: %d", N)
t.Logf("totalLeaf: %d, rejectLeaf: %d", totalLeaf, rejectsLeaf)
t.Logf("total: %d, reject: %d", total, rejects)

epsilon := 0.05
maxExpectedFails := int64(epsilon * float64(N)) // maybe 5% instead of 0.1%
if rejects > maxExpectedFails {
t.Fatalf("Failed to get less rejects as wanted: %d > %d", rejects, maxExpectedFails)
}
}

func TestAdmissionControlCleanupModes(t *testing.T) {
for _, ti := range []struct {
msg string
Expand Down