Mise en place de cache local au niveau du serveur pour améliorer les temps de traitement des requêtes #26
2
.gitignore
vendored
2
.gitignore
vendored
@ -9,3 +9,5 @@
|
|||||||
/data
|
/data
|
||||||
/out
|
/out
|
||||||
.dockerconfigjson
|
.dockerconfigjson
|
||||||
|
*.prof
|
||||||
|
proxy.test
|
7
Makefile
7
Makefile
@ -130,6 +130,13 @@ tools/grafterm/bin/grafterm:
|
|||||||
mkdir -p tools/grafterm/bin
|
mkdir -p tools/grafterm/bin
|
||||||
GOBIN=$(PWD)/tools/grafterm/bin go install github.com/slok/grafterm/cmd/grafterm@v0.2.0
|
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:
|
full-version:
|
||||||
@echo $(FULL_VERSION)
|
@echo $(FULL_VERSION)
|
||||||
|
|
||||||
|
31
doc/fr/tutorials/profiling.md
Normal file
31
doc/fr/tutorials/profiling.md
Normal file
@ -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
|
||||||
|
```
|
1
go.mod
1
go.mod
@ -93,6 +93,7 @@ require (
|
|||||||
go.opentelemetry.io/otel v1.21.0 // indirect
|
go.opentelemetry.io/otel v1.21.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.21.0 // indirect
|
go.opentelemetry.io/otel/trace v1.21.0 // indirect
|
||||||
golang.org/x/net v0.19.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/text v0.14.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/time v0.3.0 // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
|
4
go.sum
4
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-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-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.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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
6
internal/cache/cache.go
vendored
Normal file
6
internal/cache/cache.go
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
type Cache[K comparable, V any] interface {
|
||||||
|
Get(key K) (V, bool)
|
||||||
|
Set(key K, value V)
|
||||||
|
}
|
34
internal/cache/memory/cache.go
vendored
Normal file
34
internal/cache/memory/cache.go
vendored
Normal file
@ -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]{}
|
39
internal/cache/ttl/cache.go
vendored
Normal file
39
internal/cache/ttl/cache.go
vendored
Normal file
@ -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]{}
|
39
internal/cache/ttl/cache_test.go
vendored
Normal file
39
internal/cache/ttl/cache_test.go
vendored
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ package proxy
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/command/common"
|
"forge.cadoles.com/cadoles/bouncer/internal/command/common"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy"
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy"
|
||||||
@ -45,6 +46,7 @@ func RunCommand() *cli.Command {
|
|||||||
proxy.WithServerConfig(conf.Proxy),
|
proxy.WithServerConfig(conf.Proxy),
|
||||||
proxy.WithRedisConfig(conf.Redis),
|
proxy.WithRedisConfig(conf.Redis),
|
||||||
proxy.WithDirectorLayers(layers...),
|
proxy.WithDirectorLayers(layers...),
|
||||||
|
proxy.WithDirectorCacheTTL(time.Duration(conf.Proxy.Cache.TTL)),
|
||||||
)
|
)
|
||||||
|
|
||||||
addrs, srvErrs := srv.Start(ctx.Context)
|
addrs, srvErrs := srv.Start(ctx.Context)
|
||||||
|
@ -12,6 +12,18 @@ type ProxyServerConfig struct {
|
|||||||
Transport TransportConfig `yaml:"transport"`
|
Transport TransportConfig `yaml:"transport"`
|
||||||
Dial DialConfig `yaml:"dial"`
|
Dial DialConfig `yaml:"dial"`
|
||||||
Sentry SentryConfig `yaml:"sentry"`
|
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
|
// See https://pkg.go.dev/net/http#Transport
|
||||||
@ -58,13 +70,22 @@ func (c TransportConfig) AsTransport() *http.Transport {
|
|||||||
return httpTransport
|
return httpTransport
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultProxyServerConfig() ProxyServerConfig {
|
func NewDefaultTransportConfig() TransportConfig {
|
||||||
return ProxyServerConfig{
|
return TransportConfig{
|
||||||
HTTP: NewHTTPConfig("0.0.0.0", 8080),
|
ForceAttemptHTTP2: true,
|
||||||
Metrics: NewDefaultMetricsConfig(),
|
MaxIdleConns: 100,
|
||||||
Transport: NewDefaultTransportConfig(),
|
MaxIdleConnsPerHost: 100,
|
||||||
Dial: NewDefaultDialConfig(),
|
MaxConnsPerHost: 100,
|
||||||
Sentry: NewDefaultSentryConfig(),
|
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 {
|
type CacheConfig struct {
|
||||||
return TransportConfig{
|
TTL InterpolatedDuration `yaml:"ttl"`
|
||||||
ForceAttemptHTTP2: true,
|
}
|
||||||
MaxIdleConns: 100,
|
|
||||||
MaxIdleConnsPerHost: 100,
|
func NewDefaultCacheConfig() CacheConfig {
|
||||||
MaxConnsPerHost: 100,
|
return CacheConfig{
|
||||||
IdleConnTimeout: NewInterpolatedDuration(90 * time.Second),
|
TTL: *NewInterpolatedDuration(time.Second * 30),
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"forge.cadoles.com/Cadoles/go-proxy"
|
"forge.cadoles.com/Cadoles/go-proxy"
|
||||||
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
|
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/cache"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
@ -17,6 +18,9 @@ type Director struct {
|
|||||||
proxyRepository store.ProxyRepository
|
proxyRepository store.ProxyRepository
|
||||||
layerRepository store.LayerRepository
|
layerRepository store.LayerRepository
|
||||||
layerRegistry *LayerRegistry
|
layerRegistry *LayerRegistry
|
||||||
|
|
||||||
|
proxyCache cache.Cache[string, []*store.Proxy]
|
||||||
|
layerCache cache.Cache[string, []*store.Layer]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Director) rewriteRequest(r *http.Request) (*http.Request, error) {
|
func (d *Director) rewriteRequest(r *http.Request) (*http.Request, error) {
|
||||||
@ -88,7 +92,14 @@ MAIN:
|
|||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const proxiesCacheKey = "proxies"
|
||||||
|
|
||||||
func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) {
|
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))
|
headers, err := d.proxyRepository.QueryProxy(ctx, store.WithProxyQueryEnabled(true))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
@ -96,7 +107,7 @@ func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) {
|
|||||||
|
|
||||||
sort.Sort(store.ByProxyWeight(headers))
|
sort.Sort(store.ByProxyWeight(headers))
|
||||||
|
|
||||||
proxies := make([]*store.Proxy, 0, len(headers))
|
proxies = make([]*store.Proxy, 0, len(headers))
|
||||||
|
|
||||||
for _, h := range headers {
|
for _, h := range headers {
|
||||||
if !h.Enabled {
|
if !h.Enabled {
|
||||||
@ -111,10 +122,19 @@ func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) {
|
|||||||
proxies = append(proxies, proxy)
|
proxies = append(proxies, proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
d.proxyCache.Set(proxiesCacheKey, proxies)
|
||||||
|
|
||||||
return proxies, nil
|
return proxies, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([]*store.Layer, error) {
|
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))
|
headers, err := d.layerRepository.QueryLayers(ctx, proxyName, store.WithLayerQueryEnabled(true))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
@ -122,7 +142,7 @@ func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([]
|
|||||||
|
|
||||||
sort.Sort(store.ByLayerWeight(headers))
|
sort.Sort(store.ByLayerWeight(headers))
|
||||||
|
|
||||||
layers := make([]*store.Layer, 0, len(headers))
|
layers = make([]*store.Layer, 0, len(headers))
|
||||||
|
|
||||||
for _, h := range headers {
|
for _, h := range headers {
|
||||||
if !h.Enabled {
|
if !h.Enabled {
|
||||||
@ -137,6 +157,8 @@ func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([]
|
|||||||
layers = append(layers, layer)
|
layers = append(layers, layer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
d.layerCache.Set(cacheKey, layers)
|
||||||
|
|
||||||
return layers, nil
|
return layers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,8 +262,16 @@ func (d *Director) Middleware() proxy.Middleware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(proxyRepository store.ProxyRepository, layerRepository store.LayerRepository, layers ...Layer) *Director {
|
func New(proxyRepository store.ProxyRepository, layerRepository store.LayerRepository, funcs ...OptionFunc) *Director {
|
||||||
registry := NewLayerRegistry(layers...)
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
58
internal/proxy/director/options.go
Normal file
58
internal/proxy/director/options.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +1,27 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Option struct {
|
type Option struct {
|
||||||
ServerConfig config.ProxyServerConfig
|
ServerConfig config.ProxyServerConfig
|
||||||
RedisConfig config.RedisConfig
|
RedisConfig config.RedisConfig
|
||||||
DirectorLayers []director.Layer
|
DirectorLayers []director.Layer
|
||||||
|
DirectorCacheTTL time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionFunc func(*Option)
|
type OptionFunc func(*Option)
|
||||||
|
|
||||||
func defaultOption() *Option {
|
func defaultOption() *Option {
|
||||||
return &Option{
|
return &Option{
|
||||||
ServerConfig: config.NewDefaultProxyServerConfig(),
|
ServerConfig: config.NewDefaultProxyServerConfig(),
|
||||||
RedisConfig: config.NewDefaultRedisConfig(),
|
RedisConfig: config.NewDefaultRedisConfig(),
|
||||||
DirectorLayers: make([]director.Layer, 0),
|
DirectorLayers: make([]director.Layer, 0),
|
||||||
|
DirectorCacheTTL: 30 * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,3 +42,9 @@ func WithDirectorLayers(layers ...director.Layer) OptionFunc {
|
|||||||
opt.DirectorLayers = layers
|
opt.DirectorLayers = layers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithDirectorCacheTTL(ttl time.Duration) OptionFunc {
|
||||||
|
return func(opt *Option) {
|
||||||
|
opt.DirectorCacheTTL = ttl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
156
internal/proxy/proxy_test.go
Normal file
156
internal/proxy/proxy_test.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/go-proxy"
|
"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"
|
bouncerChi "forge.cadoles.com/cadoles/bouncer/internal/chi"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||||
@ -25,11 +27,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
serverConfig config.ProxyServerConfig
|
serverConfig config.ProxyServerConfig
|
||||||
redisConfig config.RedisConfig
|
redisConfig config.RedisConfig
|
||||||
directorLayers []director.Layer
|
directorLayers []director.Layer
|
||||||
proxyRepository store.ProxyRepository
|
directorCacheTTL time.Duration
|
||||||
layerRepository store.LayerRepository
|
proxyRepository store.ProxyRepository
|
||||||
|
layerRepository store.LayerRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) {
|
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(
|
director := director.New(
|
||||||
s.proxyRepository,
|
s.proxyRepository,
|
||||||
s.layerRepository,
|
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 {
|
if s.serverConfig.HTTP.UseRealIP {
|
||||||
@ -184,8 +201,9 @@ func NewServer(funcs ...OptionFunc) *Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Server{
|
return &Server{
|
||||||
serverConfig: opt.ServerConfig,
|
serverConfig: opt.ServerConfig,
|
||||||
redisConfig: opt.RedisConfig,
|
redisConfig: opt.RedisConfig,
|
||||||
directorLayers: opt.DirectorLayers,
|
directorLayers: opt.DirectorLayers,
|
||||||
|
directorCacheTTL: opt.DirectorCacheTTL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,6 +93,14 @@ proxy:
|
|||||||
credentials:
|
credentials:
|
||||||
prom: etheus
|
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)
|
# Configuration du transport HTTP(S)
|
||||||
# Voir https://pkg.go.dev/net/http#Transport
|
# Voir https://pkg.go.dev/net/http#Transport
|
||||||
transport:
|
transport:
|
||||||
|
Loading…
Reference in New Issue
Block a user