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

fix: new variant hashing #145

Merged
merged 7 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
name: Checkout client specifications
with:
repository: Unleash/client-specification
ref: refs/tags/v4.5.2
ref: refs/tags/v5.0.2
path: testdata/client-specification
- uses: actions/setup-go@v2
name: Setup go
Expand Down
9 changes: 2 additions & 7 deletions api/feature.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package api

import (
"fmt"
"math/rand"
"strconv"
"time"

"github.com/Unleash/unleash-client-go/v3/context"
"github.com/twmb/murmur3"
"github.com/Unleash/unleash-client-go/v3/internal/strategies"
)

type ParameterMap map[string]interface{}
Expand Down Expand Up @@ -105,7 +104,7 @@ func (vc VariantCollection) getVariantFromWeights(ctx *context.Context) *Variant
}
stickiness := vc.Variants[0].Stickiness

target := getNormalizedNumber(getSeed(ctx, stickiness), vc.GroupId, totalWeight)
target := strategies.NormalizedVariantValue(getSeed(ctx, stickiness), vc.GroupId, totalWeight, strategies.VariantNormalizationSeed)
counter := uint32(0)
for _, variant := range vc.Variants {
counter += uint32(variant.Weight)
Expand Down Expand Up @@ -148,7 +147,3 @@ func getSeed(ctx *context.Context, stickiness string) string {
}
return strconv.Itoa(rand.Intn(10000))
}

func getNormalizedNumber(identifier, groupId string, normalizer int) uint32 {
return (murmur3.Sum32([]byte(fmt.Sprintf("%s:%s", groupId, identifier))) % uint32(normalizer)) + 1
}
6 changes: 3 additions & 3 deletions api/variant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ func (suite *VariantTestSuite) TestGetVariant_ShouldReturnVarD() {
GroupId: mockFeature.Name,
Variants: mockFeature.Variants,
}.GetVariant(mockContext)
suite.Equal("VarD", variantSetup.Name, "Should return VarD")
suite.Equal("VarE", variantSetup.Name, "Should return VarE")
suite.Equal(true, variantSetup.Enabled, "Should be equal")
}

Expand All @@ -238,7 +238,7 @@ func (suite *VariantTestSuite) TestGetVariant_ShouldReturnVarE() {
GroupId: mockFeature.Name,
Variants: mockFeature.Variants,
}.GetVariant(mockContext)
suite.Equal("VarE", variantSetup.Name, "Should return VarE")
suite.Equal("VarF", variantSetup.Name, "Should return VarF")
suite.Equal(true, variantSetup.Enabled, "Should be equal")
}

Expand All @@ -255,7 +255,7 @@ func (suite *VariantTestSuite) TestGetVariant_ShouldReturnVarF() {
GroupId: mockFeature.Name,
Variants: mockFeature.Variants,
}.GetVariant(mockContext)
suite.Equal("VarF", variantSetup.Name, "Should return VarF")
suite.Equal("VarE", variantSetup.Name, "Should return VarE")
suite.Equal(true, variantSetup.Enabled, "Should be equal")
}

Expand Down
2 changes: 1 addition & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
const (
deprecatedSuffix = "/features"
clientName = "unleash-client-go"
clientVersion = "3.9.0"
clientVersion = "4.0.0-beta.1"
)

var defaultStrategies = []strategy.Strategy{
Expand Down
2 changes: 1 addition & 1 deletion internal/strategies/flexible_rollout.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,6 @@ func (s flexibleRolloutStrategy) IsEnabled(params map[string]interface{}, ctx *c
return false
}

normalizedID := normalizedValue(stickinessID, groupID)
normalizedID := normalizedRolloutValue(stickinessID, groupID)
return percentage > 0 && float64(normalizedID) <= percentage
}
2 changes: 1 addition & 1 deletion internal/strategies/gradual_rollout_session_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func (s gradualRolloutSessionId) IsEnabled(params map[string]interface{}, ctx *c
return false
}

normalizedId := normalizedValue(ctx.SessionId, groupId)
normalizedId := normalizedRolloutValue(ctx.SessionId, groupId)

