gddo-server: redirect based on rollout

Logic is added to redirect paths based on a rollout percentage.

Change-Id: Ic79f1064ca6af1301d2e12d398dd8fd0e7ece73b
Reviewed-on: https://go-review.googlesource.com/c/gddo/+/285876
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Julie Qiu <julie@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/gddo-server/dynconfig/dynconfig.go b/gddo-server/dynconfig/dynconfig.go
index 5207164..4464a60 100644
--- a/gddo-server/dynconfig/dynconfig.go
+++ b/gddo-server/dynconfig/dynconfig.go
@@ -29,7 +29,9 @@
 	RedirectBadges   bool
 	RedirectHomepage bool
 	RedirectSearch   bool
-	RedirectPaths    []string
+
+	RedirectPaths   []string
+	RedirectRollout uint
 }
 
 // Read reads dynamic configuration from the given location.
diff --git a/gddo-server/pkgsite.go b/gddo-server/pkgsite.go
index 162d6e1..033e867 100644
--- a/gddo-server/pkgsite.go
+++ b/gddo-server/pkgsite.go
@@ -9,6 +9,7 @@
 import (
 	"context"
 	"fmt"
+	"hash/fnv"
 	"io/ioutil"
 	"log"
 	"net/http"
@@ -217,9 +218,21 @@
 	}
 }
 
+var vcsHostsWithThreeElementRepoName = map[string]bool{
+	"bitbucket.org": true,
+	"gitea.com":     true,
+	"gitee.com":     true,
+	"github.com":    true,
+	"gitlab.com":    true,
+	"golang.org":    true,
+}
+
 // shouldRedirectURL reports whether a request to the given URL should be
 // redirected to pkg.go.dev.
 func shouldRedirectURL(r *http.Request, poller *poller.Poller) bool {
+	if poller == nil {
+		return false
+	}
 	cfg := poller.Current().(*dynconfig.DynamicConfig)
 	return shouldRedirectURLForSnapshot(r, cfg)
 }
@@ -256,8 +269,20 @@
 		return false
 	}
 
-	// TODO: redirect based on rollout percentage.
-	return false
+	if cfg.RedirectRollout >= 100 {
+		return true
+	}
+	if cfg.RedirectRollout == 0 {
+		return false
+	}
+	parts := strings.Split(r.URL.Path, "/")
+	prefix := parts[1]
+	if _, ok := vcsHostsWithThreeElementRepoName[prefix]; ok {
+		prefix = strings.Join(parts[1:3], "/")
+	}
+	h := fnv.New32a()
+	fmt.Fprintf(h, "%s", prefix)
+	return uint(h.Sum32()%100) < cfg.RedirectRollout
 }
 
 const goGithubRepoURLPath = "/github.com/golang/go"
diff --git a/gddo-server/pkgsite_test.go b/gddo-server/pkgsite_test.go
index 34ad51b..8c0dddf 100644
--- a/gddo-server/pkgsite_test.go
+++ b/gddo-server/pkgsite_test.go
@@ -8,9 +8,11 @@
 
 import (
 	"bufio"
+	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
+	"strconv"
 	"strings"
 	"testing"
 
@@ -564,3 +566,54 @@
 		})
 	}
 }
+
+func TestShouldRedirectURLForSnapshot_RolloutPercentage(t *testing.T) {
+	checkRollout := func(t *testing.T, paths []string, rollout uint, want uint) {
+		t.Helper()
+		var inExperiment int
+		for _, p := range paths {
+			req, err := http.NewRequest("GET", "http://godoc.org/"+p, nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			snapshot := &dynconfig.DynamicConfig{RedirectRollout: rollout}
+			if shouldRedirectURLForSnapshot(req, snapshot) {
+				inExperiment++
+			}
+		}
+		if rollout == 0 {
+			if inExperiment != 0 {
+				t.Fatalf("rollout is 0 and inExperiment = %d; want = 0", inExperiment)
+			}
+			return
+		}
+		got := uint(100 * inExperiment / len(paths))
+		if got != want {
+			t.Errorf("rollout = %d; want = %d", got, want)
+		}
+	}
+
+	var paths []string
+	for host := range vcsHostsWithThreeElementRepoName {
+		for i := 0; i < 1000; i++ {
+			p := host + "/" + strconv.Itoa(i) + "/foo"
+			paths = append(paths, p)
+		}
+	}
+	pathsWithCustomHost := paths
+	for i := 0; i < 1000; i++ {
+		pathsWithCustomHost = append(pathsWithCustomHost, "mymodule.com/"+strconv.Itoa(i))
+	}
+	for _, rollout := range []uint{0, 33, 47, 50, 53, 75, 100} {
+		t.Run(fmt.Sprintf("%d", rollout), func(t *testing.T) {
+			checkRollout(t, paths, rollout, rollout)
+		})
+		t.Run(fmt.Sprintf("customhost %d", rollout), func(t *testing.T) {
+			// Map of rollout set to expected rollout percentage. Numbers are
+			// skewed because all my.module.com/<i> paths are added, and they
+			// will not be opted in.
+			want := map[uint]uint{0: 0, 33: 28, 47: 40, 50: 42, 53: 45, 75: 64, 100: 100}[rollout]
+			checkRollout(t, pathsWithCustomHost, rollout, want)
+		})
+	}
+}