-
Notifications
You must be signed in to change notification settings - Fork 351
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
filters/auth: cache yaml config (#3225)
* filters/auth: benchmark jwtMetrics filter creation Signed-off-by: Alexander Yastrebov <alexander.yastrebov@zalando.de> * filters/auth: cache yaml config Cache parsed yaml config for `jwtMetrics` and `oauthTokeninfoValidate` filters to avoid parsing the same configuration over and over when these filters are appended as default filters to all routes. ``` │ HEAD~1 │ HEAD │ │ sec/op │ sec/op vs base │ JwtMetrics_CreateFilter-8 30777.00n ± 6% 18.45n ± 11% -99.94% (p=0.000 n=10) │ HEAD~1 │ HEAD │ │ B/op │ B/op vs base │ JwtMetrics_CreateFilter-8 24.33Ki ± 0% 0.00Ki ± 0% -100.00% (p=0.000 n=10) │ HEAD~1 │ HEAD │ │ allocs/op │ allocs/op vs base │ JwtMetrics_CreateFilter-8 180.0 ± 0% 0.0 ± 0% -100.00% (p=0.000 n=10) ``` Signed-off-by: Alexander Yastrebov <alexander.yastrebov@zalando.de> --------- Signed-off-by: Alexander Yastrebov <alexander.yastrebov@zalando.de>
- Loading branch information
1 parent
490de6a
commit 47d9e0d
Showing
5 changed files
with
259 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package auth | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/ghodss/yaml" | ||
) | ||
|
||
// yamlConfigParser parses and caches yaml configurations of type T. | ||
// Use [newYamlConfigParser] to create instances and ensure that *T implements [yamlConfig]. | ||
type yamlConfigParser[T any] struct { | ||
initialize func(*T) error | ||
cacheSize int | ||
cache map[string]*T | ||
} | ||
|
||
// yamlConfig must be implemented by config value pointer type. | ||
// It is used to initialize the value after parsing. | ||
type yamlConfig interface { | ||
initialize() error | ||
} | ||
|
||
// newYamlConfigParser creates a new parser with a given cache size. | ||
func newYamlConfigParser[T any, PT interface { | ||
*T | ||
yamlConfig | ||
}](cacheSize int) yamlConfigParser[T] { | ||
// We want user to specify config type T but ensure that *T implements [yamlConfig]. | ||
// | ||
// Type inference only works for functions but not for types | ||
// (see https://github.com/golang/go/issues/57270 and https://github.com/golang/go/issues/51527) | ||
// therefore we create instances using function with two type parameters | ||
// but second parameter is inferred from the first so the caller does not have to specify it. | ||
// | ||
// To use *T.initialize we setup initialize field | ||
return yamlConfigParser[T]{ | ||
initialize: func(v *T) error { return PT(v).initialize() }, | ||
cacheSize: cacheSize, | ||
cache: make(map[string]*T, cacheSize), | ||
} | ||
} | ||
|
||
// parseSingleArg calls [yamlConfigParser.parse] with the first string argument. | ||
// If args slice does not contain a single string, it returns an error. | ||
func (p *yamlConfigParser[T]) parseSingleArg(args []any) (*T, error) { | ||
if len(args) != 1 { | ||
return nil, fmt.Errorf("requires single string argument") | ||
} | ||
config, ok := args[0].(string) | ||
if !ok { | ||
return nil, fmt.Errorf("requires single string argument") | ||
} | ||
return p.parse(config) | ||
} | ||
|
||
// parse parses a yaml configuration or returns a cached value | ||
// if the exact configuration was already parsed before. | ||
// Returned value is shared by multiple callers and therefore must not be modified. | ||
func (p *yamlConfigParser[T]) parse(config string) (*T, error) { | ||
if v, ok := p.cache[config]; ok { | ||
return v, nil | ||
} | ||
|
||
v := new(T) | ||
if err := yaml.Unmarshal([]byte(config), v); err != nil { | ||
return nil, err | ||
} | ||
|
||
if err := p.initialize(v); err != nil { | ||
return nil, err | ||
} | ||
|
||
// evict random element if cache is full | ||
if p.cacheSize > 0 && len(p.cache) == p.cacheSize { | ||
for k := range p.cache { | ||
delete(p.cache, k) | ||
break | ||
} | ||
} | ||
|
||
p.cache[config] = v | ||
|
||
return v, nil | ||
} |
Oops, something went wrong.