cmd/teeproxy,internal/teeproxy,internal/breaker: delete
Requests are now teed to pkg.go.dev directly from godoc.org, so this
service is no longer needed.
Change-Id: I8190488244cec1f4faca63652b937a0703ccfd3a
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/279132
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Julie Qiu <julie@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/cmd/teeproxy/main.go b/cmd/teeproxy/main.go
deleted file mode 100644
index 7f38cab..0000000
--- a/cmd/teeproxy/main.go
+++ /dev/null
@@ -1,108 +0,0 @@
-// Copyright 2020 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// The teeproxy hits the frontend with a URL from godoc.org.
-package main
-
-import (
- "context"
- "flag"
- "fmt"
- "io/ioutil"
- "net/http"
- "os"
-
- "golang.org/x/pkgsite/internal/auth"
- "golang.org/x/pkgsite/internal/breaker"
- "golang.org/x/pkgsite/internal/config"
- "golang.org/x/pkgsite/internal/dcensus"
- "golang.org/x/pkgsite/internal/log"
- "golang.org/x/pkgsite/internal/secrets"
- "golang.org/x/pkgsite/internal/teeproxy"
-)
-
-func main() {
- var credsFile = flag.String("creds", "", "filename for credentials, when running locally")
- flag.Usage = func() {
- fmt.Fprintf(flag.CommandLine.Output(), "usage: %s [flags]\n", os.Args[0])
- flag.PrintDefaults()
- }
- flag.Parse()
- ctx := context.Background()
-
- cfg, err := config.Init(ctx)
- if err != nil {
- log.Fatal(ctx, err)
- }
- cfg.Dump(os.Stderr)
-
- log.SetLevel(cfg.LogLevel)
- if cfg.OnGCP() {
- _, err := log.UseStackdriver(ctx, cfg, "teeproxy-log")
- if err != nil {
- log.Fatal(ctx, err)
- }
- }
- client := &http.Client{}
- var jsonCreds []byte
- if *credsFile != "" {
- jsonCreds, err = ioutil.ReadFile(*credsFile)
- if err != nil {
- log.Fatal(ctx, err)
- }
- } else {
- const secretName = "load-test-agent-creds"
- log.Infof(ctx, "getting secret %q", secretName)
- s, err := secrets.Get(context.Background(), secretName)
- if err != nil {
- log.Infof(ctx, "secret %q not found", secretName)
- } else {
- jsonCreds = []byte(s)
- }
- }
-
- if jsonCreds != nil {
- client, err = auth.NewClient(ctx, jsonCreds, false)
- if err != nil {
- log.Fatal(ctx, err)
- }
- }
-
- views := append(dcensus.ServerViews,
- teeproxy.TeeproxyGddoRequestCount,
- teeproxy.TeeproxyPkgGoDevRequestCount,
- teeproxy.TeeproxyGddoRequestLatencyDistribution,
- teeproxy.TeeproxyPkgGoDevRequestLatencyDistribution,
- teeproxy.TeeproxyPkgGoDevBrokenPathCount,
- )
- dcensus.Init(cfg, views...)
-
- http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
- http.ServeFile(w, r, "content/static/img/favicon.ico")
- })
- server, err := teeproxy.NewServer(teeproxy.Config{
- AuthKey: cfg.Teeproxy.AuthKey,
- AuthValue: cfg.Teeproxy.AuthValue,
- Rate: cfg.Teeproxy.Rate,
- Burst: cfg.Teeproxy.Burst,
- BreakerConfig: breaker.Config{
- FailsToRed: cfg.Teeproxy.FailsToRed,
- FailureThreshold: cfg.Teeproxy.FailureThreshold,
- GreenInterval: cfg.Teeproxy.GreenInterval,
- MinTimeout: cfg.Teeproxy.MinTimeout,
- MaxTimeout: cfg.Teeproxy.MaxTimeout,
- SuccsToGreen: cfg.Teeproxy.SuccsToGreen,
- },
- Hosts: cfg.Teeproxy.Hosts,
- Client: client,
- })
- if err != nil {
- log.Fatal(ctx, err)
- }
- http.Handle("/", server)
-
- addr := cfg.HostAddr("localhost:8020")
- log.Infof(ctx, "Listening on addr %s", addr)
- log.Fatal(ctx, http.ListenAndServe(addr, nil))
-}
diff --git a/internal/breaker/breaker.go b/internal/breaker/breaker.go
deleted file mode 100644
index 98e40a7..0000000
--- a/internal/breaker/breaker.go
+++ /dev/null
@@ -1,271 +0,0 @@
-// Copyright 2020 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// Package breaker implements the circuit breaker pattern.
-// see https://docs.microsoft.com/en-us/previous-versions/msp-n-p/dn589784(v=pandp.10).
-//
-// This package uses different terminologies for the state of the circuit breaker
-// for better readability. Since it is unintuitive for users unfamiliar with
-// circuits that a "closed" state means to "let the requests pass through" and
-// "open" state means "don't let anything through", we use the colors red,
-// yellow, and green instead of open, half-open, and closed, respectively.
-//
-// When the API is stable, this package may be factored out to an external location.
-package breaker
-
-import (
- "fmt"
- "sync"
- "time"
-)
-
-// For testing.
-var timeNow = time.Now
-
-// numBuckets is the number of buckets in the breaker's sliding window.
-const numBuckets = 8
-
-// State represents the current state the breaker is in.
-type State int
-
-// The breaker can have three states: Red, Yellow, or Green.
-const (
- // Red state means that requests should not be allowed.
- Red State = iota
- // Yellow state means that certain requests may proceed with caution.
- Yellow
- // Green state means that requests are allowed to pass.
- Green
-)
-
-// String returns the string version of the state.
-func (s State) String() string {
- switch s {
- case Red:
- return "red state"
- case Yellow:
- return "yellow state"
- case Green:
- return "green state"
- default:
- return "invalid state"
- }
-}
-
-// Config holds the configuration values for a breaker.
-type Config struct {
- // FailsToRed is the number of failures to exceed before the breaker shifts
- // from green to red state.
- FailsToRed int
- // FailureThreshold is the failure ratio to exceed before the breaker
- // shifts from green to red state.
- FailureThreshold float64
- // GreenInterval is the length of the interval with which the breaker
- // checks for conditions to move from green to red state.
- GreenInterval time.Duration
- // MinTimeout is the minimum timeout period that the breaker stays in the
- // red state before moving to the yellow state.
- MinTimeout time.Duration
- // MaxTimeout is the maxmimum timeout period that the breaker stays in the
- // red state before moving to the yellow state.
- MaxTimeout time.Duration
- // SuccsToGreen is the number of successes required to shift from the
- // yellow state to the green state.
- SuccsToGreen int
-}
-
-// Breaker represents a circuit breaker.
-//
-// In the green state, the breaker remains green until it encounters a time
-// window of length GreenInterval where there are more than FailsToRed failures
-// and a failureRatio of more than FailureThreshold, in which case the
-// state becomes red.
-//
-// In the red state, the breaker halts all requests and waits for a timeout period
-// before shifting to the yellow state.
-//
-// In the yellow state, the breaker allows the first SuccsToGreen requests. If
-// any of these fail, the state reverts to red. Otherwise, the state becomes
-// green again.
-//
-// The timeout period is initially set to MinTimeout when the breaker shifts
-// from green to yellow. By default, the timeout period is doubled each time
-// the breaker fails to shift from the yellow state to the green state and is
-// capped at MaxTimeout.
-type Breaker struct {
- config Config
-
- // buckets represents a time sliding window, implemented as a ring buffer.
- buckets [numBuckets]bucket
- // granularity is the length of time each bucket is responsible for.
- granularity time.Duration
-
- mu sync.Mutex
- state State
- cur int
- consecutiveSuccs int
- timeout time.Duration
- lastEvent time.Time
-}
-
-// New creates a Breaker with the given configuration.
-func New(config Config) (*Breaker, error) {
- switch {
- case config.FailsToRed <= 0:
- return nil, fmt.Errorf("illegal value for FailsToRed")
- case config.FailureThreshold <= 0, config.FailureThreshold > 1:
- return nil, fmt.Errorf("illegal value for FailureThreshold")
- case config.GreenInterval <= 0:
- return nil, fmt.Errorf("illegal value for GreenInterval")
- case config.MinTimeout <= 0:
- return nil, fmt.Errorf("illegal value for MinTimeout")
- case config.MaxTimeout <= 0:
- return nil, fmt.Errorf("illegal value for MaxTimeout")
- case config.SuccsToGreen <= 0:
- return nil, fmt.Errorf("illegal value for SuccsToGreen")
- default:
- return &Breaker{
- config: config,
- state: Green,
- granularity: config.GreenInterval / numBuckets,
- timeout: config.MinTimeout,
- lastEvent: timeNow(),
- }, nil
- }
-}
-
-// State returns the state of the breaker.
-func (b *Breaker) State() State {
- b.mu.Lock()
- defer b.mu.Unlock()
- return b.checkState()
-}
-
-// checkState returns the state of the breaker without obtaining a mutex lock.
-// The state is updated if sufficient time has passed since the last event.
-func (b *Breaker) checkState() State {
- now := timeNow()
- if b.state == Red && now.After(b.lastEvent.Add(b.timeout)) {
- b.state = Yellow
- }
- return b.state
-}
-
-// Allow reports whether an event may happen at time now. If Allow returns
-// true, the user must then call Record to register whether the event succeeded.
-func (b *Breaker) Allow() bool {
- return b.State() != Red
-}
-
-// Record registers the success or failure of an event with the circuit breaker.
-// Use this function after a call to Allow returned true.
-func (b *Breaker) Record(success bool) {
- b.mu.Lock()
- defer b.mu.Unlock()
- b.update(timeNow())
- if success {
- b.succeeded()
- } else {
- b.failed()
- }
-}
-
-// succeeded signals that an allowed request has succeeded. The breaker state is
-// changed if necessary.
-func (b *Breaker) succeeded() {
- b.buckets[b.cur].successes++
- b.consecutiveSuccs++
- if b.checkState() == Yellow && b.consecutiveSuccs >= b.config.SuccsToGreen {
- b.state = Green
- b.timeout = b.config.MinTimeout
- b.resetCounts()
- }
-}
-
-// failed signals that an allowed request has failed. The breaker state is
-// changed if necessary.
-func (b *Breaker) failed() {
- b.buckets[b.cur].failures++
- b.consecutiveSuccs = 0
- switch b.checkState() {
- case Yellow:
- b.increaseTimeout()
- b.state = Red
- case Green:
- // Check conditions to move to red state.
- successes, failures := b.counts()
- totalRequests := successes + failures
- if failures <= b.config.FailsToRed || totalRequests == 0 {
- return
- }
- failureRatio := float64(failures) / float64(totalRequests)
- if failureRatio > b.config.FailureThreshold {
- b.state = Red
- }
- }
-}
-
-// update updates the values of breaker due to the passage of time.
-func (b *Breaker) update(now time.Time) {
- // Ignore updates from the past.
- if now.Before(b.lastEvent) {
- return
- }
- since := now.Sub(b.lastEvent)
- b.advance(int(since / b.granularity))
- b.lastEvent = now
-}
-
-// advance advances the breaker's sliding window by n buckets. The counts of
-// successes and failures are also updated to include only the buckets in the
-// current window.
-func (b *Breaker) advance(n int) {
- if n >= len(b.buckets) {
- b.resetCounts()
- b.cur = 0
- return
- }
- for i := 0; i < n; i++ {
- b.cur = (b.cur + 1) % len(b.buckets)
- b.buckets[b.cur].reset()
- }
-}
-
-// counts returns the total number of successes and failures in the breaker's
-// sliding window.
-func (b *Breaker) counts() (successes, failures int) {
- for _, bu := range b.buckets {
- successes += bu.successes
- failures += bu.failures
- }
- return successes, failures
-}
-
-// resetCounts resets all the buckets in breaker.
-func (b *Breaker) resetCounts() {
- for i := range b.buckets {
- b.buckets[i].reset()
- }
-}
-
-// increaseTimeout exponentially increases the breaker's timeout period,
-// capped at maxTimeout.
-func (b *Breaker) increaseTimeout() {
- if 2*b.timeout <= b.config.MaxTimeout {
- b.timeout *= 2
- return
- }
- b.timeout = b.config.MaxTimeout
-}
-
-type bucket struct {
- successes int
- failures int
-}
-
-// reset resets the values in the bucket.
-func (bu *bucket) reset() {
- bu.successes = 0
- bu.failures = 0
-}
diff --git a/internal/breaker/breaker_test.go b/internal/breaker/breaker_test.go
deleted file mode 100644
index b773980..0000000
--- a/internal/breaker/breaker_test.go
+++ /dev/null
@@ -1,717 +0,0 @@
-// Copyright 2020 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package breaker
-
-import (
- "sync"
- "testing"
- "time"
-
- "github.com/google/go-cmp/cmp"
- "github.com/google/go-cmp/cmp/cmpopts"
-)
-
-func TestBucketReset(t *testing.T) {
- bu := bucket{12, 8}
- bu.reset()
- if bu.successes != 0 {
- t.Errorf("got successes = %d, want %d", bu.successes, 0)
- }
- if bu.failures != 0 {
- t.Errorf("got failures = %d, want %d", bu.failures, 0)
- }
-}
-
-func TestResetCounts(t *testing.T) {
- b := newTestBreaker(Config{})
- for i := 0; i < len(b.buckets); i++ {
- b.buckets[i].successes = 10
- b.buckets[i].failures = 15
- }
-
- b.resetCounts()
- testBuckets(t, b.buckets[:], 0, 0)
- testCounts(t, b, 0, 0, 0)
-}
-
-func TestNewBreaker(t *testing.T) {
- timeNow = func() time.Time {
- return time.Date(2020, time.May, 26, 18, 0, 0, 0, time.UTC)
- }
- got, err := New(Config{
- FailsToRed: 10,
- FailureThreshold: 0.65,
- GreenInterval: 20 * time.Second,
- MinTimeout: 30 * time.Second,
- MaxTimeout: 16 * time.Minute,
- SuccsToGreen: 15,
- })
- if err != nil {
- t.Fatalf("New() returned %e, want nil", err)
- }
- want := &Breaker{
- config: Config{
- FailsToRed: 10,
- FailureThreshold: 0.65,
- GreenInterval: 20 * time.Second,
- MinTimeout: 30 * time.Second,
- MaxTimeout: 16 * time.Minute,
- SuccsToGreen: 15,
- },
- buckets: [numBuckets]bucket{},
- granularity: 2500 * time.Millisecond,
- state: Green,
- cur: 0,
- consecutiveSuccs: 0,
- timeout: 30 * time.Second,
- lastEvent: time.Date(2020, time.May, 26, 18, 0, 0, 0, time.UTC),
- }
-
- diff := cmp.Diff(want, got, cmpopts.IgnoreUnexported(sync.Mutex{}), cmp.AllowUnexported(Breaker{}, bucket{}))
- if diff != "" {
- t.Fatalf("mismatch (-want +got):\n%s", diff)
- }
-}
-
-func TestIllegalBreaker(t *testing.T) {
- for _, test := range []struct {
- name string
- config Config
- }{
- {
- name: "FailsToRed cannot be 0",
- config: Config{
- FailsToRed: 0,
- FailureThreshold: 0.65,
- GreenInterval: 20 * time.Second,
- MinTimeout: 30 * time.Second,
- MaxTimeout: 16 * time.Minute,
- SuccsToGreen: 15,
- },
- },
- {
- name: "FailsToRed cannot be negative",
- config: Config{
- FailsToRed: -5,
- FailureThreshold: 0.65,
- GreenInterval: 20 * time.Second,
- MinTimeout: 30 * time.Second,
- MaxTimeout: 16 * time.Minute,
- SuccsToGreen: 15,
- },
- },
- {
- name: "FailureThreshold cannot be 0",
- config: Config{
- FailsToRed: 8,
- FailureThreshold: 0,
- GreenInterval: 20 * time.Second,
- MinTimeout: 30 * time.Second,
- MaxTimeout: 16 * time.Minute,
- SuccsToGreen: 15,
- },
- },
- {
- name: "FailureThreshold cannot be negative",
- config: Config{
- FailsToRed: 8,
- FailureThreshold: -0.8,
- GreenInterval: 20 * time.Second,
- MinTimeout: 30 * time.Second,
- MaxTimeout: 16 * time.Minute,
- SuccsToGreen: 15,
- },
- },
- {
- name: "FailureThreshold cannot exceed 1",
- config: Config{
- FailsToRed: 8,
- FailureThreshold: 1.2,
- GreenInterval: 20 * time.Second,
- MinTimeout: 30 * time.Second,
- MaxTimeout: 16 * time.Minute,
- SuccsToGreen: 15,
- },
- },
- {
- name: "GreenInterval cannot be 0",
- config: Config{
- FailsToRed: 8,
- FailureThreshold: 0.65,
- GreenInterval: 0,
- MinTimeout: 30 * time.Second,
- MaxTimeout: 16 * time.Minute,
- SuccsToGreen: 15,
- },
- },
- {
- name: "GreenInterval cannot be negative",
- config: Config{
- FailsToRed: 8,
- FailureThreshold: 0.65,
- GreenInterval: -4 * time.Second,
- MinTimeout: 30 * time.Second,
- MaxTimeout: 16 * time.Minute,
- SuccsToGreen: 15,
- },
- },
- {
- name: "MinTimeout cannot be 0",
- config: Config{
- FailsToRed: 8,
- FailureThreshold: 0.65,
- GreenInterval: 20 * time.Second,
- MinTimeout: 0,
- MaxTimeout: 16 * time.Minute,
- SuccsToGreen: 15,
- },
- },
- {
- name: "MinTimeout cannot be negative",
- config: Config{
- FailsToRed: 8,
- FailureThreshold: 0.65,
- GreenInterval: 20 * time.Second,
- MinTimeout: -2 * time.Second,
- MaxTimeout: 16 * time.Minute,
- SuccsToGreen: 15,
- },
- },
- {
- name: "MaxTimeout cannot be 0",
- config: Config{
- FailsToRed: 8,
- FailureThreshold: 0.65,
- GreenInterval: 20 * time.Second,
- MinTimeout: 30 * time.Second,
- MaxTimeout: 0,
- SuccsToGreen: 15,
- },
- },
- {
- name: "MaxTimeout cannot be negative",
- config: Config{
- FailsToRed: 8,
- FailureThreshold: 0.65,
- GreenInterval: 20 * time.Second,
- MinTimeout: 30 * time.Second,
- MaxTimeout: -12 * time.Minute,
- SuccsToGreen: 15,
- },
- },
- {
- name: "SuccsToGreen cannot be 0",
- config: Config{
- FailsToRed: 8,
- FailureThreshold: 0.65,
- GreenInterval: 20 * time.Second,
- MinTimeout: 30 * time.Second,
- MaxTimeout: 16 * time.Minute,
- SuccsToGreen: 0,
- },
- },
- {
- name: "SuccsToGreen cannot be negative",
- config: Config{
- FailsToRed: 8,
- FailureThreshold: 0.65,
- GreenInterval: 20 * time.Second,
- MinTimeout: 30 * time.Second,
- MaxTimeout: 16 * time.Minute,
- SuccsToGreen: -7,
- },
- },
- {
- name: "multiple illegal values return error",
- config: Config{
- FailsToRed: 0,
- FailureThreshold: 1.4,
- GreenInterval: 20 * time.Second,
- MinTimeout: -30 * time.Second,
- MaxTimeout: 16 * time.Minute,
- SuccsToGreen: 100,
- },
- },
- } {
- t.Run(test.name, func(t *testing.T) {
- b, err := New(test.config)
- if err == nil {
- t.Fatalf("New() returned nil error")
- }
- if b != nil {
- t.Fatalf("New() returned %+v, want nil", b)
- }
- })
- }
-}
-
-func TestBreakerGranularity(t *testing.T) {
- for _, test := range []struct {
- config Config
- want time.Duration
- }{
- {
- config: Config{},
- want: 1250 * time.Millisecond,
- },
- {
- config: Config{GreenInterval: 1 * time.Second},
- want: 125 * time.Millisecond,
- },
- {
- config: Config{GreenInterval: 3 * time.Second},
- want: 375 * time.Millisecond,
- },
- {
- config: Config{GreenInterval: 1 * time.Minute},
- want: 7500 * time.Millisecond,
- },
- {
- config: Config{GreenInterval: 1 * time.Hour},
- want: 450 * time.Second,
- },
- } {
- b := newTestBreaker(test.config)
- if b.granularity != test.want {
- t.Errorf("b.granularity = %d, want %d", b.granularity, test.want)
- }
- }
-}
-
-func TestState(t *testing.T) {
- for _, want := range []State{
- Green,
- Yellow,
- Red,
- } {
- b := newTestBreaker(Config{})
- b.state = want
- if got := b.checkState(); got != want {
- t.Errorf("b.checkState() = %s, got %s", got, want)
- }
- if got := b.State(); got != want {
- t.Errorf("b.State() = %s, want %s", got, want)
- }
- }
-}
-func TestAllow(t *testing.T) {
- for _, test := range []struct {
- state State
- shouldAllow bool
- }{
- {
- state: Green,
- shouldAllow: true,
- },
- {
- state: Yellow,
- shouldAllow: true,
- },
- {
- state: Red,
- shouldAllow: false,
- },
- } {
- b := newTestBreaker(Config{})
- b.state = test.state
- allowed := b.Allow()
- if allowed != test.shouldAllow {
- t.Errorf("b.Allow() = %t in %s, want %t", allowed, test.state, test.shouldAllow)
- }
- }
-}
-
-func TestSuccesses(t *testing.T) {
- b := newTestBreaker(Config{})
- b.succeeded()
- testCounts(t, b, 1, 1, 0)
- b.succeeded()
- testCounts(t, b, 2, 2, 0)
- b.succeeded()
- testCounts(t, b, 3, 3, 0)
-}
-
-func TestFailures(t *testing.T) {
- b := newTestBreaker(Config{})
- b.failed()
- testCounts(t, b, 0, 0, 1)
- b.failed()
- testCounts(t, b, 0, 0, 2)
- b.failed()
- testCounts(t, b, 0, 0, 3)
-}
-
-func TestSucceededAndFailed(t *testing.T) {
- b := newTestBreaker(Config{})
- b.succeeded()
- testCounts(t, b, 1, 1, 0)
- b.failed()
- testCounts(t, b, 0, 1, 1)
- b.failed()
- testCounts(t, b, 0, 1, 2)
- b.succeeded()
- testCounts(t, b, 1, 2, 2)
- b.succeeded()
- testCounts(t, b, 2, 3, 2)
- b.succeeded()
- testCounts(t, b, 3, 4, 2)
- b.failed()
- testCounts(t, b, 0, 4, 3)
-}
-
-func TestUpdate(t *testing.T) {
- now := time.Now()
- b := newTestBreaker(Config{})
- b.lastEvent = now
- b.granularity = 1 * time.Second
- for i := 0; i < len(b.buckets); i++ {
- b.buckets[i].successes = 4
- b.buckets[i].failures = 9
- }
-
- // Update 0 buckets.
- b.update(now.Add(-1 * time.Second))
- if b.cur != 0 {
- t.Errorf("cur: got %d, want %d", b.cur, 0)
- }
- testBuckets(t, b.buckets[:], 4, 9)
- testCounts(t, b, 0, 4*len(b.buckets), 9*len(b.buckets))
-
- // Update next 3 buckets.
- b.update(now.Add(3 * time.Second))
- if b.cur != 3 {
- t.Errorf("cur: got %d, want %d", b.cur, 3)
- }
- testBuckets(t, b.buckets[:1], 4, 9)
- testBuckets(t, b.buckets[1:4], 0, 0)
- testBuckets(t, b.buckets[4:], 4, 9)
- testCounts(t, b, 0, 4*len(b.buckets)-12, 9*len(b.buckets)-27)
-
- // Update all buckets.
- b.update(now.Add(1003 * time.Second))
- expectedCur := 0
- if b.cur != expectedCur {
- t.Errorf("cur: got %d, want %d", b.cur, expectedCur)
- }
- testBuckets(t, b.buckets[:], 0, 0)
- testCounts(t, b, 0, 0, 0)
-}
-
-func TestStateChanges(t *testing.T) {
- for _, test := range []struct {
- name string
- config Config
- preSuccesses int
- preFailures int
- fromState State
- allow bool
- success bool
- sleep time.Duration
- toState State
- }{
- {
- name: "breaker state remains green when FailsToRed is not exceeded",
- config: Config{FailsToRed: 8, FailureThreshold: 0.5},
- preSuccesses: 6,
- preFailures: 7,
- fromState: Green,
- allow: true,
- success: false,
- toState: Green,
- },
- {
- name: "breaker state remains green when FailureThreshold is not exceeded",
- config: Config{FailsToRed: 2, FailureThreshold: 0.8},
- preSuccesses: 3,
- preFailures: 6,
- fromState: Green,
- allow: true,
- success: false,
- toState: Green,
- },
- {
- name: "breaker state remains green when failure ratio = FailureThreshold",
- config: Config{FailsToRed: 10, FailureThreshold: 0.5},
- preSuccesses: 20,
- preFailures: 19,
- fromState: Green,
- allow: true,
- success: false,
- toState: Green,
- },
- {
- name: "breaker state remains green when failures = FailsToRed",
- config: Config{FailsToRed: 10, FailureThreshold: 0.3},
- preSuccesses: 10,
- preFailures: 9,
- fromState: Green,
- allow: true,
- success: false,
- toState: Green,
- },
- {
- name: "breaker state changes to red when FailureThreshold is exceeded and after FailsToRed has been exceeded",
- config: Config{FailsToRed: 10, FailureThreshold: 0.5},
- preSuccesses: 20,
- preFailures: 20,
- fromState: Green,
- allow: true,
- success: false,
- toState: Red,
- },
- {
- name: "breaker state changes to red when FailsToRed is exceeded and after FailureThreshold has been exceeded",
- config: Config{FailsToRed: 20, FailureThreshold: 0.3},
- preSuccesses: 20,
- preFailures: 20,
- fromState: Green,
- allow: true,
- success: false,
- toState: Red,
- },
- {
- name: "breaker state changes from green to red",
- config: Config{FailsToRed: 4, FailureThreshold: 0.5},
- preSuccesses: 4,
- preFailures: 4,
- fromState: Green,
- allow: true,
- success: false,
- toState: Red,
- },
- {
- name: "failure in yellow state changes breaker to red state",
- config: Config{},
- preSuccesses: 0,
- preFailures: 0,
- fromState: Yellow,
- allow: true,
- success: false,
- toState: Red,
- },
- {
- name: "breaker state changes from yellow to green",
- config: Config{SuccsToGreen: 1},
- preSuccesses: 0,
- preFailures: 0,
- fromState: Yellow,
- allow: true,
- success: true,
- toState: Green,
- },
- {
- name: "breaker state changes from red to yellow",
- config: Config{MinTimeout: 1 * time.Second},
- preSuccesses: 0,
- preFailures: 0,
- fromState: Red,
- sleep: 1*time.Second + 1*time.Nanosecond,
- toState: Yellow,
- },
- } {
- t.Run(test.name, func(t *testing.T) {
- now := time.Time{}
- timeNow = func() time.Time { return now }
- b := newTestBreaker(test.config)
- b.state = test.fromState
- b.buckets[0].successes = test.preSuccesses
- b.buckets[0].failures = test.preFailures
-
- allowed := b.Allow()
- if allowed != test.allow {
- t.Fatalf("b.Allow() = %t in %s, want %t", allowed, test.fromState, test.allow)
- }
- if test.allow {
- b.Record(test.success)
- }
-
- // Pseudo sleep.
- now = now.Add(test.sleep)
-
- if state := b.State(); state != test.toState {
- t.Errorf("b.State() = %s, want %s", state, test.toState)
- }
- })
- }
-}
-
-func TestRunningBreaker(t *testing.T) {
- now := time.Time{}
- timeNow = func() time.Time { return now }
- b := newTestBreaker(Config{
- GreenInterval: 5 * time.Second,
- })
-
- // The following tests happen sequentially. The tests' states depend on previous tests.
- for _, test := range []struct {
- name string
- firstSleep time.Duration
- allow bool
- secondSleep time.Duration
- success bool
- wantConsecutiveSuccs int
- wantSuccesses int
- wantFailures int
- }{
- {
- name: "successFunc called after a long time updates counts",
- firstSleep: 20 * time.Second,
- allow: true,
- secondSleep: 20 * time.Second,
- success: true,
- wantConsecutiveSuccs: 1,
- wantSuccesses: 1,
- wantFailures: 0,
- },
- {
- name: "success within GreenInterval updates counts correctly",
- firstSleep: 1 * time.Second,
- allow: true,
- secondSleep: 3 * time.Second,
- success: true,
- wantConsecutiveSuccs: 2,
- wantSuccesses: 2,
- wantFailures: 0,
- },
- {
- name: "success after a long time updates counts correctly",
- firstSleep: 30 * time.Second,
- allow: true,
- secondSleep: 80 * time.Second,
- success: true,
- wantConsecutiveSuccs: 3,
- wantSuccesses: 1,
- wantFailures: 0,
- },
- {
- name: "failure within GreenInterval updates counts correctly",
- firstSleep: 1 * time.Second,
- allow: true,
- secondSleep: 3 * time.Second,
- success: false,
- wantConsecutiveSuccs: 0,
- wantSuccesses: 1,
- wantFailures: 1,
- },
- {
- name: "second failure within GreenInterval updates counts correctly",
- firstSleep: 1 * time.Millisecond,
- allow: true,
- secondSleep: 3 * time.Millisecond,
- success: false,
- wantConsecutiveSuccs: 0,
- wantSuccesses: 1,
- wantFailures: 2,
- },
- {
- name: "failure after a long time updates counts correctly",
- firstSleep: 10 * time.Second,
- allow: true,
- secondSleep: 4 * time.Minute,
- success: false,
- wantConsecutiveSuccs: 0,
- wantSuccesses: 0,
- wantFailures: 1,
- },
- } {
- t.Run(test.name, func(t *testing.T) {
- now = now.Add(test.firstSleep)
- allowed := b.Allow()
-
- if allowed != test.allow {
- t.Fatalf("breaker.Allow() = %t, want %t", allowed, test.allow)
- }
-
- now = now.Add(test.secondSleep)
- if test.allow {
- b.Record(test.success)
- }
-
- testCounts(t, b, test.wantConsecutiveSuccs, test.wantSuccesses, test.wantFailures)
- })
- }
-}
-
-func TestIncreaseTimeout(t *testing.T) {
- b := newTestBreaker(Config{
- MinTimeout: 1 * time.Second,
- MaxTimeout: 12 * time.Second,
- })
- b.timeout = 3 * time.Second
-
- b.increaseTimeout()
- testTimeouts(t, b, 6*time.Second, 1*time.Second, 12*time.Second)
- b.increaseTimeout()
- testTimeouts(t, b, 12*time.Second, 1*time.Second, 12*time.Second)
- b.increaseTimeout()
- testTimeouts(t, b, 12*time.Second, 1*time.Second, 12*time.Second)
-
- b.config.MaxTimeout = 14 * time.Second
- testTimeouts(t, b, 12*time.Second, 1*time.Second, 14*time.Second)
- b.increaseTimeout()
- testTimeouts(t, b, 14*time.Second, 1*time.Second, 14*time.Second)
- b.increaseTimeout()
- testTimeouts(t, b, 14*time.Second, 1*time.Second, 14*time.Second)
-}
-
-// newTestBreaker is like New, but with default values for easier testing.
-func newTestBreaker(config Config) *Breaker {
- if config.FailsToRed <= 0 {
- config.FailsToRed = 10
- }
- if config.FailureThreshold <= 0 {
- config.FailureThreshold = 0.5
- }
- if config.GreenInterval <= 0 {
- config.GreenInterval = 10 * time.Second
- }
- if config.MinTimeout <= 0 {
- config.MinTimeout = 30 * time.Second
- }
- if config.MaxTimeout <= 0 {
- config.MaxTimeout = 4 * time.Minute
- }
- if config.SuccsToGreen <= 0 {
- config.SuccsToGreen = 20
- }
- b, _ := New(config)
- return b
-}
-
-func testCounts(t *testing.T, b *Breaker, consecutiveSuccs, wantSuccesses, wantFailures int) {
- if b.consecutiveSuccs != consecutiveSuccs {
- t.Errorf("b.consecutiveSuccs = %d, want %d", b.consecutiveSuccs, consecutiveSuccs)
- }
- successes, failures := b.counts()
- if successes != wantSuccesses {
- t.Errorf("successes = %d, want %d", successes, wantSuccesses)
- }
- if failures != wantFailures {
- t.Errorf("failures = %d, want %d", failures, wantFailures)
- }
-}
-
-func testBuckets(t *testing.T, buckets []bucket, successes, failures int) {
- for i, bu := range buckets {
- if bu.successes != successes {
- t.Errorf("slice bucket %d successes: got %d, want %d", i, bu.successes, successes)
- }
- if bu.failures != failures {
- t.Errorf("slice bucket %d failures: got %d, want %d", i, bu.failures, failures)
- }
- }
-}
-
-func testTimeouts(t *testing.T, b *Breaker, timeout, minTimeout, maxTimeout time.Duration) {
- if b.timeout != timeout {
- t.Errorf("b.timeout = %s, want %s", b.timeout, timeout)
- }
- if b.config.MinTimeout != minTimeout {
- t.Errorf("b.config.MinTimeout = %s, want %s", b.config.MinTimeout, minTimeout)
- }
- if b.config.MaxTimeout != maxTimeout {
- t.Errorf("b.config.MaxTimeout = %s, want %s", b.config.MaxTimeout, maxTimeout)
- }
-}
diff --git a/internal/teeproxy/teeproxy.go b/internal/teeproxy/teeproxy.go
deleted file mode 100644
index 29616d8..0000000
--- a/internal/teeproxy/teeproxy.go
+++ /dev/null
@@ -1,457 +0,0 @@
-// Copyright 2020 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// Package teeproxy provides functionality for running a service which tees
-// traffic to pkg.go.dev.
-package teeproxy
-
-import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io/ioutil"
- "net/http"
- "strconv"
- "strings"
- "time"
-
- "go.opencensus.io/plugin/ochttp"
- "go.opencensus.io/stats"
- "go.opencensus.io/stats/view"
- "go.opencensus.io/tag"
- "golang.org/x/net/context/ctxhttp"
- "golang.org/x/pkgsite/internal/breaker"
- "golang.org/x/pkgsite/internal/derrors"
- "golang.org/x/pkgsite/internal/log"
- "golang.org/x/time/rate"
-)
-
-// Server receives requests from godoc.org and tees them to specified hosts.
-type Server struct {
- hosts []string
- client *http.Client
- limiter *rate.Limiter
- breakers map[string]*breaker.Breaker
- // authKey and authValue are used to indicate to pkg.go.dev that the
- // request is coming from the teeproxy.
- authKey, authValue string
-}
-
-// Config contains configuration values for Server.
-type Config struct {
- // AuthKey is the name of the header that is used by pkg.go.dev to
- // determine if a request is coming from a trusted source.
- AuthKey string
- // AuthValue is the value of the header that is used by pkg.go.dev to
- // determine that the request is coming from the teeproxy.
- AuthValue string
- // Hosts is the list of hosts that the teeproxy forwards requests to.
- Hosts []string
- // Client is the HTTP client used by the teeproxy to forward requests
- // to the hosts.
- Client *http.Client
- // Rate is the rate at which requests are rate limited.
- Rate float64
- // Burst is the maximum burst of requests permitted.
- Burst int
- BreakerConfig breaker.Config
-}
-
-// RequestEvent stores information about a godoc.org or pkg.go.dev request.
-type RequestEvent struct {
- Host string
- Path string
- URL string
- Header http.Header
- Latency time.Duration
- Status int
- Error error
- // IsRobot reports whether this request came from a robot.
- // https://github.com/golang/gddo/blob/a4ebd2f/gddo-server/main.go#L152
- IsRobot bool
-}
-
-var gddoToPkgGoDevRequest = map[string]string{
- "/": "/",
- "/-/about": "/about",
- "/-/go": "/std",
- "/-/subrepo": "/search?q=golang.org/x",
- "/C": "/C",
- "/favicon.ico": "/favicon.ico",
-}
-
-// expected404s are a list of godoc.org URLs that we expected to 404 on
-// pkg.go.dev.
-var expected404s = map[string]bool{
- "/-/bootstrap.min.css": true,
- "/-/bootstrap.min.js": true,
- "/-/bot": true,
- "/-/jquery-2.0.3.min.js": true,
- "/-/refresh": true,
- "/-/sidebar.css": true,
- "/-/site.css": true,
- "/-/site.js": true,
- "/BingSiteAuth.xml": true,
- "/google3d2f3cd4cc2bb44b.html": true,
- "/humans.txt": true,
- "/robots.txt": true,
- "/third_party/jquery.timeago.js": true,
-}
-
-// statusRedBreaker is a custom HTTP status code that denotes that a request
-// cannot be handled because the circuit breaker is in the red state.
-const statusRedBreaker = 530
-
-var (
- // keyTeeproxyStatus is a census tag for teeproxy response status codes.
- keyTeeproxyStatus = tag.MustNewKey("teeproxy.status")
- // keyTeeproxyHost is a census tag for hosts that teeproxy forward requests to.
- keyTeeproxyHost = tag.MustNewKey("teeproxy.host")
- // keyTeeproxyPath is a census tag for godoc.org paths that don't work in
- // pkg.go.dev.
- keyTeeproxyPath = tag.MustNewKey("teeproxy.path")
- // teeproxyGddoLatency holds observed latency in individual teeproxy
- // requests from godoc.org.
- teeproxyGddoLatency = stats.Float64(
- "go-discovery/teeproxy/gddo-latency",
- "Latency of a teeproxy request from godoc.org.",
- stats.UnitMilliseconds,
- )
- // teeproxyPkgGoDevLatency holds observed latency in individual teeproxy
- // requests to pkg.go.dev.
- teeproxyPkgGoDevLatency = stats.Float64(
- "go-discovery/teeproxy/pkgGoDev-latency",
- "Latency of a teeproxy request to pkg.go.dev.",
- stats.UnitMilliseconds,
- )
- // teeproxyPkgGoDevBrokenPaths counts broken paths in pkg.go.dev that work
- // in godoc.org
- teeproxyPkgGoDevBrokenPaths = stats.Int64(
- "go-discovery/teeproxy/pkgGoDev-brokenPaths",
- "Count of paths that error in pkg.go.dev but 200 in godoc.org.",
- stats.UnitDimensionless,
- )
-
- // TeeproxyGddoRequestLatencyDistribution aggregates the latency of
- // teeproxy requests from godoc.org by status code and host.
- TeeproxyGddoRequestLatencyDistribution = &view.View{
- Name: "go-discovery/teeproxy/gddo-latency",
- Measure: teeproxyGddoLatency,
- Aggregation: ochttp.DefaultLatencyDistribution,
- Description: "Teeproxy latency from godoc.org, by response status code",
- TagKeys: []tag.Key{keyTeeproxyStatus, keyTeeproxyHost},
- }
- // TeeproxyPkgGoDevRequestLatencyDistribution aggregates the latency of
- // teeproxy requests to pkg.go.dev by status code and host.
- TeeproxyPkgGoDevRequestLatencyDistribution = &view.View{
- Name: "go-discovery/teeproxy/pkgGoDev-latency",
- Measure: teeproxyPkgGoDevLatency,
- Aggregation: ochttp.DefaultLatencyDistribution,
- Description: "Teeproxy latency to pkg.go.dev, by response status code",
- TagKeys: []tag.Key{keyTeeproxyStatus, keyTeeproxyHost},
- }
- // TeeproxyGddoRequestCount counts teeproxy requests from godoc.org.
- TeeproxyGddoRequestCount = &view.View{
- Name: "go-discovery/teeproxy/gddo-count",
- Measure: teeproxyGddoLatency,
- Aggregation: view.Count(),
- Description: "Count of teeproxy requests from godoc.org",
- TagKeys: []tag.Key{keyTeeproxyStatus, keyTeeproxyHost},
- }
- // TeeproxyPkgGoDevRequestCount counts teeproxy requests to pkg.go.dev.
- TeeproxyPkgGoDevRequestCount = &view.View{
- Name: "go-discovery/teeproxy/pkgGoDev-count",
- Measure: teeproxyPkgGoDevLatency,
- Aggregation: view.Count(),
- Description: "Count of teeproxy requests to pkg.go.dev",
- TagKeys: []tag.Key{keyTeeproxyStatus, keyTeeproxyHost},
- }
- // TeeproxyPkgGoDevBrokenPathCount counts teeproxy requests to pkg.go.dev
- // that return 4xx or 5xx but return 2xx or 3xx on godoc.org.
- TeeproxyPkgGoDevBrokenPathCount = &view.View{
- Name: "go-discovery/teeproxy/pkgGoDev-brokenPath",
- Measure: teeproxyPkgGoDevBrokenPaths,
- Aggregation: view.Count(),
- Description: "Count of broken paths in pkg.go.dev",
- TagKeys: []tag.Key{keyTeeproxyStatus, keyTeeproxyHost, keyTeeproxyPath},
- }
-)
-
-// NewServer returns a new Server struct with preconfigured settings.
-//
-// The server is rate limited and allows events up to a rate of "Rate" and
-// a burst of "Burst".
-//
-// The server also implements the circuit breaker pattern and maintains a
-// breaker for each host. Each breaker can be in one of three states: green,
-// yellow, or red.
-//
-// In the green state, the breaker remains green until it encounters a time
-// window of length "GreenInterval" where there are more than of "FailsToRed"
-// failures and a failureRatio of more than "FailureThreshold", in which case
-// the state becomes red.
-//
-// In the red state, the breaker halts all requests and waits for a timeout
-// period before shifting to the yellow state.
-//
-// In the yellow state, the breaker allows the first "SuccsToGreen" requests.
-// If any of these fail, the state reverts to red.
-// Otherwise, the state becomes green again.
-//
-// The timeout period is initially set to "MinTimeout" when the breaker shifts
-// from green to yellow. By default, the timeout period is doubled each time
-// the breaker fails to shift from the yellow state to the green state and is
-// capped at "MaxTimeout".
-func NewServer(config Config) (_ *Server, err error) {
- defer derrors.Wrap(&err, "NewServer")
- var breakers = make(map[string]*breaker.Breaker)
- for _, host := range config.Hosts {
- if host == "" {
- return nil, errors.New("host cannot be empty")
- }
- b, err := breaker.New(config.BreakerConfig)
- if err != nil {
- return nil, err
- }
- breakers[host] = b
- }
- var client = http.DefaultClient
- if config.Client != nil {
- client = config.Client
- }
-
- authKey := config.AuthKey
- if authKey == "" {
- authKey = "auth-key-for-testing"
- }
- return &Server{
- hosts: config.Hosts,
- client: client,
- limiter: rate.NewLimiter(rate.Limit(config.Rate), config.Burst),
- breakers: breakers,
- authKey: authKey,
- authValue: config.AuthValue,
- }, nil
-}
-
-// ServeHTTP receives requests from godoc.org and forwards them to the
-// specified hosts.
-// These requests are validated and rate limited before being forwarded. Too
-// many error responses returned by pkg.go.dev will cause the server to back
-// off temporarily before trying to forward requests to the hosts again.
-// ServeHTTP will always reply with StatusOK as long as the request is a valid
-// godoc.org request, even if the request could not be processed by the hosts.
-// Instead, problems with processing the request by the hosts will logged.
-func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- // Ignore internal App Engine requests.
- if strings.HasPrefix(r.URL.Path, "/_ah/") {
- // Don't log requests.
- return
- }
- results, status, err := s.doRequest(r)
- if err != nil {
- log.Infof(r.Context(), "teeproxy.Server.ServeHTTP: %v", err)
- http.Error(w, http.StatusText(status), status)
- return
- }
- log.Info(r.Context(), results)
-}
-
-func (s *Server) doRequest(r *http.Request) (results map[string]*RequestEvent, status int, err error) {
- defer derrors.Wrap(&err, "doRequest(%q): referer=%q", r.URL.Path, r.Referer())
- ctx := r.Context()
- if status, err = validateTeeProxyRequest(r); err != nil {
- return results, status, err
- }
- gddoEvent, err := getGddoEvent(r)
- if err != nil {
- return results, http.StatusBadRequest, err
- }
-
- results = map[string]*RequestEvent{
- "godoc.org": gddoEvent,
- }
- if gddoEvent.IsRobot {
- // Don't tee robot requests since these will use up pkg.go.dev cache
- // space.
- return results, http.StatusOK, nil
- }
- if _, ok := expected404s[gddoEvent.Path]; ok {
- // Don't tee these requests, since we know they will 404.
- return results, http.StatusOK, nil
- }
- for _, s := range []string{
- "?import-graph",
- "?status.png",
- "?status.svg",
- "api.godoc.org",
- } {
- if strings.Contains(gddoEvent.URL, s) {
- // Don't tee these requests, since we know they will 404.
- return results, http.StatusOK, nil
- }
- }
- if len(s.hosts) > 0 {
- rateLimited := !s.limiter.Allow()
- for _, host := range s.hosts {
- event := &RequestEvent{
- Host: host,
- }
-
- if rateLimited {
- event.Status = http.StatusTooManyRequests
- event.Error = errors.New("rate limit exceeded")
- } else {
- event = s.doRequestOnHost(ctx, gddoEvent, host)
- }
-
- if event.Error != nil {
- log.Errorf(r.Context(), "teeproxy.Server.doRequest(%q): %s", host, event.Error)
- }
- results[strings.TrimPrefix(host, "https://")] = event
- recordTeeProxyMetric(r.Context(), host, gddoEvent.Path, gddoEvent.Status, event.Status, gddoEvent.Latency, event.Latency)
- }
- }
- return results, http.StatusOK, nil
-}
-
-func (s *Server) doRequestOnHost(ctx context.Context, gddoEvent *RequestEvent, host string) *RequestEvent {
- redirectPath := pkgGoDevPath(gddoEvent.Path)
- event := &RequestEvent{
- Host: host,
- Path: redirectPath,
- }
-
- breaker := s.breakers[host]
- if breaker == nil {
- // This case should never be reached.
- event.Status = http.StatusInternalServerError
- event.Error = errors.New("breaker is nil")
- return event
- }
-
- if !breaker.Allow() {
- event.Status = statusRedBreaker
- event.Error = errors.New("breaker is red")
- return event
- }
-
- event = s.makePkgGoDevRequest(ctx, host, pkgGoDevPath(gddoEvent.Path))
- if event.Error != nil {
- return event
- }
- success := event.Status < http.StatusInternalServerError
- breaker.Record(success)
- return event
-}
-
-// validateTeeProxyRequest validates that a request to the teeproxy is allowed.
-// It will return the error code and error if a request is invalid. Otherwise,
-// it will return http.StatusOK.
-func validateTeeProxyRequest(r *http.Request) (code int, err error) {
- defer derrors.Wrap(&err, "validateTeeProxyRequest(r)")
- if r.Method != "POST" {
- return http.StatusMethodNotAllowed, fmt.Errorf("%s: %q", http.StatusText(http.StatusMethodNotAllowed), r.Method)
- }
- ct := r.Header.Get("Content-Type")
- if ct != "application/json; charset=utf-8" {
- return http.StatusUnsupportedMediaType, fmt.Errorf("Content-Type %q is not supported", ct)
- }
- return http.StatusOK, nil
-}
-
-// pkgGoDevPath returns the corresponding path on pkg.go.dev for the given
-// godoc.org path.
-func pkgGoDevPath(gddoPath string) string {
- redirectPath, ok := gddoToPkgGoDevRequest[gddoPath]
- if ok {
- return redirectPath
- }
- return gddoPath
-}
-
-// getGddoEvent constructs a url.URL and RequestEvent from the request.
-func getGddoEvent(r *http.Request) (gddoEvent *RequestEvent, err error) {
- defer func() {
- derrors.Wrap(&err, "getGddoEvent(r)")
- if gddoEvent != nil && err != nil {
- log.Info(r.Context(), map[string]interface{}{
- "godoc.org": gddoEvent,
- "tee-error": err.Error(),
- })
- }
- }()
- body, err := ioutil.ReadAll(r.Body)
- if err != nil {
- return nil, err
- }
- gddoEvent = &RequestEvent{}
- if err := json.Unmarshal(body, gddoEvent); err != nil {
- return nil, err
- }
- return gddoEvent, nil
-}
-
-// makePkgGoDevRequest makes a request to the redirectHost and redirectPath,
-// and returns a requestEvent based on the output.
-func (s *Server) makePkgGoDevRequest(ctx context.Context, redirectHost, redirectPath string) *RequestEvent {
- redirectURL := redirectHost + redirectPath
- event := &RequestEvent{
- Host: redirectHost,
- Path: redirectPath,
- URL: redirectURL,
- }
- defer derrors.Wrap(&event.Error, "makePkgGoDevRequest(%q, %q)", redirectHost, redirectPath)
-
- req, err := http.NewRequest("GET", redirectURL, nil)
- if err != nil {
- event.Status = http.StatusInternalServerError
- event.Error = err
- return event
- }
- start := time.Now()
- req.Header.Set(s.authKey, s.authValue)
- resp, err := ctxhttp.Do(ctx, s.client, req)
- if err != nil {
- // Use StatusBadGateway to indicate the upstream error.
- event.Status = http.StatusBadGateway
- event.Error = err
- return event
- }
-
- event.Status = resp.StatusCode
- event.Latency = time.Since(start)
- return event
-}
-
-// recordTeeProxyMetric records the latencies and counts of requests from
-// godoc.org and to pkg.go.dev, tagged with the response status code, as well
-// as any path that errors on pkg.go.dev but not on godoc.org.
-func recordTeeProxyMetric(ctx context.Context, host, path string, gddoStatus, pkgGoDevStatus int, gddoLatency, pkgGoDevLatency time.Duration) {
- gddoL := gddoLatency.Seconds() * 1000
- pkgGoDevL := pkgGoDevLatency.Seconds() * 1000
-
- // Record latency.
- stats.RecordWithTags(ctx, []tag.Mutator{
- tag.Upsert(keyTeeproxyStatus, strconv.Itoa(pkgGoDevStatus)),
- tag.Upsert(keyTeeproxyHost, host),
- },
- teeproxyGddoLatency.M(gddoL),
- teeproxyPkgGoDevLatency.M(pkgGoDevL),
- )
-
- // Record path that returns 4xx or 5xx on pkg.go.dev but returns 2xx or 3xx
- // on godoc.org, excluding rate limiter and circuit breaker errors.
- if pkgGoDevStatus >= 400 && gddoStatus < 400 &&
- pkgGoDevStatus != http.StatusTooManyRequests && pkgGoDevStatus != statusRedBreaker {
- stats.RecordWithTags(ctx, []tag.Mutator{
- tag.Upsert(keyTeeproxyStatus, strconv.Itoa(pkgGoDevStatus)),
- tag.Upsert(keyTeeproxyHost, host),
- tag.Upsert(keyTeeproxyPath, path),
- },
- teeproxyPkgGoDevBrokenPaths.M(1),
- )
- }
-}
diff --git a/internal/teeproxy/teeproxy_test.go b/internal/teeproxy/teeproxy_test.go
deleted file mode 100644
index 2ca0616..0000000
--- a/internal/teeproxy/teeproxy_test.go
+++ /dev/null
@@ -1,428 +0,0 @@
-// Copyright 2020 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package teeproxy
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "net/http"
- "net/http/httptest"
- "testing"
- "time"
-
- "github.com/google/go-cmp/cmp"
- "github.com/google/go-cmp/cmp/cmpopts"
- "golang.org/x/pkgsite/internal/breaker"
-)
-
-func TestPkgGoDevPath(t *testing.T) {
- for _, test := range []struct {
- path string
- want string
- }{
- {
- path: "/-/about",
- want: "/about",
- },
- {
- path: "/-/subrepo",
- want: "/search?q=golang.org/x",
- },
- {
- path: "/net/http",
- want: "/net/http",
- },
- {
- path: "/",
- want: "/",
- },
- {
- path: "",
- want: "",
- },
- } {
- if got := pkgGoDevPath(test.path); got != test.want {
- t.Fatalf("pkgGoDevPath(%q) = %q; want = %q", test.path, got, test.want)
- }
- }
-}
-
-func TestPkgGoDevRequest(t *testing.T) {
- ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- }))
- defer ts.Close()
-
- ctx := context.Background()
- s := newTestServer(Config{})
-
- got := s.makePkgGoDevRequest(ctx, ts.URL, "")
- if got.Error != nil {
- t.Fatal(got.Error)
- }
-
- want := &RequestEvent{
- Host: ts.URL,
- URL: ts.URL,
- Status: http.StatusOK,
- }
- if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(RequestEvent{}, "Latency")); diff != "" {
- t.Fatalf("mismatch (-want +got):\n%s", diff)
- }
-}
-
-func TestGetGddoEvent(t *testing.T) {
- for _, test := range []struct {
- gddoEvent *RequestEvent
- }{
- {
-
- &RequestEvent{
- Host: "godoc.org",
- URL: "https://godoc.org/net/http",
- Latency: 100,
- Status: 200,
- },
- },
- } {
- requestBody, err := json.Marshal(test.gddoEvent)
- if err != nil {
- t.Fatal(err)
- }
- r := httptest.NewRequest("POST", "/", bytes.NewBuffer(requestBody))
- gotEvent, err := getGddoEvent(r)
- if err != nil {
- t.Fatal(err)
- }
- if diff := cmp.Diff(test.gddoEvent, gotEvent); diff != "" {
- t.Fatalf("mismatch (-want +got):\n%s", diff)
- }
- }
-}
-
-func TestServerHandler(t *testing.T) {
- for _, test := range []struct {
- name string
- serverConfig Config
- handler http.Handler
- steps []interface{}
- }{
- {
- name: "rate limiter permits requests below cap",
- serverConfig: Config{Rate: 20, Burst: 20},
- handler: alwaysHandler{http.StatusOK},
- steps: []interface{}{
- request{15, http.StatusOK},
- },
- },
- {
- name: "rate limiter permits requests up to cap",
- serverConfig: Config{Rate: 20, Burst: 20},
- handler: alwaysHandler{http.StatusOK},
- steps: []interface{}{
- request{20, http.StatusOK},
- },
- },
- {
- name: "rate limiter drops requests over cap",
- serverConfig: Config{Rate: 5, Burst: 5},
- handler: alwaysHandler{http.StatusOK},
- steps: []interface{}{
- request{5, http.StatusOK},
- request{6, http.StatusTooManyRequests},
- },
- },
- {
- name: "rate limiter permits requests after replenishing",
- serverConfig: Config{Rate: 2, Burst: 2},
- handler: alwaysHandler{http.StatusOK},
- steps: []interface{}{
- request{2, http.StatusOK},
- request{3, http.StatusTooManyRequests},
- wait{1 * time.Second},
- request{2, http.StatusOK},
- request{3, http.StatusTooManyRequests},
- },
- },
- {
- name: "green breaker passes requests",
- serverConfig: Config{Rate: 100, Burst: 100},
- handler: alwaysHandler{http.StatusOK},
- steps: []interface{}{
- checkState{breaker.Green},
- request{25, http.StatusOK},
- checkState{breaker.Green},
- request{25, http.StatusOK},
- checkState{breaker.Green},
- },
- },
- {
- name: "green breaker resets failure count after interval",
- serverConfig: Config{BreakerConfig: breaker.Config{
- FailsToRed: 5,
- GreenInterval: 100 * time.Millisecond,
- }},
- handler: alwaysHandler{http.StatusServiceUnavailable},
- steps: []interface{}{
- checkState{breaker.Green},
- request{5, http.StatusServiceUnavailable},
- checkState{breaker.Green},
- wait{150 * time.Millisecond},
- checkState{breaker.Green},
- request{5, http.StatusServiceUnavailable},
- checkState{breaker.Green},
- request{1, http.StatusServiceUnavailable},
- checkState{breaker.Red},
- },
- },
- {
- name: "breaker changes to red state and blocks requests",
- serverConfig: Config{BreakerConfig: breaker.Config{
- FailsToRed: 5,
- MinTimeout: 1 * time.Second,
- }},
- handler: alwaysHandler{http.StatusServiceUnavailable},
- steps: []interface{}{
- checkState{breaker.Green},
- request{6, http.StatusServiceUnavailable},
- checkState{breaker.Red},
- request{20, statusRedBreaker},
- checkState{breaker.Red},
- wait{100 * time.Millisecond},
- request{20, statusRedBreaker},
- checkState{breaker.Red},
- },
- },
- {
- name: "breaker changes to yellow state",
- serverConfig: Config{BreakerConfig: breaker.Config{
- FailsToRed: 5,
- MinTimeout: 100 * time.Millisecond,
- }},
- handler: &handler{6, http.StatusServiceUnavailable, alwaysHandler{http.StatusOK}},
- steps: []interface{}{
- request{6, http.StatusServiceUnavailable},
- checkState{breaker.Red},
- request{20, statusRedBreaker},
- checkState{breaker.Red},
- wait{150 * time.Millisecond},
- checkState{breaker.Yellow},
- request{9, http.StatusOK},
- checkState{breaker.Yellow},
- },
- },
- {
- name: "breaker changes to green state again",
- serverConfig: Config{BreakerConfig: breaker.Config{
- FailsToRed: 5,
- MinTimeout: 100 * time.Millisecond,
- SuccsToGreen: 10,
- }},
- handler: &handler{6, http.StatusServiceUnavailable, alwaysHandler{http.StatusOK}},
- steps: []interface{}{
- request{6, http.StatusServiceUnavailable},
- request{20, statusRedBreaker},
- wait{150 * time.Millisecond},
- request{9, http.StatusOK},
- checkState{breaker.Yellow},
- request{1, http.StatusOK},
- checkState{breaker.Green},
- request{5, http.StatusOK},
- },
- },
- {
- name: "breaker reverts to red state and doubles timeout period on repeated failures",
- serverConfig: Config{BreakerConfig: breaker.Config{
- FailsToRed: 5,
- MinTimeout: 100 * time.Millisecond,
- MaxTimeout: 400 * time.Millisecond,
- }},
- handler: alwaysHandler{http.StatusServiceUnavailable},
- steps: []interface{}{
- request{6, http.StatusServiceUnavailable},
- checkState{breaker.Red},
- wait{100 * time.Millisecond},
- checkState{breaker.Yellow},
- request{1, http.StatusServiceUnavailable},
- checkState{breaker.Red},
- wait{100 * time.Millisecond},
- checkState{breaker.Red},
- wait{100 * time.Millisecond},
- checkState{breaker.Yellow},
- request{1, http.StatusServiceUnavailable},
- checkState{breaker.Red},
- },
- },
- {
- name: "breaker timeout period does not exceed maxTimeout",
- serverConfig: Config{BreakerConfig: breaker.Config{
- FailsToRed: 5,
- MinTimeout: 100 * time.Millisecond,
- MaxTimeout: 100 * time.Millisecond,
- }},
- handler: alwaysHandler{http.StatusServiceUnavailable},
- steps: []interface{}{
- request{6, http.StatusServiceUnavailable},
- checkState{breaker.Red},
- wait{100 * time.Millisecond},
- checkState{breaker.Yellow},
- request{1, http.StatusServiceUnavailable},
- checkState{breaker.Red},
- wait{100 * time.Millisecond},
- checkState{breaker.Yellow},
- },
- },
- } {
- t.Run(test.name, func(t *testing.T) {
- mockPkgGoDevServer := httptest.NewServer(test.handler)
- defer mockPkgGoDevServer.Close()
- test.serverConfig.Hosts = []string{mockPkgGoDevServer.URL}
- server := newTestServer(test.serverConfig)
- executeSteps(t, server, mockPkgGoDevServer.URL, test.steps)
- })
- }
-}
-
-func executeSteps(t *testing.T, server *Server, pkgGoDevURL string, steps []interface{}) {
- for s, step := range steps {
- switch step := step.(type) {
- case request:
- for i := 0; i < step.repeat; i++ {
- event := makePostRequest(t, server, pkgGoDevURL)
- if event.Status != step.expectedStatus {
- t.Errorf("step %d request %d: got status %d, want %d", s, i, event.Status, step.expectedStatus)
- }
- }
- case wait:
- time.Sleep(step.wait)
- case checkState:
- if server.breakers[pkgGoDevURL].State() != step.expectedState {
- t.Errorf("step %d: got %s, want %s", s, server.breakers[pkgGoDevURL].State().String(), step.expectedState.String())
- }
- default:
- panic("invalid step type")
- }
- }
-}
-
-// TestHandler tests that the handler struct returns
-// the correct status codes.
-func TestHandler(t *testing.T) {
- h := &handler{5, 500, alwaysHandler{200}}
- s := httptest.NewServer(h)
- defer s.Close()
-
- for i := 0; i < 5; i++ {
- resp, err := http.PostForm(s.URL, nil)
- if err != nil {
- t.Fatal(err)
- }
- if resp.StatusCode != 500 {
- t.Errorf("request %d: got status %d, want %d", i, resp.StatusCode, 500)
- }
- }
-
- for i := 0; i < 20; i++ {
- resp, err := http.PostForm(s.URL, nil)
- if err != nil {
- t.Fatal(err)
- }
- if resp.StatusCode != 200 {
- t.Errorf("request %d: got status %d, want %d", i, resp.StatusCode, 200)
- }
- }
-}
-
-func makePostRequest(t *testing.T, server *Server, pkgGoDevURL string) *RequestEvent {
- gddoEvent := &RequestEvent{
- Host: "godoc.org",
- URL: "https://godoc.org/net/http",
- }
- requestBody, err := json.Marshal(gddoEvent)
- if err != nil {
- t.Fatal(err)
- }
- r := httptest.NewRequest("POST", "/", bytes.NewBuffer(requestBody))
- r.Header.Set("Content-Type", "application/json; charset=utf-8")
- results, _, err := server.doRequest(r)
- if err != nil || results == nil {
- t.Fatalf("doRequest = %v: %v", results, err)
- }
- event := results[pkgGoDevURL]
- if event == nil {
- t.Fatalf("results[%q] = %v", pkgGoDevURL, event)
- }
- return event
-}
-
-// newTestServer is like NewServer, but with default values for easier testing.
-func newTestServer(config Config) *Server {
- // Set default values.
- if config.Rate <= 0 {
- config.Rate = 50
- }
- if config.Burst <= 0 {
- config.Burst = 50
- }
- if config.BreakerConfig.FailsToRed <= 0 {
- config.BreakerConfig.FailsToRed = 10
- }
- if config.BreakerConfig.FailureThreshold <= 0 {
- config.BreakerConfig.FailureThreshold = 0.5
- }
- if config.BreakerConfig.GreenInterval <= 0 {
- config.BreakerConfig.GreenInterval = 200 * time.Millisecond
- }
- if config.BreakerConfig.MinTimeout <= 0 {
- config.BreakerConfig.MinTimeout = 100 * time.Millisecond
- }
- if config.BreakerConfig.MaxTimeout <= 0 {
- config.BreakerConfig.MaxTimeout = 400 * time.Millisecond
- }
- if config.BreakerConfig.SuccsToGreen <= 0 {
- config.BreakerConfig.SuccsToGreen = 20
- }
-
- server, _ := NewServer(config)
- return server
-}
-
-// handler returns statusCode for the first n requests
-// and uses innerHandler to serve the remaining requests.
-type handler struct {
- n int
- statusCode int
- innerHandler http.Handler
-}
-
-func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if h.n <= 0 {
- h.innerHandler.ServeHTTP(w, r)
- return
- }
- h.n--
- w.WriteHeader(h.statusCode)
-}
-
-type alwaysHandler struct {
- statusCode int
-}
-
-func (h alwaysHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(h.statusCode)
-}
-
-type request struct {
- repeat int
- expectedStatus int
-}
-
-type wait struct {
- wait time.Duration
-}
-
-type checkState struct {
- expectedState breaker.State
-}