From 54a24b76be09128c6e4a8a4a0e476c15e90aca89 Mon Sep 17 00:00:00 2001 From: Chris Banks Date: Mon, 30 Oct 2023 14:13:22 +0000 Subject: [PATCH] WIP: add a configurable default backend. 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. --- lib/router.go | 19 ++++++++++++++++--- main.go | 9 ++++++++- triemux/metrics.go | 8 -------- triemux/mux.go | 33 +++++++++++++++------------------ triemux/mux_test.go | 39 ++++----------------------------------- 5 files changed, 43 insertions(+), 65 deletions(-) diff --git a/lib/router.go b/lib/router.go index a05ae423..0131bc83 100644 --- a/lib/router.go +++ b/lib/router.go @@ -2,6 +2,7 @@ package router import ( "fmt" + "log" "net/http" "net/url" "os" @@ -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 @@ -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) @@ -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, @@ -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) diff --git a/main.go b/main.go index f2d878e3..927ce3e4 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/http" + "net/url" "os" "runtime" "time" @@ -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) @@ -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, diff --git a/triemux/metrics.go b/triemux/metrics.go index 187e7177..de415f40 100644 --- a/triemux/metrics.go +++ b/triemux/metrics.go @@ -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", @@ -22,7 +15,6 @@ var ( func RegisterMetrics(r prometheus.Registerer) { r.MustRegister( - entryNotFoundCountMetric, internalServiceUnavailableCountMetric, ) } diff --git a/triemux/mux.go b/triemux/mux.go index a669c9b0..d686dd38 100644 --- a/triemux/mux.go +++ b/triemux/mux.go @@ -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) { @@ -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 diff --git a/triemux/mux_test.go b/triemux/mux_test.go index 209a1265..d9343feb 100644 --- a/triemux/mux_test.go +++ b/triemux/mux_test.go @@ -6,8 +6,6 @@ import ( "os" "strings" "testing" - - promtest "github.com/prometheus/client_golang/prometheus/testutil" ) type SplitExample struct { @@ -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) } @@ -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) } @@ -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 {