openmeter / pkg /featuregate /featuregate.go
Leon4gr45's picture
Upload folder using huggingface_hub (part 9)
fea99b3 verified
Raw
History Blame Contribute Delete
4.36 kB
package featuregate
import (
"context"
"errors"
"fmt"
"net/http"
"slices"
"strings"
lru "github.com/hashicorp/golang-lru/v2"
"github.com/samber/lo"
"github.com/openmeterio/openmeter/pkg/framework/commonhttp"
"github.com/openmeterio/openmeter/pkg/framework/operation"
)
type Gate interface {
EvaluateBool(namespace, flag string, defaultValue bool) (bool, error)
}
func NewNoop() Gate {
return Noop{}
}
type Noop struct{}
func (n Noop) EvaluateBool(string, string, bool) (bool, error) {
return true, nil
}
var _ fmt.Stringer = (*FeatureFlag)(nil)
type FeatureFlag string
func (f FeatureFlag) String() string {
return string(f)
}
const (
CtxKeyCredits FeatureFlag = "om_ff_credits_enabled"
)
func ContextResolver() contextResolver {
return contextResolver{}
}
type contextResolver struct{}
func (r contextResolver) Credits(ctx context.Context) bool {
value, found := ctx.Value(CtxKeyCredits).(bool)
if !found {
return true
}
return value
}
type Flags map[FeatureFlag]string
func (f *Flags) Keys() []FeatureFlag {
return []FeatureFlag{CtxKeyCredits}
}
func (f *Flags) Validate() error {
if f == nil || len(*f) == 0 {
return errors.New("featuregate is enabled but missing flags setup")
}
keys := f.Keys()
for k := range *f {
if !slices.Contains(keys, k) {
return fmt.Errorf("invalid key: %s", k)
}
}
return nil
}
func (f *Flags) Credits() string {
if f == nil {
return ""
}
value, ok := (*f)[CtxKeyCredits]
if !ok {
return ""
}
return value
}
const defaultCacheSize = 1024
func NewFeatureGateChecker(gate Gate, flags Flags, flagOverrides map[FeatureFlag]bool) *FeatureGateChecker {
checker := &FeatureGateChecker{
Gate: gate,
Flags: flags,
FlagOverrides: flagOverrides,
}
cacheSize := defaultCacheSize
var err error
checker.store, err = lru.New[string, bool](cacheSize)
if err != nil {
return checker
}
return checker
}
type FeatureGateChecker struct {
Gate Gate
Flags Flags
// FlagOverrides is used to handle config level feature setups
// ex. if a feature is disabled on config level, then we are not going to call the feature gate
FlagOverrides map[FeatureFlag]bool
store *lru.Cache[string, bool]
}
func (h *FeatureGateChecker) Validate() error {
if h == nil || h.Gate == nil {
return errors.New("feature gate is required")
}
return nil
}
func (h *FeatureGateChecker) Enabled(ns string, flag string) (bool, error) {
if h == nil {
return true, nil
}
if h.Gate == nil {
return true, nil
}
if flag == "" {
return true, nil
}
cacheKey := strings.Join([]string{flag, ns}, "_")
flagResult, cached := h.getFromCache(cacheKey)
if !cached {
enabled, err := h.Gate.EvaluateBool(ns, flag, false)
if err != nil {
return false, err
}
h.addToCache(cacheKey, enabled)
return enabled, nil
}
return flagResult, nil
}
// getFromCache supposed to make cache fault tolerant
// so if store is not initialized, we return cache false
func (h FeatureGateChecker) getFromCache(key string) (bool, bool) {
if h.store == nil {
return false, false
}
return h.store.Get(key)
}
// addToCache supposed to make cache fault tolerant
// so if store is not initialized, we do an early exit
func (h FeatureGateChecker) addToCache(key string, value bool) {
if h.store == nil {
return
}
h.store.Add(key, value)
}
func NewMiddleware[Request any, Response any](getNamespace func(ctx context.Context) (string, bool), checker *FeatureGateChecker) operation.Middleware[Request, Response] {
return func(next operation.Operation[Request, Response]) operation.Operation[Request, Response] {
return func(ctx context.Context, request Request) (Response, error) {
ns, ok := getNamespace(ctx)
if !ok {
return lo.Empty[Response](), commonhttp.NewHTTPError(http.StatusInternalServerError, errors.New("internal server error"))
}
for _, contextFlagKey := range lo.Union(lo.Keys(checker.Flags), lo.Keys(checker.FlagOverrides)) {
if !checker.FlagOverrides[contextFlagKey] {
ctx = context.WithValue(ctx, contextFlagKey, false)
continue
}
configFlagKey := checker.Flags[contextFlagKey]
result, err := checker.Enabled(ns, configFlagKey)
if err != nil {
return lo.Empty[Response](), err
}
ctx = context.WithValue(ctx, contextFlagKey, result)
}
return next(ctx, request)
}
}
}