-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Complex FF Definition #12
Conversation
flags.go
Outdated
} | ||
h.Write([]byte(buffer.String())) | ||
bs := h.Sum(nil) | ||
encodedStr := hex.EncodeToString(bs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can just use binary.LittleEndian.Uint16(buf)
to get a uint16 from raw binary bytes :)
Also, we should probably use 64 bits!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I figured there was a way instead of going thru this hex workaround! Thanks 👍 I also thought it was odd the Ruby impl limited it to 16 bits...I figured the more the better in terms of randomness/bits of entropy but tried to mirror the Ruby impl as much as possible.
examples_test.go
Outdated
@@ -14,15 +14,18 @@ func Example() { | |||
backend := BackendFromFile("flags.csv") | |||
Init(30*time.Second, backend) | |||
|
|||
if Enabled(ctx, "go.sun.mercury") { | |||
enabled, err := Enabled(ctx, "go.sun.moon", map[string]string{"host_name": "apibox_123"}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't agree with the log line below it 😨
flags.go
Outdated
Flags []Flag `json:"flags"` | ||
UpdatedTime float64 `json:"updated"` | ||
Flags []FlagJSONFormat `json:"flags"` | ||
UpdatedTime float64 `json:"updated"` | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should try to retain the ability to parse "version 1" of this.
flags.go
Outdated
type MatchListRule struct { | ||
Property string | ||
Values []string | ||
OnMatch string `json:"on_match"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems a bit weird for both rule types to have OnMatch
and OnMiss
. Maybe instead we can factor that out?
type RuleInfo struct {
OnMatch string
OnMiss string
Rule Rule
}
flags.go
Outdated
func getProperty(ctx context.Context, props map[string]string, prop string) string { | ||
if v, ok := props[prop]; ok { | ||
return v | ||
} else if v, ok := ctx.Value(overrideContextKey).(string); ok { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
overrideContextKey
lets us say "flag foo
is forced enabled". It's not useful for getting tags.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah sorry this was me being careless with copy+paste :|
flags.go
Outdated
} else if v, ok := ctx.Value(overrideContextKey).(string); ok { | ||
return v | ||
} else { | ||
return "" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should probably be an error of some sort if the tag doesn't exist in props.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup agreed, adjusting
flags.go
Outdated
@@ -188,7 +313,48 @@ func parseFlagsJSON(r io.Reader) (map[string]Flag, time.Time, error) { | |||
log.Print("[goforit] error parsing JSON file:\n", err) | |||
return nil, time.Time{}, err | |||
} | |||
return flagsToMap(v.Flags), time.Unix(int64(v.UpdatedTime), 0), nil | |||
// we first unmarshall into the intermediate FlagJSONFormat |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can do some cool things with the json.Unmarshaler
interface here. https://golang.org/pkg/encoding/json/#Unmarshaler . Then each type becomes responsible for unmarshaling itself, it makes the code easier to read usually.
flags.go
Outdated
flags[name] = Flag{Name: name, Rate: rate} | ||
var active = false | ||
var rules = []Rule{} | ||
if rate > 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could just unconditionally add the RateRule, right? If Rate: 0
, then it will be equivalent to off
flags.go
Outdated
case "off": | ||
enabled = false | ||
return | ||
case "match_list": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is "match_list" really a matchBehavior
???
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sigh another careless mistake...changed it to "continue". Thanks!
flags.go
Outdated
return | ||
} | ||
} | ||
enabled = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we get to the bottom, it should probably be disabled!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough, changed!
flags.go
Outdated
matchBehavior = r.onMiss() | ||
} | ||
switch matchBehavior { | ||
case "on": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use constants for "on", "off", "continue"? And their own type even!
Overall this looks quite good! |
@@ -23,11 +24,77 @@ type jsonFileBackend struct { | |||
filename string | |||
} | |||
|
|||
type flagJson struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is cute, it's like a merge of both old and new flags!
It might be better to use Version
to select old vs new? But I'm uncertain, happy to keep this. We should add a comment to explain what this is for, since it's not obvious.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i thought about versioning but if its ok with you, i think this just might be simpler for now! i added a comment explaining what was happening
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, that's fine
backend.go
Outdated
|
||
if len(raw.Rules) == 0 { | ||
if raw.Rate > 0 { | ||
raw.Active = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And if the Rate is 0, then we're still ok, because that's equivalent to inactive. Cooool. Again, comment this :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup! i added a comment here as well.
flags.go
Outdated
matchBehavior = r.OnMiss | ||
} | ||
switch matchBehavior { | ||
case "on": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use constants here please.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch, fixed!
flags.go
Outdated
enabled = false | ||
return | ||
} | ||
|
||
// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
???
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
woops...removed
flags.go
Outdated
case "continue": | ||
continue | ||
default: | ||
log.Printf("[goforit] unknown match behavior: " + string(matchBehavior)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Things like this should probably be some kind of metric too? But let's punt on that for now, maybe it's a good case for Sentry integration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, we should use the logger inside g *goforit
instead of just a basic log.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed to g.logger, but yeah would love to tackle Sentry before we ship!
flags.go
Outdated
var buffer bytes.Buffer | ||
buffer.WriteString(flag) | ||
for _, val := range r.Properties { | ||
prop, err := getProperty(props, val) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add a separator between props, so that ("foo", "barblah") is not equivalent to ("foobar","blah"). Maybe use a zero byte, since that's unlikely to be in an actual property value.
flags.go
Outdated
bs := h.Sum(nil) | ||
// get the most significant 32 digits | ||
x := binary.BigEndian.Uint32(bs) | ||
// check to see if the 16 most significant bits of the hex |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment is a lie, we're not using 16 bits
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch, fixed :)
flags.go
Outdated
for _, r := range flag.Rules { | ||
res, err := r.Rule.Handle(flag.Name, mergedProperties) | ||
if err != nil { | ||
log.Printf("[goforit] error evaluating rule:\n", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using Printf
without any percent-interpolations! That's making some tests fail
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, thats why the builds were failing. great, fixed them up!
flags.go
Outdated
@@ -201,6 +334,12 @@ func (g *goforit) SetStalenessThreshold(threshold time.Duration) { | |||
g.stalenessThreshold = threshold | |||
} | |||
|
|||
func (g *goforit) AddDefaultTags(tags map[string]string) { | |||
for k, v := range tags { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Protect with a mutex?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, was meaning to add this, just did! thanks!
flags_test.go
Outdated
@@ -182,6 +292,56 @@ func assertFlagsEqual(t *testing.T, expected, actual map[string]Flag) { | |||
} | |||
} | |||
|
|||
// func TestDefaultTags(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
???
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed, thanks
// test combination of global tag and local props | ||
g.AddDefaultTags(map[string]string{"cluster": "northwest-01"}) | ||
assert.True(t, g.Enabled(context.Background(), "test", map[string]string{"host_name": "apibox_001", "db": "mongo-prod"})) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also be able to check the log buffer for errors.
Getting super close! |
flags.go
Outdated
for k, v := range g.defaultTags { | ||
mergedProperties[k] = v | ||
} | ||
g.tagsMtx.RUnlock() // should i defer this? maybe inside its own scope? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could have a little getDefaultTags
method to copy-and-return them :)
} | ||
|
||
// if there are no rules, but flag is active, always return true | ||
if len(flag.Rules) == 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is confusing. Shouldn't "continue off the end of the rules" always have the same behavior?
Awesome tests! |
LGTM |
This is a WIP branch for complex feature flag definition. Formalizes a flag into three properties -- name, an active toggle (on/off), and an optional sequence of rules.
Rules can be of many types -- we have a match_list and rate rule type initially.
Rules must be able to be executed and produce a boolean value, and must specify behavior if evaluated to true or false (return true, return false, or continue to the next rule). Clients must now provide a map of properties which will be used by rules in their evaluation.