return percentage > 0.0 && float64(normalizedId) <= percentage
}
4 changes: 2 additions & 2 deletions internal/strategies/gradual_rollout_session_id_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestGradualRolloutSessionId_IsEnabled(t *testing.T) {
t.Run("p1=p2", func(t *testing.T) {
sessionId := "123123"
groupId := "group1"
percentage := normalizedValue(sessionId, groupId)
percentage := normalizedRolloutValue(sessionId, groupId)

params := map[string]interface{}{
strategy.ParamPercentage: percentage,
Expand All @@ -57,7 +57,7 @@ func TestGradualRolloutSessionId_IsEnabled(t *testing.T) {
t.Run("p1<p2", func(t *testing.T) {
sessionId := "123123"
groupId := "group1"
percentage := normalizedValue(sessionId, groupId) - 1
percentage := normalizedRolloutValue(sessionId, groupId) - 1

params := map[string]interface{}{
strategy.ParamPercentage: percentage,
Expand Down
2 changes: 1 addition & 1 deletion internal/strategies/gradual_rollout_user_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func (s gradualRolloutUserId) IsEnabled(params map[string]interface{}, ctx *cont
return false
}

normalizedId := normalizedValue(ctx.UserId, groupId)
normalizedId := normalizedRolloutValue(ctx.UserId, groupId)

return percentage > 0.0 && float64(normalizedId) <= percentage
}
4 changes: 2 additions & 2 deletions internal/strategies/gradual_rollout_user_id_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func TestGradualRolloutUserId_IsEnabled(t *testing.T) {
t.Run("p1=p2", func(t *testing.T) {
userId := "123123"
groupId := "group1"
percentage := normalizedValue(userId, groupId)
percentage := normalizedRolloutValue(userId, groupId)

params := map[string]interface{}{
strategy.ParamPercentage: percentage,
Expand All @@ -58,7 +58,7 @@ func TestGradualRolloutUserId_IsEnabled(t *testing.T) {
t.Run("p1<p2", func(t *testing.T) {
userId := "123123"
groupId := "group1"
percentage := normalizedValue(userId, groupId) - 1
percentage := normalizedRolloutValue(userId, groupId) - 1

params := map[string]interface{}{
strategy.ParamPercentage: percentage,
Expand Down
12 changes: 9 additions & 3 deletions internal/strategies/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"github.com/twmb/murmur3"
)

var VariantNormalizationSeed uint32 = 86028157

func round(f float64) int {
if f < -0.5 {
return int(f - 0.5)
Expand Down Expand Up @@ -50,11 +52,15 @@ func parameterAsFloat64(param interface{}) (result float64, ok bool) {
return
}

func normalizedValue(id string, groupId string) uint32 {
hash := murmur3.New32()
func normalizedRolloutValue(id string, groupId string) uint32 {
return NormalizedVariantValue(id, groupId, 100, 0)
}

func NormalizedVariantValue(id string, groupId string, normalizer int, seed uint32) uint32 {
hash := murmur3.SeedNew32(seed)
hash.Write([]byte(groupId + ":" + id))
hashCode := hash.Sum32()
return hashCode%uint32(100) + 1
return hashCode%uint32(normalizer) + 1
}

// coalesce returns the first non-empty string in the list of arguments
Expand Down
15 changes: 10 additions & 5 deletions internal/strategies/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,14 @@ func TestParameterAsFloat64(t *testing.T) {
}
}

func TestNormalizedValue(t *testing.T) {
assert.Equal(t, uint32(73), normalizedValue("123", "gr1"))
assert.Equal(t, uint32(25), normalizedValue("999", "groupX"))
func TestNormalizedRolloutValue(t *testing.T) {
assert.Equal(t, uint32(73), normalizedRolloutValue("123", "gr1"))
assert.Equal(t, uint32(25), normalizedRolloutValue("999", "groupX"))
}

func TestNormalizedVariantValue(t *testing.T) {
assert.Equal(t, uint32(96), NormalizedVariantValue("123", "gr1", 100, VariantNormalizationSeed))
assert.Equal(t, uint32(60), NormalizedVariantValue("999", "groupX", 100, VariantNormalizationSeed))
}

func TestCoalesce(t *testing.T) {
Expand Down Expand Up @@ -109,14 +114,14 @@ func BenchmarkNormalizedValue(b *testing.B) {
b.Run("value less than 32 bytes", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = normalizedValue(smallId, smallGroup)
_ = normalizedRolloutValue(smallId, smallGroup)
}
})

b.Run("value greater than 32 bytes", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = normalizedValue(largeId, largeGroup)
_ = normalizedRolloutValue(largeId, largeGroup)
}
})
}
Loading