Skip to content

Commit

Permalink
WIP: add a configurable default backend.
Browse files Browse the repository at this point in the history
Allow specifying a default backend, where we forward all requests that
don't match any entry in the exact or prefix tries.

93% of routes go to the same backend (`government-frontend`) so this
should allow shrinking the route database by an order of magnitude as
well as making it a little easier to eliminate Router in the long run.
  • Loading branch information
sengi committed Oct 30, 2023
1 parent e34f4ea commit 54a24b7
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 65 deletions.
19 changes: 16 additions & 3 deletions lib/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package router

import (
"fmt"
"log"
"net/http"
"net/url"
"os"
Expand Down Expand Up @@ -40,6 +41,7 @@ const (
// MongoReplicaSet, MongoReplicaSetMember etc. should move out of this module.
type Router struct {
mux *triemux.Mux
defaultHandler http.Handler
lock sync.RWMutex
mongoReadToOptime bson.MongoTimestamp
logger logger.Logger
Expand Down Expand Up @@ -92,7 +94,7 @@ func RegisterMetrics(r prometheus.Registerer) {

// NewRouter returns a new empty router instance. You will need to call
// SelfUpdateRoutes() to initialise the self-update process for routes.
func NewRouter(o Options) (rt *Router, err error) {
func NewRouter(defaultBackend *url.URL, o Options) (rt *Router, err error) {
logInfo("router: using mongo poll interval:", o.MongoPollInterval)
logInfo("router: using backend connect timeout:", o.BackendConnTimeout)
logInfo("router: using backend header timeout:", o.BackendHeaderTimeout)
Expand All @@ -108,9 +110,20 @@ func NewRouter(o Options) (rt *Router, err error) {
return nil, err
}

defaultHandler := handlers.NewBackendHandler(
"default",
defaultBackend,
o.BackendConnTimeout,
o.BackendHeaderTimeout,
l,
)
if err != nil {
log.Fatal(err)
}

reloadChan := make(chan bool, 1)
rt = &Router{
mux: triemux.NewMux(),
mux: triemux.NewMux(defaultHandler),
mongoReadToOptime: mongoReadToOptime,
logger: l,
opts: o,
Expand Down Expand Up @@ -235,7 +248,7 @@ func (rt *Router) reloadRoutes(db *mgo.Database, currentOptime bson.MongoTimesta
}()

logInfo("router: reloading routes")
newmux := triemux.NewMux()
newmux := triemux.NewMux(rt.defaultHandler)

backends := rt.loadBackends(db.C("backends"))
loadRoutes(db.C("routes"), newmux, backends)
Expand Down
9 changes: 8 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"log"
"net/http"
"net/url"
"os"
"runtime"
"time"
Expand Down Expand Up @@ -96,6 +97,7 @@ func main() {
beHeaderTimeout = getenvDuration("ROUTER_BACKEND_HEADER_TIMEOUT", "20s")
feReadTimeout = getenvDuration("ROUTER_FRONTEND_READ_TIMEOUT", "60s")
feWriteTimeout = getenvDuration("ROUTER_FRONTEND_WRITE_TIMEOUT", "60s")
defaultBackend = getenv("ROUTER_DEFAULT_BACKEND_URL", "http://government-frontend")
)

log.Printf("using frontend read timeout: %v", feReadTimeout)
Expand All @@ -110,7 +112,12 @@ func main() {

router.RegisterMetrics(prometheus.DefaultRegisterer)

rout, err := router.NewRouter(router.Options{
d, err := url.Parse(defaultBackend)
if err != nil {
log.Fatal(err)
}

rout, err := router.NewRouter(d, router.Options{
MongoURL: mongoURL,
MongoDBName: mongoDBName,
MongoPollInterval: mongoPollInterval,
Expand Down
8 changes: 0 additions & 8 deletions triemux/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@ import (
)

var (
entryNotFoundCountMetric = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "router_triemux_entry_not_found_total",
Help: "Number of route lookups for which no route was found",
},
)

internalServiceUnavailableCountMetric = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "router_service_unavailable_error_total",
Expand All @@ -22,7 +15,6 @@ var (

func RegisterMetrics(r prometheus.Registerer) {
r.MustRegister(
entryNotFoundCountMetric,
internalServiceUnavailableCountMetric,
)
}
33 changes: 15 additions & 18 deletions triemux/mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,24 @@ import (
)

type Mux struct {
mu sync.RWMutex
exactTrie *trie.Trie[http.Handler]
prefixTrie *trie.Trie[http.Handler]
count int
mu sync.RWMutex
defaultHandler http.Handler
exactTrie *trie.Trie[http.Handler]
prefixTrie *trie.Trie[http.Handler]
count int
}

// NewMux makes a new empty Mux.
func NewMux() *Mux {
func NewMux(defaultHandler http.Handler) *Mux {
return &Mux{
exactTrie: trie.NewTrie[http.Handler](),
defaultHandler: defaultHandler,
exactTrie: trie.NewTrie[http.Handler](),
prefixTrie: trie.NewTrie[http.Handler](),
}
}

// ServeHTTP dispatches the request to a backend with a registered route
// matching the request path, or 404s.
// matching the request path, or the default backend.
//
// If the routing table is empty, return a 503.
func (mux *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Expand All @@ -42,28 +44,23 @@ func (mux *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}

handler, ok := mux.lookup(r.URL.Path)
if !ok {
http.NotFound(w, r)
return
}
handler.ServeHTTP(w, r)
mux.lookup(r.URL.Path).ServeHTTP(w, r)
}

// lookup finds a URL path in the Mux and returns the corresponding handler.
func (mux *Mux) lookup(path string) (handler http.Handler, ok bool) {
func (mux *Mux) lookup(path string) http.Handler {
mux.mu.RLock()
defer mux.mu.RUnlock()

pathSegments := splitPath(path)
if handler, ok = mux.exactTrie.Get(pathSegments); !ok {
handler, ok := mux.exactTrie.Get(pathSegments)
if !ok {
handler, ok = mux.prefixTrie.GetLongestPrefix(pathSegments)
}
if !ok {
entryNotFoundCountMetric.Inc()
return nil, false
return mux.defaultHandler
}
return
return handler
}

// Handle adds a route (either an exact path or a path prefix) to the Mux and
Expand Down
39 changes: 4 additions & 35 deletions triemux/mux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (
"os"
"strings"
"testing"

promtest "github.com/prometheus/client_golang/prometheus/testutil"
)

type SplitExample struct {
Expand Down Expand Up @@ -162,48 +160,19 @@ var lookupExamples = []LookupExample{
}

func TestLookup(t *testing.T) {
beforeCount := promtest.ToFloat64(entryNotFoundCountMetric)

for _, ex := range lookupExamples {
testLookup(t, ex)
}

afterCount := promtest.ToFloat64(entryNotFoundCountMetric)
notFoundCount := afterCount - beforeCount

var expectedNotFoundCount int

for _, ex := range lookupExamples {
for _, c := range ex.checks {
if !c.ok {
expectedNotFoundCount++
}
}
}

if expectedNotFoundCount == 0 {
t.Errorf("expectedNotFoundCount should not be zero")
}

if notFoundCount != float64(expectedNotFoundCount) {
t.Errorf(
"Expected notFoundCount (%f) ok to be %f",
notFoundCount, float64(expectedNotFoundCount),
)
}
}

func testLookup(t *testing.T, ex LookupExample) {
mux := NewMux()
mux := NewMux(nil)
for _, r := range ex.registrations {
t.Logf("Register(path:%v, prefix:%v, handler:%v)", r.path, r.prefix, r.handler)
mux.Handle(r.path, r.prefix, r.handler)
}
for _, c := range ex.checks {
handler, ok := mux.lookup(c.path)
if ok != c.ok {
t.Errorf("Expected lookup(%v) ok to be %v, was %v", c.path, c.ok, ok)
}
handler := mux.lookup(c.path)
if handler != c.handler {
t.Errorf("Expected lookup(%v) to map to handler %v, was %v", c.path, c.handler, handler)
}
Expand All @@ -217,7 +186,7 @@ var statsExample = []Registration{
}

func TestRouteCount(t *testing.T) {
mux := NewMux()
mux := NewMux(nil)
for _, reg := range statsExample {
mux.Handle(reg.path, reg.prefix, reg.handler)
}
Expand All @@ -238,7 +207,7 @@ func loadStrings(filename string) []string {
func benchSetup() *Mux {
routes := loadStrings("testdata/routes")

tm := NewMux()
tm := NewMux(nil)
tm.Handle("/government", true, a)

for _, l := range routes {
Expand Down

0 comments on commit 54a24b7

Please sign in to comment.