Skip to content

Instantly share code, notes, and snippets.

@juandspy
Last active July 10, 2024 10:41
Show Gist options
  • Save juandspy/c1456f69f6d529e1425f27dde56301fe to your computer and use it in GitHub Desktop.
Save juandspy/c1456f69f6d529e1425f27dde56301fe to your computer and use it in GitHub Desktop.
Check Unleash gradual rollout stickiness

Benchmark Unleash for canary rollouts

This code is intended to check if canary rollouts are:

  1. sticky
  2. proportional to what we set in the UI

In this case I created a feature flag called insights-operator-gathering-conditions-service in insights.unleash.devshift.com with a stickiness in the userId field and a gradual rollout of 50% of the population.

I created a benchmark where I check the answers of this dummy function defined in main.go

func getInt(clusterID string) int {
	if unleash.IsEnabled(
		unleashFeatureToggle,
		unleash.WithContext(context.Context{
			UserId: clusterID,
		})) {
		return 1
	}
	return 0
}

that returns 1 for all clusterID (userId in terms of Unleash) that are categorized by Unleash as canary candidates. The benchmark is configured using some constant values

const (
	nClusters           = 500
	nRequestsPerCluster = 10
	wantPercentage      = 50
)

and prints a summary with the results:

❯ go test -v -bench=. -count 1
goos: darwin
goarch: arm64
pkg: github.com/juandspy/learning-feature-flags
BenchmarkGetInt
Total percentage of clusters using the canary version: 50.40%, want 50%
Total percentage of clusters using the canary version: 49.40%, want 50%
Total percentage of clusters using the canary version: 49.20%, want 50%
Total percentage of clusters using the canary version: 50.40%, want 50%
Total percentage of clusters using the canary version: 52.40%, want 50%
Total percentage of clusters using the canary version: 50.60%, want 50%
BenchmarkGetInt-8       1000000000               0.005782 ns/op
PASS
ok      github.com/juandspy/learning-feature-flags      0.922s

We can see that the percentage of clusters identified for the canary is around 50% and that the benchmark passes without any failure. This is important as there is a check for stickiness:

// Ensure the stickiness:
// Assert that all items in results[u] are the same
first := results[u][0]
for j := 1; j < nRequestsPerCluster; j++ {
    if results[u][j] != first {
        b.Errorf("Values are not the same for UUID %s: %v", u, results[u])
    }
}
package main
import (
"fmt"
"testing"
"github.com/google/uuid"
)
const (
nClusters = 500
nRequestsPerCluster = 10
wantPercentage = 50
)
// BenchmarkGetInt make sure that the getInt function returns a "1" around
// "wantPercentage" times
func BenchmarkGetInt(b *testing.B) {
results := make(map[string][]int)
// total counts the amount of clusters receiving the canary version
total := 0
for i := 0; i < nClusters; i++ {
u := uuid.New().String()
results[u] = make([]int, nRequestsPerCluster)
for j := 0; j < nRequestsPerCluster; j++ {
results[u][j] = getInt(u)
}
// Ensure the stickiness:
// Assert that all items in results[u] are the same
first := results[u][0]
for j := 1; j < nRequestsPerCluster; j++ {
if results[u][j] != first {
b.Errorf("Values are not the same for UUID %s: %v", u, results[u])
}
}
total += first
}
// Count the percentage of clusters using the canary version
gotPercentage := float32(total) / nClusters * 100
fmt.Printf(
"Total percentage of clusters using the canary version: %.2f%%, want %d%%\n",
gotPercentage, wantPercentage)
}
module github.com/juandspy/learning-feature-flags
go 1.22.2
require github.com/Unleash/unleash-client-go/v4 v4.1.1
require (
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.1.1 // indirect
github.com/stretchr/testify v1.2.2 // indirect
github.com/twmb/murmur3 v1.1.8 // indirect
)
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Unleash/unleash-client-go/v4 v4.1.1 h1:2qa/hxj7AApuA/E3MvxawiTbghu3smJV4C9ia7QP538=
github.com/Unleash/unleash-client-go/v4 v4.1.1/go.mod h1:gUbLOk661ZfMpFDDHkW6rduDW/7Ru8uZHfXJDjQUs2s=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
gopkg.in/h2non/gock.v1 v1.0.10/go.mod h1:KHI4Z1sxDW6P4N3DfTWSEza07YpkQP7KJBfglRMEjKY=
package main
import (
"log"
"net/http"
"os"
"github.com/Unleash/unleash-client-go/v4"
"github.com/Unleash/unleash-client-go/v4/context"
"github.com/joho/godotenv"
)
const (
unleashURL = "https://insights.unleash.devshift.net/api/"
unleashApp = "local-benchmarks"
unleashFeatureToggle = "insights-operator-gathering-conditions-service"
)
func init() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
unleashToken := os.Getenv("UNLEASH_TOKEN")
unleash.Initialize(
// unleash.WithListener(&unleash.DebugListener{}),
unleash.WithAppName(unleashApp),
unleash.WithUrl(unleashURL),
unleash.WithCustomHeaders(http.Header{"Authorization": {unleashToken}}),
)
// Note this will block until the default client is ready
unleash.WaitForReady()
}
func getInt(clusterID string) int {
if unleash.IsEnabled(
unleashFeatureToggle,
unleash.WithContext(context.Context{
UserId: clusterID,
})) {
return 1
}
return 0
}
func main() {
// unleash.Close()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment