Skip to content

Commit

Permalink
added support for controlling randomize percentage
Browse files Browse the repository at this point in the history
  • Loading branch information
Charlie Moad committed Nov 30, 2020
1 parent bbbd3ed commit 257cfb4
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 70 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,12 @@

This project adheres to [Semantic Versioning](http://semver.org/).

Every release, along with the migration instructions, is documented on the Github [Releases page](https://github.com/SalesLoft/open-source-template/releases).
Every release, along with the migration instructions, is documented on the Github [Releases page](https://github.com/SalesLoft/gorollout/releases).

### v1.1.0

Added support for controlling the randomizing percentage between feature flags. Setting to false will ensure that features are active for the same teams when rolled out the same percentage.

### v1.0.0

Initial release of library. Feature complete and ready for production use.
30 changes: 30 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Introduction

First off, thank you for considering contributing to gorollout. Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue, assessing changes, and helping you finalize your pull requests.

Keep an open mind! Improving documentation, bug triaging, or writing tutorials are all examples of helpful contributions that mean less work for you.

# Ground Rules

This includes not just how to communicate with others (being respectful, considerate, etc) but also technical responsibilities (importance of testing, project dependencies, etc).

Responsibilities

* Ensure cross-platform compatibility for every change that's accepted. Windows, Mac, Debian & Ubuntu Linux.
* Code should be properly formatted using `gofmt`.
* Create issues for any major changes and enhancements that you wish to make. Discuss things transparently and get community feedback.
* Keep feature versions as small as possible, preferably one new feature per version.
* Semantic versioning will be used.
* Be welcoming to newcomers and encourage diverse new contributors from all backgrounds.

# How to report a bug

If you find a security vulnerability, do NOT open an issue. Email [[email protected]](mailto:[email protected]) instead.

When filing an issue, make sure to answer these five questions:

1. What version of Go are you using (go version)?
1. What operating system and processor architecture are you using?
1. What did you do?
1. What did you expect to see?
1. What did you see instead?
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ func main() {
// check if a feature is active, globally
manager.IsActive(apples)

// check if a feature is active for a specific team
manager.IsTeamActive(99, apples)
// check if a feature is active for a specific team (randomize percentage disabled)
manager.IsTeamActive(99, apples, false)

// check multiple feature flags at once
manager.IsActiveMulti(apples, bananas)
Expand Down
5 changes: 5 additions & 0 deletions cmd/rollout/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ func activatePercentageFeatureFlag(c *cli.Context) error {
},
),
c.String("prefix"),
false,
)

return manager.ActivatePercentage(ff, uint8(percentage))
Expand All @@ -204,6 +205,7 @@ func activateFeatureFlag(c *cli.Context) error {
},
),
c.String("prefix"),
false,
)

return manager.Activate(ff)
Expand All @@ -222,6 +224,7 @@ func deactivateFeatureFlag(c *cli.Context) error {
},
),
c.String("prefix"),
false,
)

return manager.Deactivate(ff)
Expand Down Expand Up @@ -250,6 +253,7 @@ func activateTeamFeatureFlag(c *cli.Context) error {
},
),
c.String("prefix"),
false,
)

return manager.ActivateTeam(teamID, ff)
Expand Down Expand Up @@ -278,6 +282,7 @@ func deactivateTeamFeatureFlag(c *cli.Context) error {
},
),
c.String("prefix"),
false,
)

return manager.DeactivateTeam(teamID, ff)
Expand Down
10 changes: 8 additions & 2 deletions feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,18 @@ func (f *Feature) deactivateTeam(teamID int64) {
delete(f.teamIDs, teamID)
}

func (f *Feature) isTeamActive(teamID int64) bool {
func (f *Feature) isTeamActive(teamID int64, randomizePercentage bool) bool {
if f.percentage == 100 {
// feature is globally active
return true
} else if crc32.ChecksumIEEE([]byte(f.name+strconv.FormatInt(teamID, 10))) < randBase*uint32(f.percentage) {
} else if randomizePercentage && crc32.ChecksumIEEE([]byte(f.name+strconv.FormatInt(teamID, 10))) < randBase*uint32(f.percentage) {
// include the feature name in the checksum when randomizing percentage
return true
} else if !randomizePercentage && crc32.ChecksumIEEE([]byte(strconv.FormatInt(teamID, 10))) < randBase*uint32(f.percentage) {
// only use the team id for the checksum when not randomizing the percentage
return true
} else if _, active := f.teamIDs[teamID]; active {
// check if the team is explicitly active
return true
}

Expand Down
87 changes: 49 additions & 38 deletions feature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ func TestEncodeDecode(t *testing.T) {
assert.EqualValues(t, in.Name(), out.Name())
assert.EqualValues(t, in.percentage, out.percentage)
assert.EqualValues(t, in.teamIDs, out.teamIDs)
assert.True(t, out.isTeamActive(1))
assert.True(t, out.isTeamActive(2))
assert.True(t, out.isTeamActive(3))
assert.True(t, out.isTeamActive(1, false))
assert.True(t, out.isTeamActive(2, false))
assert.True(t, out.isTeamActive(3, false))

// encode just percentage
in = NewFeature("example")
Expand Down Expand Up @@ -69,39 +69,39 @@ func TestEncodeDecode(t *testing.T) {
assert.EqualValues(t, in.percentage, out.percentage)
assert.EqualValues(t, in.teamIDs, out.teamIDs)

assert.True(t, out.isTeamActive(1))
assert.True(t, out.isTeamActive(2))
assert.True(t, out.isTeamActive(3))
assert.True(t, out.isTeamActive(1, false))
assert.True(t, out.isTeamActive(2, false))
assert.True(t, out.isTeamActive(3, false))
}

func TestEnableDisableTeam(t *testing.T) {
f := NewFeature("example")
assert.False(t, f.isTeamActive(1))
assert.False(t, f.isTeamActive(1, false))

f.activateTeam(1)
f.activateTeam(2)

assert.True(t, f.isTeamActive(1))
assert.True(t, f.isTeamActive(2))
assert.False(t, f.isTeamActive(3))
assert.True(t, f.isTeamActive(1, false))
assert.True(t, f.isTeamActive(2, false))
assert.False(t, f.isTeamActive(3, false))

f.deactivateTeam(1)

assert.False(t, f.isTeamActive(1))
assert.True(t, f.isTeamActive(2))
assert.False(t, f.isTeamActive(1, false))
assert.True(t, f.isTeamActive(2, false))
}

func TestEnableDisableFeature(t *testing.T) {
f := NewFeature("example")
assert.False(t, f.isTeamActive(1))
assert.False(t, f.isTeamActive(1, false))

f.activate()
assert.True(t, f.isTeamActive(1))
assert.True(t, f.isTeamActive(999999999999))
assert.True(t, f.isTeamActive(1, false))
assert.True(t, f.isTeamActive(999999999999, false))

f.deactivate()
assert.False(t, f.isTeamActive(1))
assert.False(t, f.isTeamActive(99999999999))
assert.False(t, f.isTeamActive(1, false))
assert.False(t, f.isTeamActive(99999999999, false))
}

func TestRollout(t *testing.T) {
Expand All @@ -111,33 +111,33 @@ func TestRollout(t *testing.T) {
// 25% < Team 2 < 50%
// 0 % < Team 3 < 25%

assert.False(t, f.isTeamActive(1))
assert.False(t, f.isTeamActive(2))
assert.False(t, f.isTeamActive(3))
assert.False(t, f.isTeamActive(1, true))
assert.False(t, f.isTeamActive(2, true))
assert.False(t, f.isTeamActive(3, true))

f.activatePercentage(25)

assert.False(t, f.isTeamActive(1))
assert.False(t, f.isTeamActive(2))
assert.True(t, f.isTeamActive(3))
assert.False(t, f.isTeamActive(1, true))
assert.False(t, f.isTeamActive(2, true))
assert.True(t, f.isTeamActive(3, true))

f.activatePercentage(50)

assert.False(t, f.isTeamActive(1))
assert.True(t, f.isTeamActive(2))
assert.True(t, f.isTeamActive(3))
assert.False(t, f.isTeamActive(1, true))
assert.True(t, f.isTeamActive(2, true))
assert.True(t, f.isTeamActive(3, true))

f.activatePercentage(75)

assert.False(t, f.isTeamActive(1))
assert.True(t, f.isTeamActive(2))
assert.True(t, f.isTeamActive(3))
assert.False(t, f.isTeamActive(1, true))
assert.True(t, f.isTeamActive(2, true))
assert.True(t, f.isTeamActive(3, true))

f.activatePercentage(100)

assert.True(t, f.isTeamActive(1))
assert.True(t, f.isTeamActive(2))
assert.True(t, f.isTeamActive(3))
assert.True(t, f.isTeamActive(1, true))
assert.True(t, f.isTeamActive(2, true))
assert.True(t, f.isTeamActive(3, true))
}

func TestRolloutactivateTeamMix(t *testing.T) {
Expand All @@ -147,14 +147,25 @@ func TestRolloutactivateTeamMix(t *testing.T) {
// 25% < Team 2 < 50%
// 0 % < Team 3 < 25%

assert.False(t, f.isTeamActive(1))
assert.False(t, f.isTeamActive(2))
assert.False(t, f.isTeamActive(3))
assert.False(t, f.isTeamActive(1, true))
assert.False(t, f.isTeamActive(2, true))
assert.False(t, f.isTeamActive(3, true))

f.activateTeam(1)
f.activatePercentage(25)

assert.True(t, f.isTeamActive(1))
assert.False(t, f.isTeamActive(2))
assert.True(t, f.isTeamActive(3))
assert.True(t, f.isTeamActive(1, true))
assert.False(t, f.isTeamActive(2, true))
assert.True(t, f.isTeamActive(3, true))
}

func TestRandomizePercentage(t *testing.T) {
f := NewFeature("example")
f.activatePercentage(50)

// teamID == 10 will be active at 50% when randomized, but not when static
teamID := int64(10)

assert.True(t, f.isTeamActive(teamID, true))
assert.False(t, f.isTeamActive(teamID, false))
}
16 changes: 9 additions & 7 deletions manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@ import (

// Manager persists and fetches feature toggles to/from redis
type Manager struct {
client redis.Cmdable
keyPrefix string
client redis.Cmdable
keyPrefix string
randomizePercentage bool
}

// NewManager constructs a new Manager instance
func NewManager(client redis.Cmdable, keyPrefix string) *Manager {
func NewManager(client redis.Cmdable, keyPrefix string, randomizePercentage bool) *Manager {
// nothing is retrieved from redis at this point
// everything is fetched on demand
return &Manager{
client: client,
keyPrefix: keyPrefix,
client: client,
keyPrefix: keyPrefix,
randomizePercentage: randomizePercentage,
}
}

Expand Down Expand Up @@ -242,7 +244,7 @@ func (m *Manager) IsTeamActive(teamID int64, feature *Feature) (bool, error) {
return false, err
}

return feature.isTeamActive(teamID), nil
return feature.isTeamActive(teamID, m.randomizePercentage), nil
}

// IsTeamActiveMulti returns whether the given features are globally active
Expand Down Expand Up @@ -279,7 +281,7 @@ func (m *Manager) IsTeamActiveMulti(teamID int64, features ...*Feature) ([]bool,
if err := msgpack.Unmarshal([]byte(t), features[i]); err != nil {
return nil, err
}
results[i] = features[i].isTeamActive(teamID)
results[i] = features[i].isTeamActive(teamID, m.randomizePercentage)

default:
return nil, fmt.Errorf("unexpected type (%T) for msgpack value: %v", v, v)
Expand Down
Loading

0 comments on commit 257cfb4

Please sign in to comment.