diff options
Diffstat (limited to 'internal')
-rw-r--r-- | internal/rollout/rollout.go | 53 | ||||
-rw-r--r-- | internal/source/domains.go | 15 | ||||
-rw-r--r-- | internal/source/domains/gitlabsourceconfig/gitlabsourceconfig.go | 13 | ||||
-rw-r--r-- | internal/source/domains_test.go | 73 |
4 files changed, 152 insertions, 2 deletions
diff --git a/internal/rollout/rollout.go b/internal/rollout/rollout.go new file mode 100644 index 00000000..ec592c38 --- /dev/null +++ b/internal/rollout/rollout.go @@ -0,0 +1,53 @@ +package rollout + +import ( + "errors" + "hash/fnv" + "math/rand" +) + +// Rollout returns true and no error when during this run something should +// happen for given actor according to the stickiness and likelyhood passed +// as a percentage value to this function. It returns false rollout and an +// error if the percentage value is negative or higher than 100. +func Rollout(actor string, percentage int, stickiness string) (bool, error) { + if percentage < 0 || percentage > 100 { + return false, errors.New("Rollout value should be between 0 and 100 inclusive") + } + + if percentage == 0 { + return false, nil + } + + if percentage == 100 { + return true, nil + } + + switch stickiness { + case "random": + return random(percentage), nil + default: + return forActor(actor, percentage), nil + } +} + +// random guarantees no stickiness. For every call it will yield a random +// true/false based on the provided rollout percentage. +func random(percentage int) bool { + return rand.Intn(100) < percentage +} + +// forActor provides "stickines", i.e. guarantees that the same actor +// gets the same result every time. It also assures that an actor which is +// among the first 10% will also be among the first 20%. +func forActor(actor string, percentage int) bool { + h := fnv.New32a() + h.Write([]byte(actor)) + sum32 := h.Sum32() + + if sum32 == 0 { + return false + } + + return (sum32 % uint32(100)) < uint32(percentage) +} diff --git a/internal/source/domains.go b/internal/source/domains.go index 22187855..79357766 100644 --- a/internal/source/domains.go +++ b/internal/source/domains.go @@ -4,7 +4,10 @@ import ( "errors" "time" + log "github.com/sirupsen/logrus" + "gitlab.com/gitlab-org/gitlab-pages/internal/domain" + "gitlab.com/gitlab-org/gitlab-pages/internal/rollout" "gitlab.com/gitlab-org/gitlab-pages/internal/source/disk" "gitlab.com/gitlab-org/gitlab-pages/internal/source/domains/gitlabsourceconfig" "gitlab.com/gitlab-org/gitlab-pages/internal/source/gitlab" @@ -81,5 +84,17 @@ func (d *Domains) source(domain string) Source { } } + r := gitlabSourceConfig.Domains.Rollout + + enabled, err := rollout.Rollout(domain, r.Percentage, r.Stickiness) + if err != nil { + log.WithError(err).Error("Rollout error") + return d.disk + } + + if enabled { + return d.gitlab + } + return d.disk } diff --git a/internal/source/domains/gitlabsourceconfig/gitlabsourceconfig.go b/internal/source/domains/gitlabsourceconfig/gitlabsourceconfig.go index 4eda31aa..ebc8b485 100644 --- a/internal/source/domains/gitlabsourceconfig/gitlabsourceconfig.go +++ b/internal/source/domains/gitlabsourceconfig/gitlabsourceconfig.go @@ -14,6 +14,13 @@ import ( type GitlabSourceDomains struct { Enabled []string Broken string + Rollout GitlabSourceRollout +} + +// GitlabSourceRollout holds the rollout strategy and percentage +type GitlabSourceRollout struct { + Stickiness string + Percentage int } // GitlabSourceConfig holds the configuration for the gitlab source @@ -36,8 +43,10 @@ func (config *GitlabSourceConfig) UpdateFromYaml(content []byte) error { *config = updated log.WithFields(log.Fields{ - "Enabled domains": config.Domains.Enabled, - "Broken domain": config.Domains.Broken, + "Enabled domains": config.Domains.Enabled, + "Broken domain": config.Domains.Broken, + "Rollout %": config.Domains.Rollout.Percentage, + "Rollout stickiness": config.Domains.Rollout.Stickiness, }).Info("gitlab source config updated") return nil diff --git a/internal/source/domains_test.go b/internal/source/domains_test.go index bbdb8c21..d639b02d 100644 --- a/internal/source/domains_test.go +++ b/internal/source/domains_test.go @@ -1,6 +1,7 @@ package source import ( + "math/rand" "testing" "time" @@ -109,3 +110,75 @@ func TestGetDomain(t *testing.T) { require.NoError(t, err) }) } + +func TestGetDomainWithIncrementalrolloutOfGitLabSource(t *testing.T) { + // This will produce the following pseudo-random sequence: 5, 87, 68 + rand.Seed(42) + + // Generates FNV hash 4091421005, 4091421005 % 100 = 5 + domain05 := "test-domain-a.com" + // Generates FNV 2643293380, 2643293380 % 100 = 80 + domain80 := "test-domain-b.com" + + diskSource := disk.New() + + gitlabSourceConfig.Domains.Rollout.Percentage = 80 + + type testDomain struct { + name string + source string + times int + } + + tests := map[string]struct { + stickiness string + domains []testDomain + }{ + // domain05 should always use gitlab source, + // domain80 should use disk source + "default stickiness": { + stickiness: "", + domains: []testDomain{ + {name: domain05, source: "gitlab"}, + {name: domain80, source: "disk"}, + {name: domain05, source: "gitlab"}, + }, + }, + // Given that randSeed(42) will produce the following pseudo-random sequence: + // {5, 87, 68} the first and third call for domain05 should use gitlab source, + // while the second one should use disk source + "no stickiness": { + stickiness: "random", + domains: []testDomain{ + {name: domain05, source: "gitlab"}, + {name: domain05, source: "disk"}, + {name: domain05, source: "gitlab"}, + }}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + gitlabSource := NewMockSource() + for _, d := range tc.domains { + if d.source == "gitlab" { + gitlabSource.On("GetDomain", d.name). + Return(&domain.Domain{Name: d.name}, nil). + Once() + } + } + defer gitlabSource.AssertExpectations(t) + + domains := &Domains{ + disk: diskSource, + gitlab: gitlabSource, + } + + gitlabSourceConfig.Domains.Rollout.Stickiness = tc.stickiness + + for _, domain := range tc.domains { + _, err := domains.GetDomain(domain.name) + require.NoError(t, err) + } + }) + } +} |