1 Star 9 Fork 0

Feature Probe/server-sdk-go

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
克隆/下载
evaluate.go 14.83 KB
一键复制 编辑 原始数据 按行查看 历史
jianggang 提交于 2023-06-01 20:30 +08:00 . :bug:fix: repo map concurrency issue (#21)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
package featureprobe
import (
"crypto/sha1"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/masterminds/semver"
)
type Repository struct {
toggles atomic.Value
segments atomic.Value
debugUntilTime atomic.Uint64
}
type RepositoryData struct {
Toggles map[string]Toggle `json:"toggles"`
Segments map[string]Segment `json:"segments"`
DebugUntilTime uint64 `json:"debugUntilTime"`
}
type Toggles struct {
Toggles map[string]Toggle `json:"toggles"`
Segments map[string]Segment `json:"segments,omitempty"`
}
type Toggle struct {
Key string `json:"key"`
Enabled bool `json:"enabled"`
TrackAccessEvents bool `json:"trackAccessEvents"`
LastModified uint64 `json:"lastModified"`
Version uint64 `json:"version"`
ForClient bool `json:"forClient"`
DisabledServe Serve `json:"disabledServe"`
DefaultServe Serve `json:"defaultServe"`
Rules []Rule `json:"rules"`
Variations []interface{} `json:"variations"`
Prerequisites []Prerequisite `json:"prerequisites"`
}
type Segment struct {
Key string `json:"key"`
UniqId string `json:"uniqueId"`
Version uint64 `json:"version"`
Rules []Rule `json:"rules"`
}
type Serve struct {
Select *int `json:"select,omitempty"`
Split *Split `json:"split,omitempty"`
}
type Rule struct {
Serve Serve `json:"serve"`
Conditions []Condition `json:"conditions"`
}
type Split struct {
Distribution [][]Range `json:"distribution"`
BucketBy string `json:"bucketBy,omitempty"`
Salt string `json:"salt,omitempty"`
}
type Range struct {
Lower int `json:"-"`
Upper int `json:"-"`
}
type Condition struct {
Type string `json:"type"`
Subject string `json:"subject"`
Predicate string `json:"predicate"`
Objects []string `json:"objects"`
}
type EvalParam struct {
Key string
IsDetail bool
User FPUser
Variations []interface{}
Segments map[string]Segment
}
type EvalDetail struct {
Value interface{}
RuleIndex *int
VariationIndex *int
Version *uint64
Reason string
}
type Prerequisite struct {
Key string `json:"key"`
Value interface{} `json:"value"`
}
var (
ErrPrerequisiteNotExist = errors.New("prerequisite toggle not exist")
ErrPrerequisiteDeepOverflow = errors.New("prerequisite deep overflow")
)
func saltHash(key string, salt string, bucketSize uint32) int {
h := sha1.New()
h.Write([]byte(key + salt))
bytes := h.Sum(nil)
size := len(bytes)
value := binary.BigEndian.Uint32(bytes[size-4 : size])
// avoid negative number mod
mod := int64(value) % int64(bucketSize)
return int(mod)
}
func (r *Range) UnmarshalJSON(data []byte) error {
var raw []int
err := json.Unmarshal(data, &raw)
if err != nil {
return err
}
if len(raw) != 2 {
return fmt.Errorf("invalid distribution range")
}
*r = Range{
Lower: raw[0],
Upper: raw[1],
}
return nil
}
func (t *Toggle) eval(user FPUser, toggles map[string]Toggle, segments map[string]Segment, defaultValue interface{}, deep int) (interface{}, error) {
detail, err := t.evalDetail(user, toggles, segments, defaultValue, deep)
return detail.Value, err
}
func (t *Toggle) evalDetail(user FPUser, toggles map[string]Toggle, segments map[string]Segment, defaultValue interface{}, deep int) (EvalDetail, error) {
detail, err := t.doEvalDetail(user, toggles, segments, defaultValue, deep)
if err == ErrPrerequisiteDeepOverflow || err == ErrPrerequisiteNotExist {
defaultDetail, evalErr := t.createDefaultEvalDetail(EvalParam{
User: user,
Segments: segments,
Variations: t.Variations,
Key: t.Key,
}, defaultValue)
if evalErr == nil {
defaultDetail.Reason = err.Error()
}
return defaultDetail, evalErr
}
return detail, err
}
func (t *Toggle) prerequisite(user FPUser, toggles map[string]Toggle, segments map[string]Segment, defaultValue interface{}, deep int) (bool, error) {
if t.Prerequisites == nil && len(t.Prerequisites) == 0 {
return true, nil
}
for _, prerequisite := range t.Prerequisites {
toggle, exists := toggles[prerequisite.Key]
if !exists {
return false, ErrPrerequisiteNotExist
}
result, err := toggle.doEvalDetail(user, toggles, segments, defaultValue, deep-1)
if err != nil {
return false, err
}
if result.Value == nil || fmt.Sprintf("%v", result.Value) != fmt.Sprintf("%v", prerequisite.Value) {
return false, nil
}
}
return true, nil
}
func (t *Toggle) doEvalDetail(user FPUser, toggles map[string]Toggle, segments map[string]Segment, defaultValue interface{}, deep int) (EvalDetail, error) {
if deep <= 0 {
return t.buildEvalDetail(defaultValue, nil, nil, ""), ErrPrerequisiteDeepOverflow
}
params := EvalParam{
User: user,
Segments: segments,
Variations: t.Variations,
Key: t.Key,
}
if !t.Enabled {
serve, index, err := t.DisabledServe.selectVariation(params)
if err != nil {
return t.buildEvalDetail(defaultValue, nil, nil, err.Error()), err
}
return t.buildEvalDetail(serve, nil, index, "disabled"), nil
}
match, err := t.prerequisite(user, toggles, segments, defaultValue, deep)
if err != nil {
return t.buildEvalDetail(defaultValue, nil, nil, ""), err
}
if !match {
return t.createDefaultEvalDetail(params, defaultValue)
}
for ruleIndex, rule := range t.Rules {
serve, vi, err := rule.serveVariation(params)
if err != nil {
return t.buildEvalDetail(defaultValue, &ruleIndex, nil, err.Error()), err
}
if serve != nil {
return t.buildEvalDetail(serve, &ruleIndex, vi, fmt.Sprintf("rule %d ", ruleIndex)), nil
}
}
return t.createDefaultEvalDetail(params, defaultValue)
}
func (t *Toggle) createDefaultEvalDetail(params EvalParam, defaultValue interface{}) (EvalDetail, error) {
serve, vi, err := t.DefaultServe.selectVariation(params)
if err != nil {
return t.buildEvalDetail(defaultValue, nil, nil, err.Error()), err
}
return t.buildEvalDetail(serve, nil, vi, "default"), nil
}
func (t *Toggle) buildEvalDetail(value interface{}, ruleIndex *int, variationIndex *int, reason string) EvalDetail {
return EvalDetail{
Value: value,
VariationIndex: variationIndex,
RuleIndex: ruleIndex,
Version: &t.Version,
Reason: reason,
}
}
func (s *Serve) selectVariation(params EvalParam) (interface{}, *int, error) {
var index *int = nil
if s.Select != nil {
index = s.Select
} else {
i, err := s.Split.findIndex(params)
if err != nil {
return nil, nil, err
}
index = &i
}
length := len(params.Variations)
if *index >= length {
return nil, nil, fmt.Errorf("index %d overflow, variations count is %d", index, length)
}
return params.Variations[*index], index, nil
}
func (s *Split) findIndex(params EvalParam) (int, error) {
hashKey, err := s.hashKey(params)
if err != nil {
return -1, err
}
var salt string
if len(s.Salt) == 0 {
salt = params.Key
} else {
salt = s.Salt
}
bucketIndex := saltHash(hashKey, salt, 10000)
variation := s.getVariation(bucketIndex)
if variation == -1 {
return variation, fmt.Errorf("not find hash_bucket in distribution")
}
return variation, nil
}
func (s *Split) getVariation(bucketIndex int) int {
for v, d := range s.Distribution {
for _, r := range d {
if r.Lower <= bucketIndex && bucketIndex < r.Upper {
return v
}
}
}
return -1
}
func (s *Split) hashKey(params EvalParam) (string, error) {
var hashKey string
user := params.User
if len(s.BucketBy) == 0 {
hashKey = user.Key()
} else {
bucketBy := s.BucketBy
key := user.Get(bucketBy)
if len(key) != 0 {
hashKey = key
} else {
return "", fmt.Errorf("user with id: %s does not have attribute named: [%s]", user.Key(), key)
}
}
return hashKey, nil
}
func (r *Rule) serveVariation(params EvalParam) (interface{}, *int, error) {
for _, c := range r.Conditions {
if !c.meet(params.User, params.Segments) {
return nil, nil, nil
}
}
return r.Serve.selectVariation(params)
}
func (c *Condition) meet(user FPUser, segments map[string]Segment) bool {
switch c.Type {
case "string":
return c.matchStringCondition(user, c.Predicate)
case "segment":
return c.matchSegmentCondition(user, c.Predicate, segments)
case "datetime":
return c.matchDatetimeCondition(user, c.Predicate)
case "semver":
return c.matchSemverCondition(user, c.Predicate)
case "number":
return c.matchNumberCondition(user, c.Predicate)
}
return false
}
func (c *Condition) matchStringCondition(user FPUser, predicate string) bool {
if !user.ContainAttr(c.Subject) {
return false
}
customValue := user.Get(c.Subject)
switch predicate {
case "is one of":
return c.matchObjects(func(o string) bool { return customValue == o })
case "starts with":
return c.matchObjects(func(o string) bool { return strings.HasPrefix(customValue, o) })
case "ends with":
return c.matchObjects(func(o string) bool { return strings.HasSuffix(customValue, o) })
case "contains":
return c.matchObjects(func(o string) bool { return strings.Contains(customValue, o) })
case "matches regex":
return c.matchObjects(func(o string) bool {
matched, err := regexp.Match(o, []byte(customValue))
if err != nil {
return false
}
return matched
})
case "is not any of":
return !c.matchStringCondition(user, "is one of")
case "does not start with":
return !c.matchStringCondition(user, "starts with")
case "does not end with":
return !c.matchStringCondition(user, "ends with")
case "does not contain":
return !c.matchStringCondition(user, "contains")
case "does not match regex":
return !c.matchStringCondition(user, "matches regex")
}
return false
}
func (c *Condition) matchSegmentCondition(user FPUser, predicate string, segments map[string]Segment) bool {
if segments == nil {
return false
}
switch predicate {
case "is in":
return c.userInSegments(user, segments)
case "is not in":
return !c.userInSegments(user, segments)
}
return false
}
func (c *Condition) userDatetime(user FPUser) (int64, error) {
customValue := user.Get(c.Subject)
if len(customValue) == 0 {
return time.Now().Unix(), nil
}
return strconv.ParseInt(customValue, 10, 64)
}
func (c *Condition) matchDatetimeCondition(user FPUser, predicate string) bool {
cv, err := c.userDatetime(user)
if err != nil {
return false
}
switch predicate {
case "after":
return c.matchDatetimeObjects(func(o int64) bool { return cv >= o })
case "before":
return c.matchDatetimeObjects(func(o int64) bool { return cv < o })
}
return false
}
func (c *Condition) matchSemverCondition(user FPUser, predicate string) bool {
customValue := user.Get(c.Subject)
if len(customValue) == 0 {
return false
}
cv, err := semver.NewVersion(customValue)
if err != nil {
return false
}
switch predicate {
case "=":
return c.matchSemVerObjects(func(o *semver.Version) bool { return cv.Equal(o) })
case "!=":
return !c.matchSemverCondition(user, "=")
case ">":
return c.matchSemVerObjects(func(o *semver.Version) bool { return cv.GreaterThan(o) })
case ">=":
return c.matchSemVerObjects(func(o *semver.Version) bool { return cv.GreaterThan(o) || cv.Equal(o) })
case "<":
return c.matchSemVerObjects(func(o *semver.Version) bool { return cv.LessThan(o) })
case "<=":
return c.matchSemVerObjects(func(o *semver.Version) bool { return cv.LessThan(o) || cv.Equal(o) })
}
return false
}
func (c *Condition) matchNumberCondition(user FPUser, predicate string) bool {
customValue := user.Get(c.Subject)
if len(customValue) == 0 {
return false
}
cv, err := strconv.ParseFloat(customValue, 32)
if err != nil {
return false
}
switch predicate {
case "=":
return c.matchNumberObjects(func(o float64) bool { return cv == o })
case "!=":
return !c.matchNumberCondition(user, "=")
case ">":
return c.matchNumberObjects(func(o float64) bool { return cv > o })
case ">=":
return c.matchNumberObjects(func(o float64) bool { return cv >= o })
case "<":
return c.matchNumberObjects(func(o float64) bool { return cv < o })
case "<=":
return c.matchNumberObjects(func(o float64) bool { return cv <= o })
}
return false
}
func (c *Condition) userInSegments(user FPUser, segments map[string]Segment) bool {
for _, segmentKey := range c.Objects {
segment, ok := segments[segmentKey]
if ok {
if segment.contains(user) {
return true
}
}
}
return false
}
func (c *Condition) matchObjects(f func(string) bool) bool {
for _, o := range c.Objects {
if f(o) {
return true
}
}
return false
}
func (c *Condition) matchDatetimeObjects(f func(int64) bool) bool {
for _, o := range c.Objects {
co, err := strconv.ParseInt(o, 10, 64)
if err != nil {
return false
}
if f(co) {
return true
}
}
return false
}
func (c *Condition) matchNumberObjects(f func(float64) bool) bool {
for _, o := range c.Objects {
co, err := strconv.ParseFloat(o, 32)
if err != nil {
return false
}
if f(co) {
return true
}
}
return false
}
func (c *Condition) matchSemVerObjects(f func(*semver.Version) bool) bool {
for _, o := range c.Objects {
co, err := semver.NewVersion(o)
if err != nil {
return false
}
if f(co) {
return true
}
}
return false
}
func (s *Segment) contains(user FPUser) bool {
for _, rule := range s.Rules {
if rule.allow(user) {
return true
}
}
return false
}
func (r *Rule) allow(user FPUser) bool {
for _, condition := range r.Conditions {
if condition.meet(user, nil) {
return true
}
}
return false
}
func (repo *Repository) Clear() {
repo.toggles.Store(make(map[string]Toggle))
repo.segments.Store(make(map[string]Segment))
repo.debugUntilTime.Store(0)
}
func (repo *Repository) getToggles() (result map[string]Toggle) {
toggles := repo.toggles.Load()
if toggles == nil {
return make(map[string]Toggle)
}
result = toggles.(map[string]Toggle)
return
}
func (repo *Repository) getToggle(toggleKey string) (result Toggle, ok bool) {
toggles := repo.toggles.Load()
if toggles == nil {
return Toggle{}, false
}
togglesMap, ok := toggles.(map[string]Toggle)
if !ok {
return Toggle{}, false
}
result, ok = togglesMap[toggleKey]
return
}
func (repo *Repository) getSegments() (result map[string]Segment) {
segments := repo.segments.Load()
if segments == nil {
return make(map[string]Segment)
}
result = segments.(map[string]Segment)
return
}
func (repo *Repository) getSegment(segmentKey string) (result Segment, ok bool) {
segments := repo.toggles.Load()
if segments == nil {
return Segment{}, false
}
segmentsMap, ok := segments.(map[string]Segment)
if !ok {
return Segment{}, false
}
result, ok = segmentsMap[segmentKey]
return
}
func (repo *Repository) getDebugUntilTime() (result uint64) {
result = repo.debugUntilTime.Load()
return
}
func (repo *Repository) flush(data RepositoryData) {
repo.toggles.Store(data.Toggles)
repo.segments.Store(data.Segments)
repo.debugUntilTime.Store(data.DebugUntilTime)
}
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
Go
1
https://gitee.com/featureprobe/server-sdk-go.git
git@gitee.com:featureprobe/server-sdk-go.git
featureprobe
server-sdk-go
server-sdk-go
main

搜索帮助