From 44accecc392a4389f404b30a2f17483f226ecfb2 Mon Sep 17 00:00:00 2001 From: darkweak Date: Sun, 3 Jul 2022 18:18:58 +0200 Subject: [PATCH] Optimization(all): Decrease allocs and responses time --- api/prometheus/prometheus.go | 4 +- docker-compose.yml.test | 11 +++ plugins/base.go | 79 +++++++------------ plugins/echo/souin_test.go | 11 ++- plugins/gin/souin_test.go | 1 + plugins/goyave/souin_test.go | 3 +- plugins/souin/main.go | 1 + plugins/souin/main_test.go | 56 +++++++++++++ .../github.com/darkweak/souin/rfc/bridge.go | 48 +++++------ plugins/webgo/souin_test.go | 13 +++ rfc/bridge.go | 48 +++++------ 11 files changed, 164 insertions(+), 111 deletions(-) create mode 100644 plugins/souin/main_test.go diff --git a/api/prometheus/prometheus.go b/api/prometheus/prometheus.go index ed1a36c05..26dbf5343 100644 --- a/api/prometheus/prometheus.go +++ b/api/prometheus/prometheus.go @@ -68,7 +68,9 @@ var registered map[string]interface{} // Increment will increment the counter. func Increment(name string) { - registered[name].(prometheus.Counter).Inc() + if _, ok := registered[name]; ok { + registered[name].(prometheus.Counter).Inc() + } } // Increment will add the referred value the counter. diff --git a/docker-compose.yml.test b/docker-compose.yml.test index 4c425c04c..c2124c5f0 100644 --- a/docker-compose.yml.test +++ b/docker-compose.yml.test @@ -36,6 +36,17 @@ services: - "2380:2380" command: sh -c 'apk update; apk add curl;etcd -listen-client-urls http://0.0.0.0:2379 -advertise-client-urls http://etcd:2379' <<: *networks + + traefik: + image: traefik:latest + command: --providers.docker + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + whoami: + image: traefik/whoami + labels: + - traefik.http.routers.whoami.rule=Host(`domain.com`) networks: your_network: external: true diff --git a/plugins/base.go b/plugins/base.go index 53780739e..be37526d6 100644 --- a/plugins/base.go +++ b/plugins/base.go @@ -108,69 +108,44 @@ func DefaultSouinPluginCallback( ) (e error) { prometheus.Increment(prometheus.RequestCounter) start := time.Now() - coalesceable := make(chan bool) - responses := make(chan *http.Response) - defer func() { - close(coalesceable) - close(responses) - }() cacheCandidate := !strings.Contains(req.Header.Get("Cache-Control"), "no-cache") cacheKey := req.Context().Value(context.Key).(string) retriever.SetMatchedURLFromRequest(req) - go func() { - defer func() { - _ = recover() - }() - coalesceable <- retriever.GetTransport().GetCoalescingLayerStorage().Exists(cacheKey) - }() - - if cacheCandidate { - go func() { - defer func() { - _ = recover() - }() - r, _ := rfc.CachedResponse( - retriever.GetProvider(), - req, - cacheKey, - retriever.GetTransport(), - false, - ) - - responses <- rfc.ValidateMaxAgeCachedResponse(req, r) - - r, _ = rfc.CachedResponse( - retriever.GetProvider(), - req, - "STALE_"+cacheKey, - retriever.GetTransport(), - false, - ) - - responses <- rfc.ValidateStaleCachedResponse(req, r) - }() - } - if cacheCandidate { - response, open := <-responses - if open && nil != response { - rh := response.Header - rfc.HitCache(&rh, retriever.GetMatchedURL().TTL.Duration) - prometheus.Increment(prometheus.CachedResponseCounter) - sendAnyCachedResponse(rh, response, res) - return + r, stale, err := rfc.CachedResponse( + retriever.GetProvider(), + req, + cacheKey, + retriever.GetTransport(), + ) + if err != nil { + retriever.GetConfiguration().GetLogger().Sugar().Debugf("An error ocurred while retrieving the (stale)? key %s: %v", cacheKey, err) + return err } - response, open = <-responses - if open && nil != response { - rh := response.Header - rfc.HitStaleCache(&rh, retriever.GetMatchedURL().TTL.Duration) - sendAnyCachedResponse(rh, response, res) + if r != nil { + rh := r.Header + if stale { + rfc.HitStaleCache(&rh, retriever.GetMatchedURL().TTL.Duration) + } else { + rfc.HitCache(&rh, retriever.GetMatchedURL().TTL.Duration) + prometheus.Increment(prometheus.CachedResponseCounter) + } + sendAnyCachedResponse(rh, r, res) + return } } + coalesceable := make(chan bool) + defer close(coalesceable) + go func() { + defer func() { + _ = recover() + }() + coalesceable <- retriever.GetTransport().GetCoalescingLayerStorage().Exists(cacheKey) + }() prometheus.Increment(prometheus.NoCachedResponseCounter) if <-coalesceable && rc != nil { rc.Temporize(req, res, nextMiddleware) diff --git a/plugins/echo/souin_test.go b/plugins/echo/souin_test.go index 9fa23f493..7c917bd2b 100644 --- a/plugins/echo/souin_test.go +++ b/plugins/echo/souin_test.go @@ -2,11 +2,15 @@ package souin import ( "encoding/json" + "fmt" "io/ioutil" "net/http" "net/http/httptest" "testing" + "time" + "github.com/darkweak/souin/configurationtypes" + "github.com/darkweak/souin/plugins" "github.com/labstack/echo/v4" ) @@ -91,7 +95,11 @@ func Test_SouinEchoPlugin_Process_APIHandle(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/souin-api/souin", nil) req.Header = http.Header{} res := httptest.NewRecorder() - s := New(DevDefaultConfiguration) + dc := DevDefaultConfiguration + dc.DefaultCache.Nuts = configurationtypes.CacheProvider{ + Path: "/tmp/souin" + time.Now().UTC().String(), + } + s := New(dc) e := echo.New() c := e.NewContext(req, res) @@ -107,6 +115,7 @@ func Test_SouinEchoPlugin_Process_APIHandle(t *testing.T) { } b, _ := ioutil.ReadAll(res.Result().Body) defer res.Result().Body.Close() + fmt.Println(string(b)) if string(b) != "[]" { t.Error("The response body must be an empty array because no request has been stored") } diff --git a/plugins/gin/souin_test.go b/plugins/gin/souin_test.go index 262369ee5..b9caa8817 100644 --- a/plugins/gin/souin_test.go +++ b/plugins/gin/souin_test.go @@ -7,6 +7,7 @@ import ( "net/http/httptest" "testing" + "github.com/darkweak/souin/plugins" "github.com/gin-gonic/gin" ) diff --git a/plugins/goyave/souin_test.go b/plugins/goyave/souin_test.go index 7f4bf1b3e..3e3f36362 100644 --- a/plugins/goyave/souin_test.go +++ b/plugins/goyave/souin_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/darkweak/souin/plugins" "goyave.dev/goyave/v4" "goyave.dev/goyave/v4/config" ) @@ -123,7 +124,7 @@ func (suite *HttpCacheMiddlewareTestSuite) Test_SouinFiberPlugin_Middleware_APIH if string(b) != "[]" { suite.T().Error("The response body must be an empty array because no request has been stored") } - res = suite.Middleware(httpcache.Handle, suite.CreateTestRequest(httptest.NewRequest(http.MethodGet, "/handled", nil)), func(response *goyave.Response, r *goyave.Request) { + _ = suite.Middleware(httpcache.Handle, suite.CreateTestRequest(httptest.NewRequest(http.MethodGet, "/handled", nil)), func(response *goyave.Response, r *goyave.Request) { response.String(http.StatusOK, "Hello, World 👋!") }) res = suite.Middleware(httpcache.Handle, SouinAPIRequest, func(response *goyave.Response, r *goyave.Request) {}) diff --git a/plugins/souin/main.go b/plugins/souin/main.go index d63080dcd..8f11d2548 100644 --- a/plugins/souin/main.go +++ b/plugins/souin/main.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "net/http" + _ "net/http/pprof" "net/url" "time" diff --git a/plugins/souin/main_test.go b/plugins/souin/main_test.go new file mode 100644 index 000000000..1ee59ac47 --- /dev/null +++ b/plugins/souin/main_test.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" + + "github.com/darkweak/souin/cache/coalescing" + "github.com/darkweak/souin/cache/service" + "github.com/darkweak/souin/errors" + "github.com/darkweak/souin/plugins" + "github.com/darkweak/souin/plugins/souin/configuration" + souintypes "github.com/darkweak/souin/plugins/souin/types" +) + +func Benchmark_Souin_Handler(b *testing.B) { + c := configuration.GetConfiguration() + fmt.Printf("%+v\n", c) + rc := coalescing.Initialize() + retriever := souinPluginInitializerFromConfiguration(c) + for i := 0; i < b.N; i++ { + writer := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "http://domain.com/"+strconv.Itoa(i), nil) + request.Header.Set("Date", time.Now().UTC().Format(time.RFC1123)) + request = retriever.GetContext().Method.SetContext(request) + + if !plugins.CanHandle(request, retriever) { + writer.Header().Set("Cache-Status", "Souin; fwd=uri-miss") + return + } + + request = retriever.GetContext().SetContext(request) + callback := func(rw http.ResponseWriter, rq *http.Request, ret souintypes.SouinRetrieverResponseProperties) error { + rr := service.RequestReverseProxy(rq, ret) + select { + case <-rq.Context().Done(): + c.GetLogger().Debug("The request was canceled by the user.") + return &errors.CanceledRequestContextError{} + default: + rr.Proxy.ServeHTTP(rw, rq) + } + + return nil + } + if plugins.HasMutation(request, writer) { + _ = callback(writer, request, *retriever) + } + retriever.SetMatchedURLFromRequest(request) + coalescing.ServeResponse(writer, request, retriever, plugins.DefaultSouinPluginCallback, rc, func(_ http.ResponseWriter, _ *http.Request) error { + return callback(writer, request, *retriever) + }) + } +} diff --git a/plugins/traefik/vendor/github.com/darkweak/souin/rfc/bridge.go b/plugins/traefik/vendor/github.com/darkweak/souin/rfc/bridge.go index afc805991..ec8909301 100644 --- a/plugins/traefik/vendor/github.com/darkweak/souin/rfc/bridge.go +++ b/plugins/traefik/vendor/github.com/darkweak/souin/rfc/bridge.go @@ -18,21 +18,23 @@ const ( // CachedResponse returns the cached http.Response for req if present, and nil // otherwise. -func CachedResponse(c types.AbstractProviderInterface, req *http.Request, cachedKey string, transport types.TransportInterface, update bool) (*http.Response, error) { +func CachedResponse(c types.AbstractProviderInterface, req *http.Request, cachedKey string, transport types.TransportInterface) (*http.Response, bool, error) { clonedReq := cloneRequest(req) cachedVal := c.Prefix(cachedKey, req) - b := bytes.NewBuffer(cachedVal) - response, _ := http.ReadResponse(bufio.NewReader(b), clonedReq) + response, _ := http.ReadResponse(bufio.NewReader(bytes.NewBuffer(cachedVal)), clonedReq) - if update && nil != response && ValidateCacheControl(response) { + if nil != response && ValidateCacheControl(response) { SetCacheStatusEventually(response) - go func() { - clonedReq.Response = response - // Update current cached response in background - _, _ = transport.UpdateCacheEventually(clonedReq) - }() + return ValidateMaxAgeCachedResponse(req, response), false, nil + } else if response == nil { + staleCachedVal := c.Prefix(cachedKey, req) + response, _ = http.ReadResponse(bufio.NewReader(bytes.NewBuffer(staleCachedVal)), clonedReq) + if nil != response && ValidateCacheControl(response) { + SetCacheStatusEventually(response) + } + return ValidateMaxAgeCachedResponse(req, response), true, nil } - return response, nil + return nil, false, nil } func commonCacheControl(req *http.Request, t http.RoundTripper) (*http.Response, error) { @@ -54,31 +56,18 @@ func (t *VaryTransport) deleteCache(key string) { } // BaseRoundTrip is the base for RoundTrip -func (t *VaryTransport) BaseRoundTrip(req *http.Request, shouldReUpdate bool) (string, bool, *http.Response) { +func (t *VaryTransport) BaseRoundTrip(req *http.Request) (string, bool, *http.Response) { cacheKey := req.Context().Value(context.Key).(string) cacheable := IsVaryCacheable(req) - cachedResp := req.Response - if cachedResp == nil { - cachedResp = new(http.Response) - } - if cachedResp.Header == nil { - cachedResp.Header = make(http.Header) - } - - if cacheable { - cr, _ := CachedResponse(t.GetProvider(), req, cacheKey, t, shouldReUpdate) - if cr != nil { - cachedResp = cr - } - } else { + if !cacheable { go func() { t.CoalescingLayerStorage.Set(cacheKey) }() t.deleteCache(cacheKey) } - return cacheKey, cacheable, cachedResp + return cacheKey, cacheable, req.Response } func commonVaryMatchesVerification(cachedResp *http.Response, req *http.Request) *http.Response { @@ -122,10 +111,13 @@ func (t *VaryTransport) UpdateCacheEventually(req *http.Request) (*http.Response req.Response.Header.Set("Cache-Control", t.ConfigurationURL.DefaultCacheControl) } - cacheKey, cacheable, cachedResp := t.BaseRoundTrip(req, false) + cacheKey, cacheable, cachedResp := t.BaseRoundTrip(req) if cacheable && cachedResp != nil { rDate, _ := time.Parse(time.RFC1123, req.Header.Get("Date")) + if cachedResp.Header == nil { + cachedResp.Header = http.Header{} + } cachedResp.Header.Set("Date", rDate.Format(http.TimeFormat)) } else { if _, err := commonCacheControl(req, t); err != nil { @@ -153,7 +145,7 @@ func (t *VaryTransport) UpdateCacheEventually(req *http.Request) (*http.Response // to give the server a chance to respond with NotModified. If this happens, then the cached Response // will be returned. func (t *VaryTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - cacheKey, cacheable, cachedResp := t.BaseRoundTrip(req, true) + cacheKey, cacheable, cachedResp := t.BaseRoundTrip(req) transport := t.Transport.Transport if transport == nil { diff --git a/plugins/webgo/souin_test.go b/plugins/webgo/souin_test.go index e89c3915f..80a3d20f2 100644 --- a/plugins/webgo/souin_test.go +++ b/plugins/webgo/souin_test.go @@ -5,10 +5,12 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "strconv" "testing" "time" "github.com/bnkamalesh/webgo/v6" + "github.com/darkweak/souin/plugins" ) func Test_NewHTTPCache(t *testing.T) { @@ -61,6 +63,17 @@ func prepare() (res *httptest.ResponseRecorder, res2 *httptest.ResponseRecorder, return } +func Benchmark_SouinWebgoPlugin_Middleware(b *testing.B) { + for i := 0; i < b.N; i++ { + res := httptest.NewRecorder() + httpcache := NewHTTPCache(DevDefaultConfiguration) + httpcache.Middleware(res, httptest.NewRequest(http.MethodGet, "/handled"+strconv.Itoa(i), nil), func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("Returns something")) + }) + } +} + func Test_SouinWebgoPlugin_Middleware(t *testing.T) { res, res2, router := prepare() req := httptest.NewRequest(http.MethodGet, "/handled", nil) diff --git a/rfc/bridge.go b/rfc/bridge.go index afc805991..ec8909301 100644 --- a/rfc/bridge.go +++ b/rfc/bridge.go @@ -18,21 +18,23 @@ const ( // CachedResponse returns the cached http.Response for req if present, and nil // otherwise. -func CachedResponse(c types.AbstractProviderInterface, req *http.Request, cachedKey string, transport types.TransportInterface, update bool) (*http.Response, error) { +func CachedResponse(c types.AbstractProviderInterface, req *http.Request, cachedKey string, transport types.TransportInterface) (*http.Response, bool, error) { clonedReq := cloneRequest(req) cachedVal := c.Prefix(cachedKey, req) - b := bytes.NewBuffer(cachedVal) - response, _ := http.ReadResponse(bufio.NewReader(b), clonedReq) + response, _ := http.ReadResponse(bufio.NewReader(bytes.NewBuffer(cachedVal)), clonedReq) - if update && nil != response && ValidateCacheControl(response) { + if nil != response && ValidateCacheControl(response) { SetCacheStatusEventually(response) - go func() { - clonedReq.Response = response - // Update current cached response in background - _, _ = transport.UpdateCacheEventually(clonedReq) - }() + return ValidateMaxAgeCachedResponse(req, response), false, nil + } else if response == nil { + staleCachedVal := c.Prefix(cachedKey, req) + response, _ = http.ReadResponse(bufio.NewReader(bytes.NewBuffer(staleCachedVal)), clonedReq) + if nil != response && ValidateCacheControl(response) { + SetCacheStatusEventually(response) + } + return ValidateMaxAgeCachedResponse(req, response), true, nil } - return response, nil + return nil, false, nil } func commonCacheControl(req *http.Request, t http.RoundTripper) (*http.Response, error) { @@ -54,31 +56,18 @@ func (t *VaryTransport) deleteCache(key string) { } // BaseRoundTrip is the base for RoundTrip -func (t *VaryTransport) BaseRoundTrip(req *http.Request, shouldReUpdate bool) (string, bool, *http.Response) { +func (t *VaryTransport) BaseRoundTrip(req *http.Request) (string, bool, *http.Response) { cacheKey := req.Context().Value(context.Key).(string) cacheable := IsVaryCacheable(req) - cachedResp := req.Response - if cachedResp == nil { - cachedResp = new(http.Response) - } - if cachedResp.Header == nil { - cachedResp.Header = make(http.Header) - } - - if cacheable { - cr, _ := CachedResponse(t.GetProvider(), req, cacheKey, t, shouldReUpdate) - if cr != nil { - cachedResp = cr - } - } else { + if !cacheable { go func() { t.CoalescingLayerStorage.Set(cacheKey) }() t.deleteCache(cacheKey) } - return cacheKey, cacheable, cachedResp + return cacheKey, cacheable, req.Response } func commonVaryMatchesVerification(cachedResp *http.Response, req *http.Request) *http.Response { @@ -122,10 +111,13 @@ func (t *VaryTransport) UpdateCacheEventually(req *http.Request) (*http.Response req.Response.Header.Set("Cache-Control", t.ConfigurationURL.DefaultCacheControl) } - cacheKey, cacheable, cachedResp := t.BaseRoundTrip(req, false) + cacheKey, cacheable, cachedResp := t.BaseRoundTrip(req) if cacheable && cachedResp != nil { rDate, _ := time.Parse(time.RFC1123, req.Header.Get("Date")) + if cachedResp.Header == nil { + cachedResp.Header = http.Header{} + } cachedResp.Header.Set("Date", rDate.Format(http.TimeFormat)) } else { if _, err := commonCacheControl(req, t); err != nil { @@ -153,7 +145,7 @@ func (t *VaryTransport) UpdateCacheEventually(req *http.Request) (*http.Response // to give the server a chance to respond with NotModified. If this happens, then the cached Response // will be returned. func (t *VaryTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - cacheKey, cacheable, cachedResp := t.BaseRoundTrip(req, true) + cacheKey, cacheable, cachedResp := t.BaseRoundTrip(req) transport := t.Transport.Transport if transport == nil {