Skip to content
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

Merged
merged 11 commits into from
Mar 9, 2018
Merged

Complex FF Definition #12

merged 11 commits into from
Mar 9, 2018

Conversation

asoni-stripe
Copy link
Contributor

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.

flags.go Outdated
}
h.Write([]byte(buffer.String()))
bs := h.Sum(nil)
encodedStr := hex.EncodeToString(bs)
Copy link
Contributor

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!

Copy link
Contributor Author

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"})
Copy link
Contributor

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"`
}
Copy link
Contributor

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"`
Copy link
Contributor

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 {
Copy link
Contributor

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.

Copy link
Contributor Author

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 ""
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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 {
Copy link
Contributor

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":
Copy link
Contributor

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???

Copy link
Contributor Author

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
Copy link
Contributor

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!

Copy link
Contributor Author

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":
Copy link
Contributor

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!

@vasi-stripe
Copy link
Contributor

Overall this looks quite good!

@@ -23,11 +24,77 @@ type jsonFileBackend struct {
filename string
}

type flagJson struct {
Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor

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
Copy link
Contributor

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 :)

Copy link
Contributor Author

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":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use constants here please.

Copy link
Contributor Author

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
}

//
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

???

Copy link
Contributor Author

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))
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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)
Copy link
Contributor

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
Copy link
Contributor

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

Copy link
Contributor Author

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)
Copy link
Contributor

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

Copy link
Contributor Author

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Protect with a mutex?

Copy link
Contributor Author

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

???

Copy link
Contributor Author

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"}))

Copy link
Contributor

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.

@vasi-stripe
Copy link
Contributor

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?
Copy link
Contributor

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 {
Copy link
Contributor

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?

@vasi-stripe
Copy link
Contributor

Awesome tests!

@vasi-stripe vasi-stripe changed the title [WIP] Complex FF Definition Complex FF Definition Mar 9, 2018
@vasi-stripe
Copy link
Contributor

LGTM

@asoni-stripe asoni-stripe merged commit ce6c759 into master Mar 9, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants