Skip to content

Commit 6a15eaa

Browse files
authored
Merge pull request #3 from SalesLoft/randomize-percentage
added support for controlling randomize percentage
2 parents bbbd3ed + 257cfb4 commit 6a15eaa

8 files changed

+132
-70
lines changed

CHANGELOG.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,12 @@
22

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

5-
Every release, along with the migration instructions, is documented on the Github [Releases page](https://github.com/SalesLoft/open-source-template/releases).
5+
Every release, along with the migration instructions, is documented on the Github [Releases page](https://github.com/SalesLoft/gorollout/releases).
6+
7+
### v1.1.0
8+
9+
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.
10+
11+
### v1.0.0
12+
13+
Initial release of library. Feature complete and ready for production use.

CONTRIBUTING.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Introduction
2+
3+
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.
4+
5+
Keep an open mind! Improving documentation, bug triaging, or writing tutorials are all examples of helpful contributions that mean less work for you.
6+
7+
# Ground Rules
8+
9+
This includes not just how to communicate with others (being respectful, considerate, etc) but also technical responsibilities (importance of testing, project dependencies, etc).
10+
11+
Responsibilities
12+
13+
* Ensure cross-platform compatibility for every change that's accepted. Windows, Mac, Debian & Ubuntu Linux.
14+
* Code should be properly formatted using `gofmt`.
15+
* Create issues for any major changes and enhancements that you wish to make. Discuss things transparently and get community feedback.
16+
* Keep feature versions as small as possible, preferably one new feature per version.
17+
* Semantic versioning will be used.
18+
* Be welcoming to newcomers and encourage diverse new contributors from all backgrounds.
19+
20+
# How to report a bug
21+
22+
If you find a security vulnerability, do NOT open an issue. Email [[email protected]](mailto:[email protected]) instead.
23+
24+
When filing an issue, make sure to answer these five questions:
25+
26+
1. What version of Go are you using (go version)?
27+
1. What operating system and processor architecture are you using?
28+
1. What did you do?
29+
1. What did you expect to see?
30+
1. What did you see instead?

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ func main() {
4646
// check if a feature is active, globally
4747
manager.IsActive(apples)
4848

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

5252
// check multiple feature flags at once
5353
manager.IsActiveMulti(apples, bananas)

cmd/rollout/main.go

+5
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ func activatePercentageFeatureFlag(c *cli.Context) error {
186186
},
187187
),
188188
c.String("prefix"),
189+
false,
189190
)
190191

191192
return manager.ActivatePercentage(ff, uint8(percentage))
@@ -204,6 +205,7 @@ func activateFeatureFlag(c *cli.Context) error {
204205
},
205206
),
206207
c.String("prefix"),
208+
false,
207209
)
208210

209211
return manager.Activate(ff)
@@ -222,6 +224,7 @@ func deactivateFeatureFlag(c *cli.Context) error {
222224
},
223225
),
224226
c.String("prefix"),
227+
false,
225228
)
226229

227230
return manager.Deactivate(ff)
@@ -250,6 +253,7 @@ func activateTeamFeatureFlag(c *cli.Context) error {
250253
},
251254
),
252255
c.String("prefix"),
256+
false,
253257
)
254258

255259
return manager.ActivateTeam(teamID, ff)
@@ -278,6 +282,7 @@ func deactivateTeamFeatureFlag(c *cli.Context) error {
278282
},
279283
),
280284
c.String("prefix"),
285+
false,
281286
)
282287

283288
return manager.DeactivateTeam(teamID, ff)

feature.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,18 @@ func (f *Feature) deactivateTeam(teamID int64) {
7171
delete(f.teamIDs, teamID)
7272
}
7373

74-
func (f *Feature) isTeamActive(teamID int64) bool {
74+
func (f *Feature) isTeamActive(teamID int64, randomizePercentage bool) bool {
7575
if f.percentage == 100 {
76+
// feature is globally active
7677
return true
77-
} else if crc32.ChecksumIEEE([]byte(f.name+strconv.FormatInt(teamID, 10))) < randBase*uint32(f.percentage) {
78+
} else if randomizePercentage && crc32.ChecksumIEEE([]byte(f.name+strconv.FormatInt(teamID, 10))) < randBase*uint32(f.percentage) {
79+
// include the feature name in the checksum when randomizing percentage
80+
return true
81+
} else if !randomizePercentage && crc32.ChecksumIEEE([]byte(strconv.FormatInt(teamID, 10))) < randBase*uint32(f.percentage) {
82+
// only use the team id for the checksum when not randomizing the percentage
7883
return true
7984
} else if _, active := f.teamIDs[teamID]; active {
85+
// check if the team is explicitly active
8086
return true
8187
}
8288

feature_test.go

+49-38
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ func TestEncodeDecode(t *testing.T) {
3333
assert.EqualValues(t, in.Name(), out.Name())
3434
assert.EqualValues(t, in.percentage, out.percentage)
3535
assert.EqualValues(t, in.teamIDs, out.teamIDs)
36-
assert.True(t, out.isTeamActive(1))
37-
assert.True(t, out.isTeamActive(2))
38-
assert.True(t, out.isTeamActive(3))
36+
assert.True(t, out.isTeamActive(1, false))
37+
assert.True(t, out.isTeamActive(2, false))
38+
assert.True(t, out.isTeamActive(3, false))
3939

4040
// encode just percentage
4141
in = NewFeature("example")
@@ -69,39 +69,39 @@ func TestEncodeDecode(t *testing.T) {
6969
assert.EqualValues(t, in.percentage, out.percentage)
7070
assert.EqualValues(t, in.teamIDs, out.teamIDs)
7171

72-
assert.True(t, out.isTeamActive(1))
73-
assert.True(t, out.isTeamActive(2))
74-
assert.True(t, out.isTeamActive(3))
72+
assert.True(t, out.isTeamActive(1, false))
73+
assert.True(t, out.isTeamActive(2, false))
74+
assert.True(t, out.isTeamActive(3, false))
7575
}
7676

7777
func TestEnableDisableTeam(t *testing.T) {
7878
f := NewFeature("example")
79-
assert.False(t, f.isTeamActive(1))
79+
assert.False(t, f.isTeamActive(1, false))
8080

8181
f.activateTeam(1)
8282
f.activateTeam(2)
8383

84-
assert.True(t, f.isTeamActive(1))
85-
assert.True(t, f.isTeamActive(2))
86-
assert.False(t, f.isTeamActive(3))
84+
assert.True(t, f.isTeamActive(1, false))
85+
assert.True(t, f.isTeamActive(2, false))
86+
assert.False(t, f.isTeamActive(3, false))
8787

8888
f.deactivateTeam(1)
8989

90-
assert.False(t, f.isTeamActive(1))
91-
assert.True(t, f.isTeamActive(2))
90+
assert.False(t, f.isTeamActive(1, false))
91+
assert.True(t, f.isTeamActive(2, false))
9292
}
9393

9494
func TestEnableDisableFeature(t *testing.T) {
9595
f := NewFeature("example")
96-
assert.False(t, f.isTeamActive(1))
96+
assert.False(t, f.isTeamActive(1, false))
9797

9898
f.activate()
99-
assert.True(t, f.isTeamActive(1))
100-
assert.True(t, f.isTeamActive(999999999999))
99+
assert.True(t, f.isTeamActive(1, false))
100+
assert.True(t, f.isTeamActive(999999999999, false))
101101

102102
f.deactivate()
103-
assert.False(t, f.isTeamActive(1))
104-
assert.False(t, f.isTeamActive(99999999999))
103+
assert.False(t, f.isTeamActive(1, false))
104+
assert.False(t, f.isTeamActive(99999999999, false))
105105
}
106106

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

114-
assert.False(t, f.isTeamActive(1))
115-
assert.False(t, f.isTeamActive(2))
116-
assert.False(t, f.isTeamActive(3))
114+
assert.False(t, f.isTeamActive(1, true))
115+
assert.False(t, f.isTeamActive(2, true))
116+
assert.False(t, f.isTeamActive(3, true))
117117

118118
f.activatePercentage(25)
119119

120-
assert.False(t, f.isTeamActive(1))
121-
assert.False(t, f.isTeamActive(2))
122-
assert.True(t, f.isTeamActive(3))
120+
assert.False(t, f.isTeamActive(1, true))
121+
assert.False(t, f.isTeamActive(2, true))
122+
assert.True(t, f.isTeamActive(3, true))
123123

124124
f.activatePercentage(50)
125125

126-
assert.False(t, f.isTeamActive(1))
127-
assert.True(t, f.isTeamActive(2))
128-
assert.True(t, f.isTeamActive(3))
126+
assert.False(t, f.isTeamActive(1, true))
127+
assert.True(t, f.isTeamActive(2, true))
128+
assert.True(t, f.isTeamActive(3, true))
129129

130130
f.activatePercentage(75)
131131

132-
assert.False(t, f.isTeamActive(1))
133-
assert.True(t, f.isTeamActive(2))
134-
assert.True(t, f.isTeamActive(3))
132+
assert.False(t, f.isTeamActive(1, true))
133+
assert.True(t, f.isTeamActive(2, true))
134+
assert.True(t, f.isTeamActive(3, true))
135135

136136
f.activatePercentage(100)
137137

138-
assert.True(t, f.isTeamActive(1))
139-
assert.True(t, f.isTeamActive(2))
140-
assert.True(t, f.isTeamActive(3))
138+
assert.True(t, f.isTeamActive(1, true))
139+
assert.True(t, f.isTeamActive(2, true))
140+
assert.True(t, f.isTeamActive(3, true))
141141
}
142142

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

150-
assert.False(t, f.isTeamActive(1))
151-
assert.False(t, f.isTeamActive(2))
152-
assert.False(t, f.isTeamActive(3))
150+
assert.False(t, f.isTeamActive(1, true))
151+
assert.False(t, f.isTeamActive(2, true))
152+
assert.False(t, f.isTeamActive(3, true))
153153

154154
f.activateTeam(1)
155155
f.activatePercentage(25)
156156

157-
assert.True(t, f.isTeamActive(1))
158-
assert.False(t, f.isTeamActive(2))
159-
assert.True(t, f.isTeamActive(3))
157+
assert.True(t, f.isTeamActive(1, true))
158+
assert.False(t, f.isTeamActive(2, true))
159+
assert.True(t, f.isTeamActive(3, true))
160+
}
161+
162+
func TestRandomizePercentage(t *testing.T) {
163+
f := NewFeature("example")
164+
f.activatePercentage(50)
165+
166+
// teamID == 10 will be active at 50% when randomized, but not when static
167+
teamID := int64(10)
168+
169+
assert.True(t, f.isTeamActive(teamID, true))
170+
assert.False(t, f.isTeamActive(teamID, false))
160171
}

manager.go

+9-7
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,19 @@ import (
99

1010
// Manager persists and fetches feature toggles to/from redis
1111
type Manager struct {
12-
client redis.Cmdable
13-
keyPrefix string
12+
client redis.Cmdable
13+
keyPrefix string
14+
randomizePercentage bool
1415
}
1516

1617
// NewManager constructs a new Manager instance
17-
func NewManager(client redis.Cmdable, keyPrefix string) *Manager {
18+
func NewManager(client redis.Cmdable, keyPrefix string, randomizePercentage bool) *Manager {
1819
// nothing is retrieved from redis at this point
1920
// everything is fetched on demand
2021
return &Manager{
21-
client: client,
22-
keyPrefix: keyPrefix,
22+
client: client,
23+
keyPrefix: keyPrefix,
24+
randomizePercentage: randomizePercentage,
2325
}
2426
}
2527

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

245-
return feature.isTeamActive(teamID), nil
247+
return feature.isTeamActive(teamID, m.randomizePercentage), nil
246248
}
247249

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

284286
default:
285287
return nil, fmt.Errorf("unexpected type (%T) for msgpack value: %v", v, v)

0 commit comments

Comments
 (0)