From 3a9fde9bc928e63b6dddfff83bc483fe29a25b71 Mon Sep 17 00:00:00 2001 From: William Petit Date: Tue, 28 May 2024 16:45:15 +0200 Subject: [PATCH] feat: improve perf by caching proxy and layers locally --- .gitignore | 2 + Makefile | 7 ++ doc/fr/tutorials/profiling.md | 31 ++++++ go.mod | 1 + go.sum | 4 +- internal/cache/cache.go | 6 ++ internal/cache/memory/cache.go | 34 ++++++ internal/cache/ttl/cache.go | 39 +++++++ internal/cache/ttl/cache_test.go | 39 +++++++ internal/command/server/proxy/run.go | 2 + internal/config/proxy_server.go | 58 ++++++---- internal/proxy/director/director.go | 40 ++++++- internal/proxy/director/options.go | 58 ++++++++++ internal/proxy/option.go | 22 ++-- internal/proxy/proxy_test.go | 156 +++++++++++++++++++++++++++ internal/proxy/server.go | 36 +++++-- misc/packaging/common/config.yml | 8 ++ 17 files changed, 498 insertions(+), 45 deletions(-) create mode 100644 doc/fr/tutorials/profiling.md create mode 100644 internal/cache/cache.go create mode 100644 internal/cache/memory/cache.go create mode 100644 internal/cache/ttl/cache.go create mode 100644 internal/cache/ttl/cache_test.go create mode 100644 internal/proxy/director/options.go create mode 100644 internal/proxy/proxy_test.go diff --git a/.gitignore b/.gitignore index 219ca77..1f15930 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ /data /out .dockerconfigjson +*.prof +proxy.test \ No newline at end of file diff --git a/Makefile b/Makefile index e914877..5ae896c 100644 --- a/Makefile +++ b/Makefile @@ -130,6 +130,13 @@ tools/grafterm/bin/grafterm: mkdir -p tools/grafterm/bin GOBIN=$(PWD)/tools/grafterm/bin go install github.com/slok/grafterm/cmd/grafterm@v0.2.0 +bench: + go test -bench=. -run '^$$' -count=10 ./... + +tools/benchstat/bin/benchstat: + mkdir -p tools/benchstat/bin + GOBIN=$(PWD)/tools/benchstat/bin go install golang.org/x/perf/cmd/benchstat@latest + full-version: @echo $(FULL_VERSION) diff --git a/doc/fr/tutorials/profiling.md b/doc/fr/tutorials/profiling.md new file mode 100644 index 0000000..ad26be2 --- /dev/null +++ b/doc/fr/tutorials/profiling.md @@ -0,0 +1,31 @@ +# Analyser les performances de Bouncer + +1. Lancer un benchmark du proxy + + ```shell + go test -bench=. -run '^$' -count=5 -cpuprofile bench_proxy.prof ./internal/proxy + ``` + +2. Visualiser les temps d'exécution + + ```shell + go tool pprof -web bench_proxy.prof + ``` + +3. Comparer les performances d'une exécution à l'autre + + ```shell + # Lancer un premier benchmark + go test -bench=. -run '^$' -count=10 ./internal/proxy > bench_before.txt + + # Faire des modifications sur les sources + + # Lancer un second benchmark + go test -bench=. -run '^$' -count=10 ./internal/proxy > bench_after.txt + + # Installer l'outil benchstat + make tools/benchstat/bin/benchstat + + # Comparer les rapports + tools/benchstat/bin/benchstat bench_before.txt bench_after.txt + ``` diff --git a/go.mod b/go.mod index 7db0c06..4f31540 100644 --- a/go.mod +++ b/go.mod @@ -93,6 +93,7 @@ require ( go.opentelemetry.io/otel v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect golang.org/x/net v0.19.0 // indirect + golang.org/x/sync v0.7.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index 22b7f46..7417ad1 100644 --- a/go.sum +++ b/go.sum @@ -405,8 +405,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/cache/cache.go b/internal/cache/cache.go new file mode 100644 index 0000000..cc60dd6 --- /dev/null +++ b/internal/cache/cache.go @@ -0,0 +1,6 @@ +package cache + +type Cache[K comparable, V any] interface { + Get(key K) (V, bool) + Set(key K, value V) +} diff --git a/internal/cache/memory/cache.go b/internal/cache/memory/cache.go new file mode 100644 index 0000000..0aadab3 --- /dev/null +++ b/internal/cache/memory/cache.go @@ -0,0 +1,34 @@ +package memory + +import ( + "sync" + + cache "forge.cadoles.com/cadoles/bouncer/internal/cache" +) + +type Cache[K comparable, V any] struct { + store *sync.Map +} + +// Get implements cache.Cache. +func (c *Cache[K, V]) Get(key K) (V, bool) { + raw, exists := c.store.Load(key) + if !exists { + return *new(V), false + } + + return raw.(V), exists +} + +// Set implements cache.Cache. +func (c *Cache[K, V]) Set(key K, value V) { + c.store.Store(key, value) +} + +func NewCache[K comparable, V any]() *Cache[K, V] { + return &Cache[K, V]{ + store: new(sync.Map), + } +} + +var _ cache.Cache[string, bool] = &Cache[string, bool]{} diff --git a/internal/cache/ttl/cache.go b/internal/cache/ttl/cache.go new file mode 100644 index 0000000..6dfba51 --- /dev/null +++ b/internal/cache/ttl/cache.go @@ -0,0 +1,39 @@ +package ttl + +import ( + "time" + + cache "forge.cadoles.com/cadoles/bouncer/internal/cache" +) + +type Cache[K comparable, V any] struct { + timestamps cache.Cache[K, time.Time] + values cache.Cache[K, V] + ttl time.Duration +} + +// Get implements cache.Cache. +func (c *Cache[K, V]) Get(key K) (V, bool) { + timestamp, exists := c.timestamps.Get(key) + if !exists || timestamp.Add(c.ttl).Before(time.Now()) { + return *new(V), false + } + + return c.values.Get(key) +} + +// Set implements cache.Cache. +func (c *Cache[K, V]) Set(key K, value V) { + c.timestamps.Set(key, time.Now()) + c.values.Set(key, value) +} + +func NewCache[K comparable, V any](values cache.Cache[K, V], timestamps cache.Cache[K, time.Time], ttl time.Duration) *Cache[K, V] { + return &Cache[K, V]{ + values: values, + timestamps: timestamps, + ttl: ttl, + } +} + +var _ cache.Cache[string, bool] = &Cache[string, bool]{} diff --git a/internal/cache/ttl/cache_test.go b/internal/cache/ttl/cache_test.go new file mode 100644 index 0000000..742cc03 --- /dev/null +++ b/internal/cache/ttl/cache_test.go @@ -0,0 +1,39 @@ +package ttl + +import ( + "testing" + "time" + + "forge.cadoles.com/cadoles/bouncer/internal/cache/memory" +) + +func TestCache(t *testing.T) { + cache := NewCache( + memory.NewCache[string, int](), + memory.NewCache[string, time.Time](), + time.Second, + ) + + key := "foo" + + if _, exists := cache.Get(key); exists { + t.Errorf("cache.Get(\"%s\"): should not exists", key) + } + + cache.Set(key, 1) + + value, exists := cache.Get(key) + if !exists { + t.Errorf("cache.Get(\"%s\"): should exists", key) + } + + if e, g := 1, value; e != g { + t.Errorf("cache.Get(\"%s\"): expected '%v', got '%v'", key, e, g) + } + + time.Sleep(time.Second) + + if _, exists := cache.Get("foo"); exists { + t.Errorf("cache.Get(\"%s\"): should not exists", key) + } +} diff --git a/internal/command/server/proxy/run.go b/internal/command/server/proxy/run.go index aba5089..ae0a876 100644 --- a/internal/command/server/proxy/run.go +++ b/internal/command/server/proxy/run.go @@ -3,6 +3,7 @@ package proxy import ( "fmt" "strings" + "time" "forge.cadoles.com/cadoles/bouncer/internal/command/common" "forge.cadoles.com/cadoles/bouncer/internal/proxy" @@ -45,6 +46,7 @@ func RunCommand() *cli.Command { proxy.WithServerConfig(conf.Proxy), proxy.WithRedisConfig(conf.Redis), proxy.WithDirectorLayers(layers...), + proxy.WithDirectorCacheTTL(time.Duration(conf.Proxy.Cache.TTL)), ) addrs, srvErrs := srv.Start(ctx.Context) diff --git a/internal/config/proxy_server.go b/internal/config/proxy_server.go index fa6f5a3..781b635 100644 --- a/internal/config/proxy_server.go +++ b/internal/config/proxy_server.go @@ -12,6 +12,18 @@ type ProxyServerConfig struct { Transport TransportConfig `yaml:"transport"` Dial DialConfig `yaml:"dial"` Sentry SentryConfig `yaml:"sentry"` + Cache CacheConfig `yaml:"cache"` +} + +func NewDefaultProxyServerConfig() ProxyServerConfig { + return ProxyServerConfig{ + HTTP: NewHTTPConfig("0.0.0.0", 8080), + Metrics: NewDefaultMetricsConfig(), + Transport: NewDefaultTransportConfig(), + Dial: NewDefaultDialConfig(), + Sentry: NewDefaultSentryConfig(), + Cache: NewDefaultCacheConfig(), + } } // See https://pkg.go.dev/net/http#Transport @@ -58,13 +70,22 @@ func (c TransportConfig) AsTransport() *http.Transport { return httpTransport } -func NewDefaultProxyServerConfig() ProxyServerConfig { - return ProxyServerConfig{ - HTTP: NewHTTPConfig("0.0.0.0", 8080), - Metrics: NewDefaultMetricsConfig(), - Transport: NewDefaultTransportConfig(), - Dial: NewDefaultDialConfig(), - Sentry: NewDefaultSentryConfig(), +func NewDefaultTransportConfig() TransportConfig { + return TransportConfig{ + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 100, + MaxConnsPerHost: 100, + IdleConnTimeout: NewInterpolatedDuration(90 * time.Second), + TLSHandshakeTimeout: NewInterpolatedDuration(10 * time.Second), + ExpectContinueTimeout: NewInterpolatedDuration(1 * time.Second), + ResponseHeaderTimeout: NewInterpolatedDuration(10 * time.Second), + DisableCompression: false, + DisableKeepAlives: false, + ReadBufferSize: 4096, + WriteBufferSize: 4096, + MaxResponseHeaderBytes: 0, + InsecureSkipVerify: false, } } @@ -85,21 +106,12 @@ func NewDefaultDialConfig() DialConfig { } } -func NewDefaultTransportConfig() TransportConfig { - return TransportConfig{ - ForceAttemptHTTP2: true, - MaxIdleConns: 100, - MaxIdleConnsPerHost: 100, - MaxConnsPerHost: 100, - IdleConnTimeout: NewInterpolatedDuration(90 * time.Second), - TLSHandshakeTimeout: NewInterpolatedDuration(10 * time.Second), - ExpectContinueTimeout: NewInterpolatedDuration(1 * time.Second), - ResponseHeaderTimeout: NewInterpolatedDuration(10 * time.Second), - DisableCompression: false, - DisableKeepAlives: false, - ReadBufferSize: 4096, - WriteBufferSize: 4096, - MaxResponseHeaderBytes: 0, - InsecureSkipVerify: false, +type CacheConfig struct { + TTL InterpolatedDuration `yaml:"ttl"` +} + +func NewDefaultCacheConfig() CacheConfig { + return CacheConfig{ + TTL: *NewInterpolatedDuration(time.Second * 30), } } diff --git a/internal/proxy/director/director.go b/internal/proxy/director/director.go index 5deab83..32458d6 100644 --- a/internal/proxy/director/director.go +++ b/internal/proxy/director/director.go @@ -7,6 +7,7 @@ import ( "forge.cadoles.com/Cadoles/go-proxy" "forge.cadoles.com/Cadoles/go-proxy/wildcard" + "forge.cadoles.com/cadoles/bouncer/internal/cache" "forge.cadoles.com/cadoles/bouncer/internal/store" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" @@ -17,6 +18,9 @@ type Director struct { proxyRepository store.ProxyRepository layerRepository store.LayerRepository layerRegistry *LayerRegistry + + proxyCache cache.Cache[string, []*store.Proxy] + layerCache cache.Cache[string, []*store.Layer] } func (d *Director) rewriteRequest(r *http.Request) (*http.Request, error) { @@ -88,7 +92,14 @@ MAIN: return r, nil } +const proxiesCacheKey = "proxies" + func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) { + proxies, exists := d.proxyCache.Get(proxiesCacheKey) + if exists { + return proxies, nil + } + headers, err := d.proxyRepository.QueryProxy(ctx, store.WithProxyQueryEnabled(true)) if err != nil { return nil, errors.WithStack(err) @@ -96,7 +107,7 @@ func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) { sort.Sort(store.ByProxyWeight(headers)) - proxies := make([]*store.Proxy, 0, len(headers)) + proxies = make([]*store.Proxy, 0, len(headers)) for _, h := range headers { if !h.Enabled { @@ -111,10 +122,19 @@ func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) { proxies = append(proxies, proxy) } + d.proxyCache.Set(proxiesCacheKey, proxies) + return proxies, nil } func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([]*store.Layer, error) { + cacheKey := "layers-" + string(proxyName) + + layers, exists := d.layerCache.Get(cacheKey) + if exists { + return layers, nil + } + headers, err := d.layerRepository.QueryLayers(ctx, proxyName, store.WithLayerQueryEnabled(true)) if err != nil { return nil, errors.WithStack(err) @@ -122,7 +142,7 @@ func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([] sort.Sort(store.ByLayerWeight(headers)) - layers := make([]*store.Layer, 0, len(headers)) + layers = make([]*store.Layer, 0, len(headers)) for _, h := range headers { if !h.Enabled { @@ -137,6 +157,8 @@ func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([] layers = append(layers, layer) } + d.layerCache.Set(cacheKey, layers) + return layers, nil } @@ -240,8 +262,16 @@ func (d *Director) Middleware() proxy.Middleware { } } -func New(proxyRepository store.ProxyRepository, layerRepository store.LayerRepository, layers ...Layer) *Director { - registry := NewLayerRegistry(layers...) +func New(proxyRepository store.ProxyRepository, layerRepository store.LayerRepository, funcs ...OptionFunc) *Director { + opts := NewOptions(funcs...) - return &Director{proxyRepository, layerRepository, registry} + registry := NewLayerRegistry(opts.Layers...) + + return &Director{ + proxyRepository: proxyRepository, + layerRepository: layerRepository, + layerRegistry: registry, + proxyCache: opts.ProxyCache, + layerCache: opts.LayerCache, + } } diff --git a/internal/proxy/director/options.go b/internal/proxy/director/options.go new file mode 100644 index 0000000..c785aeb --- /dev/null +++ b/internal/proxy/director/options.go @@ -0,0 +1,58 @@ +package director + +import ( + "time" + + "forge.cadoles.com/cadoles/bouncer/internal/cache" + "forge.cadoles.com/cadoles/bouncer/internal/cache/memory" + "forge.cadoles.com/cadoles/bouncer/internal/cache/ttl" + "forge.cadoles.com/cadoles/bouncer/internal/store" +) + +type Options struct { + Layers []Layer + ProxyCache cache.Cache[string, []*store.Proxy] + LayerCache cache.Cache[string, []*store.Layer] +} + +type OptionFunc func(opts *Options) + +func NewOptions(funcs ...OptionFunc) *Options { + opts := &Options{ + Layers: make([]Layer, 0), + ProxyCache: ttl.NewCache( + memory.NewCache[string, []*store.Proxy](), + memory.NewCache[string, time.Time](), + 30*time.Second, + ), + LayerCache: ttl.NewCache( + memory.NewCache[string, []*store.Layer](), + memory.NewCache[string, time.Time](), + 30*time.Second, + ), + } + + for _, fn := range funcs { + fn(opts) + } + + return opts +} + +func WithLayers(layers ...Layer) OptionFunc { + return func(opts *Options) { + opts.Layers = layers + } +} + +func WithProxyCache(cache cache.Cache[string, []*store.Proxy]) OptionFunc { + return func(opts *Options) { + opts.ProxyCache = cache + } +} + +func WithLayerCache(cache cache.Cache[string, []*store.Layer]) OptionFunc { + return func(opts *Options) { + opts.LayerCache = cache + } +} diff --git a/internal/proxy/option.go b/internal/proxy/option.go index e9b434b..aa1e2a4 100644 --- a/internal/proxy/option.go +++ b/internal/proxy/option.go @@ -1,23 +1,27 @@ package proxy import ( + "time" + "forge.cadoles.com/cadoles/bouncer/internal/config" "forge.cadoles.com/cadoles/bouncer/internal/proxy/director" ) type Option struct { - ServerConfig config.ProxyServerConfig - RedisConfig config.RedisConfig - DirectorLayers []director.Layer + ServerConfig config.ProxyServerConfig + RedisConfig config.RedisConfig + DirectorLayers []director.Layer + DirectorCacheTTL time.Duration } type OptionFunc func(*Option) func defaultOption() *Option { return &Option{ - ServerConfig: config.NewDefaultProxyServerConfig(), - RedisConfig: config.NewDefaultRedisConfig(), - DirectorLayers: make([]director.Layer, 0), + ServerConfig: config.NewDefaultProxyServerConfig(), + RedisConfig: config.NewDefaultRedisConfig(), + DirectorLayers: make([]director.Layer, 0), + DirectorCacheTTL: 30 * time.Second, } } @@ -38,3 +42,9 @@ func WithDirectorLayers(layers ...director.Layer) OptionFunc { opt.DirectorLayers = layers } } + +func WithDirectorCacheTTL(ttl time.Duration) OptionFunc { + return func(opt *Option) { + opt.DirectorCacheTTL = ttl + } +} diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go new file mode 100644 index 0000000..93f38f7 --- /dev/null +++ b/internal/proxy/proxy_test.go @@ -0,0 +1,156 @@ +package proxy_test + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "net/http/httputil" + "net/url" + "os" + "testing" + "time" + + "forge.cadoles.com/Cadoles/go-proxy" + "forge.cadoles.com/cadoles/bouncer/internal/cache/memory" + "forge.cadoles.com/cadoles/bouncer/internal/cache/ttl" + "forge.cadoles.com/cadoles/bouncer/internal/proxy/director" + "forge.cadoles.com/cadoles/bouncer/internal/store" + redisStore "forge.cadoles.com/cadoles/bouncer/internal/store/redis" + "github.com/pkg/errors" + "github.com/redis/go-redis/v9" +) + +func BenchmarkProxy(b *testing.B) { + redisEndpoint := os.Getenv("BOUNCER_BENCH_REDIS_ADDR") + if redisEndpoint == "" { + redisEndpoint = "127.0.0.1:6379" + } + + client := redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: []string{redisEndpoint}, + }) + + proxyRepository := redisStore.NewProxyRepository(client) + layerRepository := redisStore.NewLayerRepository(client) + + backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + if _, err := w.Write([]byte("Hello, world.")); err != nil { + b.Logf("[ERROR] %+v", errors.WithStack(err)) + } + })) + defer backend.Close() + + if err := waitFor(backend.URL, 5*time.Second); err != nil { + b.Fatalf("[FATAL] %+v", errors.WithStack(err)) + } + + b.Logf("started backend '%s'", backend.URL) + + ctx := context.Background() + + proxyName := store.ProxyName(b.Name()) + + b.Logf("creating proxy '%s'", proxyName) + + if err := proxyRepository.DeleteProxy(ctx, proxyName); err != nil { + b.Fatalf("[FATAL] %+v", errors.WithStack(err)) + } + + if _, err := proxyRepository.CreateProxy(ctx, proxyName, backend.URL, "*"); err != nil { + b.Fatalf("[FATAL] %+v", errors.WithStack(err)) + } + + if _, err := proxyRepository.UpdateProxy(ctx, proxyName, store.WithProxyUpdateEnabled(true)); err != nil { + b.Fatalf("[FATAL] %+v", errors.WithStack(err)) + } + + director := director.New( + proxyRepository, layerRepository, + director.WithLayerCache( + ttl.NewCache( + memory.NewCache[string, []*store.Layer](), + memory.NewCache[string, time.Time](), + 30*time.Second, + ), + ), + director.WithProxyCache( + ttl.NewCache( + memory.NewCache[string, []*store.Proxy](), + memory.NewCache[string, time.Time](), + 30*time.Second, + ), + ), + ) + + directorMiddleware := director.Middleware() + + handler := proxy.New( + proxy.WithRequestTransformers( + director.RequestTransformer(), + ), + proxy.WithResponseTransformers( + director.ResponseTransformer(), + ), + proxy.WithReverseProxyFactory(func(ctx context.Context, target *url.URL) *httputil.ReverseProxy { + reverse := httputil.NewSingleHostReverseProxy(target) + reverse.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { + b.Logf("[ERROR] %s", errors.WithStack(err)) + } + return reverse + }), + ) + + server := httptest.NewServer(directorMiddleware(handler)) + defer server.Close() + + b.Logf("started proxy '%s'", server.URL) + + httpClient := server.Client() + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + res, err := httpClient.Get(server.URL) + if err != nil { + b.Errorf("could not fetch server url: %+v", errors.WithStack(err)) + } + + body, err := io.ReadAll(res.Body) + if err != nil { + b.Errorf("could not read response body: %+v", errors.WithStack(err)) + } + + b.Logf("%s - %v", res.Status, string(body)) + + if err := res.Body.Close(); err != nil { + b.Errorf("could not close response body: %+v", errors.WithStack(err)) + } + } +} + +func waitFor(url string, ttl time.Duration) error { + var lastErr error + timeout := time.After(ttl) + for { + select { + case <-timeout: + if lastErr != nil { + return lastErr + } + + return errors.New("wait timed out") + default: + res, err := http.Get(url) + if err != nil { + lastErr = errors.WithStack(err) + continue + } + + if res.StatusCode >= 200 && res.StatusCode < 400 { + return nil + } + } + } +} diff --git a/internal/proxy/server.go b/internal/proxy/server.go index 8cd7f21..93fb5cc 100644 --- a/internal/proxy/server.go +++ b/internal/proxy/server.go @@ -11,6 +11,8 @@ import ( "time" "forge.cadoles.com/Cadoles/go-proxy" + "forge.cadoles.com/cadoles/bouncer/internal/cache/memory" + "forge.cadoles.com/cadoles/bouncer/internal/cache/ttl" bouncerChi "forge.cadoles.com/cadoles/bouncer/internal/chi" "forge.cadoles.com/cadoles/bouncer/internal/config" "forge.cadoles.com/cadoles/bouncer/internal/proxy/director" @@ -25,11 +27,12 @@ import ( ) type Server struct { - serverConfig config.ProxyServerConfig - redisConfig config.RedisConfig - directorLayers []director.Layer - proxyRepository store.ProxyRepository - layerRepository store.LayerRepository + serverConfig config.ProxyServerConfig + redisConfig config.RedisConfig + directorLayers []director.Layer + directorCacheTTL time.Duration + proxyRepository store.ProxyRepository + layerRepository store.LayerRepository } func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) { @@ -86,7 +89,21 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e director := director.New( s.proxyRepository, s.layerRepository, - s.directorLayers..., + director.WithLayers(s.directorLayers...), + director.WithLayerCache( + ttl.NewCache( + memory.NewCache[string, []*store.Layer](), + memory.NewCache[string, time.Time](), + s.directorCacheTTL, + ), + ), + director.WithProxyCache( + ttl.NewCache( + memory.NewCache[string, []*store.Proxy](), + memory.NewCache[string, time.Time](), + s.directorCacheTTL, + ), + ), ) if s.serverConfig.HTTP.UseRealIP { @@ -184,8 +201,9 @@ func NewServer(funcs ...OptionFunc) *Server { } return &Server{ - serverConfig: opt.ServerConfig, - redisConfig: opt.RedisConfig, - directorLayers: opt.DirectorLayers, + serverConfig: opt.ServerConfig, + redisConfig: opt.RedisConfig, + directorLayers: opt.DirectorLayers, + directorCacheTTL: opt.DirectorCacheTTL, } } diff --git a/misc/packaging/common/config.yml b/misc/packaging/common/config.yml index d512091..3dfd1c1 100644 --- a/misc/packaging/common/config.yml +++ b/misc/packaging/common/config.yml @@ -93,6 +93,14 @@ proxy: credentials: prom: etheus + # Configuration de la mise en cache + # locale des données proxy/layers + cache: + # Les proxys/layers sont mis en cache local pour une durée de 30s + # par défaut. Si les modifications sont rares, vous pouvez augmenter + # cette valeur pour réduire la "pression" sur le serveur Redis. + ttl: 30s + # Configuration du transport HTTP(S) # Voir https://pkg.go.dev/net/http#Transport transport: