61a1a99112
Fixes #73 ## Why `Classifier.Classify` runs on every proxied request and recompiled the Blocklist/Patterns/Immutable/Mutable regex lists each time. Regex compilation is expensive and fully redundant. ## Changes - Memoise compilation in a `sync.Map` keyed by pattern text (`compileCached`); each distinct pattern compiles once and is reused. Patterns that fail to compile are cached as a typed nil so they are not retried. No invalidation needed since the pattern text is the key. ## Validation - `go test ./internal/proxy/` and `make e2e` pass. Reviewed-on: #88 Co-authored-by: Ben Vincent <ben@unkin.net> Co-committed-by: Ben Vincent <ben@unkin.net>
101 lines
2.2 KiB
Go
101 lines
2.2 KiB
Go
package proxy
|
|
|
|
import (
|
|
"regexp"
|
|
"sync"
|
|
|
|
"git.unkin.net/unkin/artifactapi/internal/provider"
|
|
"git.unkin.net/unkin/artifactapi/pkg/models"
|
|
)
|
|
|
|
type Classification int
|
|
|
|
const (
|
|
ClassImmutable Classification = iota
|
|
ClassMutable
|
|
ClassDenied
|
|
)
|
|
|
|
func (c Classification) String() string {
|
|
switch c {
|
|
case ClassImmutable:
|
|
return "immutable"
|
|
case ClassMutable:
|
|
return "mutable"
|
|
case ClassDenied:
|
|
return "denied"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
type Classifier struct {
|
|
provider provider.Provider
|
|
}
|
|
|
|
func NewClassifier(p provider.Provider) *Classifier {
|
|
return &Classifier{provider: p}
|
|
}
|
|
|
|
func (c *Classifier) Classify(remote models.Remote, path string) Classification {
|
|
if matchesAny(path, compilePatterns(remote.Blocklist)) {
|
|
return ClassDenied
|
|
}
|
|
|
|
if len(remote.Patterns) > 0 && !matchesAny(path, compilePatterns(remote.Patterns)) {
|
|
return ClassDenied
|
|
}
|
|
|
|
if matchesAny(path, compilePatterns(remote.ImmutablePatterns)) {
|
|
return ClassImmutable
|
|
}
|
|
|
|
if matchesAny(path, compilePatterns(remote.MutablePatterns)) {
|
|
return ClassMutable
|
|
}
|
|
|
|
if c.provider.Classify(path) == provider.Mutable {
|
|
return ClassMutable
|
|
}
|
|
|
|
return ClassImmutable
|
|
}
|
|
|
|
// patternCache memoises regex compilation. Classify runs on every proxied
|
|
// request and previously recompiled each remote's pattern lists every time;
|
|
// keying by the pattern string lets each distinct pattern compile once and
|
|
// then be reused, with no invalidation needed (the pattern text is the key).
|
|
// A pattern that fails to compile is cached as a typed nil so we don't retry.
|
|
var patternCache sync.Map // map[string]*regexp.Regexp
|
|
|
|
func compileCached(pattern string) *regexp.Regexp {
|
|
if v, ok := patternCache.Load(pattern); ok {
|
|
return v.(*regexp.Regexp)
|
|
}
|
|
re, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
re = nil
|
|
}
|
|
patternCache.Store(pattern, re)
|
|
return re
|
|
}
|
|
|
|
func compilePatterns(patterns []string) []*regexp.Regexp {
|
|
compiled := make([]*regexp.Regexp, 0, len(patterns))
|
|
for _, p := range patterns {
|
|
if re := compileCached(p); re != nil {
|
|
compiled = append(compiled, re)
|
|
}
|
|
}
|
|
return compiled
|
|
}
|
|
|
|
func matchesAny(path string, patterns []*regexp.Regexp) bool {
|
|
for _, re := range patterns {
|
|
if re.MatchString(path) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|