From b4f4fcd437c2f9816f9511217bde703679808679 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 9 Sep 2019 21:44:58 -0600 Subject: Migrate some selection policy tests over to v2 --- modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go | 2 +- modules/caddyhttp/reverseproxy/hosts.go | 20 +- .../caddyhttp/reverseproxy/selectionpolicies.go | 6 +- .../reverseproxy/selectionpolicies_test.go | 604 +++++++++------------ 4 files changed, 272 insertions(+), 360 deletions(-) diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go index 66779e4..91039c9 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go @@ -55,7 +55,7 @@ type Transport struct { // PATH_INFO for the CGI script to use. SplitPath string `json:"split_path,omitempty"` - // Environment Variables + // Environment variables (TODO: make a map of string to value...?) EnvVars [][2]string `json:"env,omitempty"` // The duration used to set a deadline when connecting to an upstream. diff --git a/modules/caddyhttp/reverseproxy/hosts.go b/modules/caddyhttp/reverseproxy/hosts.go index ad27625..1c0fae3 100644 --- a/modules/caddyhttp/reverseproxy/hosts.go +++ b/modules/caddyhttp/reverseproxy/hosts.go @@ -34,19 +34,21 @@ type Host interface { // Unhealthy returns true if the backend is unhealthy. Unhealthy() bool - // CountRequest counts the given number of requests - // as currently in process with the host. The count - // should not go below 0. + // CountRequest atomically counts the given number of + // requests as currently in process with the host. The + // count should not go below 0. CountRequest(int) error - // CountFail counts the given number of failures - // with the host. The count should not go below 0. + // CountFail atomically counts the given number of + // failures with the host. The count should not go + // below 0. CountFail(int) error - // SetHealthy marks the host as either healthy (true) - // or unhealthy (false). If the given status is the - // same, this should be a no-op. It returns true if - // the given status was different, false otherwise. + // SetHealthy atomically marks the host as either + // healthy (true) or unhealthy (false). If the given + // status is the same, this should be a no-op and + // return false. It returns true if the status was + // changed; i.e. if it is now different from before. SetHealthy(bool) (bool, error) } diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies.go b/modules/caddyhttp/reverseproxy/selectionpolicies.go index 9680583..5bb2d62 100644 --- a/modules/caddyhttp/reverseproxy/selectionpolicies.go +++ b/modules/caddyhttp/reverseproxy/selectionpolicies.go @@ -82,7 +82,7 @@ type RandomChoiceSelection struct { // CaddyModule returns the Caddy module information. func (RandomChoiceSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.reverse_proxy.selection_policies.random_choice", + Name: "http.handlers.reverse_proxy.selection_policies.random_choose", New: func() caddy.Module { return new(RandomChoiceSelection) }, } } @@ -147,14 +147,14 @@ func (LeastConnSelection) CaddyModule() caddy.ModuleInfo { func (LeastConnSelection) Select(pool UpstreamPool, _ *http.Request) *Upstream { var bestHost *Upstream var count int - var leastReqs int + leastReqs := -1 for _, host := range pool { if !host.Available() { continue } numReqs := host.NumRequests() - if numReqs < leastReqs { + if leastReqs == -1 || numReqs < leastReqs { leastReqs = numReqs count = 0 } diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies_test.go b/modules/caddyhttp/reverseproxy/selectionpolicies_test.go index 8006fb1..e9939d6 100644 --- a/modules/caddyhttp/reverseproxy/selectionpolicies_test.go +++ b/modules/caddyhttp/reverseproxy/selectionpolicies_test.go @@ -14,350 +14,260 @@ package reverseproxy -// TODO: finish migrating these - -// import ( -// "net/http" -// "net/http/httptest" -// "os" -// "testing" -// ) - -// var workableServer *httptest.Server - -// func TestMain(m *testing.M) { -// workableServer = httptest.NewServer(http.HandlerFunc( -// func(w http.ResponseWriter, r *http.Request) { -// // do nothing -// })) -// r := m.Run() -// workableServer.Close() -// os.Exit(r) -// } - -// type customPolicy struct{} - -// func (customPolicy) Select(pool HostPool, _ *http.Request) Host { -// return pool[0] -// } - -// func testPool() HostPool { -// pool := []*UpstreamHost{ -// { -// Name: workableServer.URL, // this should resolve (healthcheck test) -// }, -// { -// Name: "http://localhost:99998", // this shouldn't -// }, -// { -// Name: "http://C", -// }, -// } -// return HostPool(pool) -// } - -// func TestRoundRobinPolicy(t *testing.T) { -// pool := testPool() -// rrPolicy := &RoundRobin{} -// request, _ := http.NewRequest("GET", "/", nil) - -// h := rrPolicy.Select(pool, request) -// // First selected host is 1, because counter starts at 0 -// // and increments before host is selected -// if h != pool[1] { -// t.Error("Expected first round robin host to be second host in the pool.") -// } -// h = rrPolicy.Select(pool, request) -// if h != pool[2] { -// t.Error("Expected second round robin host to be third host in the pool.") -// } -// h = rrPolicy.Select(pool, request) -// if h != pool[0] { -// t.Error("Expected third round robin host to be first host in the pool.") -// } -// // mark host as down -// pool[1].Unhealthy = 1 -// h = rrPolicy.Select(pool, request) -// if h != pool[2] { -// t.Error("Expected to skip down host.") -// } -// // mark host as up -// pool[1].Unhealthy = 0 - -// h = rrPolicy.Select(pool, request) -// if h == pool[2] { -// t.Error("Expected to balance evenly among healthy hosts") -// } -// // mark host as full -// pool[1].Conns = 1 -// pool[1].MaxConns = 1 -// h = rrPolicy.Select(pool, request) -// if h != pool[2] { -// t.Error("Expected to skip full host.") -// } -// } - -// func TestLeastConnPolicy(t *testing.T) { -// pool := testPool() -// lcPolicy := &LeastConn{} -// request, _ := http.NewRequest("GET", "/", nil) - -// pool[0].Conns = 10 -// pool[1].Conns = 10 -// h := lcPolicy.Select(pool, request) -// if h != pool[2] { -// t.Error("Expected least connection host to be third host.") -// } -// pool[2].Conns = 100 -// h = lcPolicy.Select(pool, request) -// if h != pool[0] && h != pool[1] { -// t.Error("Expected least connection host to be first or second host.") -// } -// } - -// func TestCustomPolicy(t *testing.T) { -// pool := testPool() -// customPolicy := &customPolicy{} -// request, _ := http.NewRequest("GET", "/", nil) - -// h := customPolicy.Select(pool, request) -// if h != pool[0] { -// t.Error("Expected custom policy host to be the first host.") -// } -// } - -// func TestIPHashPolicy(t *testing.T) { -// pool := testPool() -// ipHash := &IPHash{} -// request, _ := http.NewRequest("GET", "/", nil) -// // We should be able to predict where every request is routed. -// request.RemoteAddr = "172.0.0.1:80" -// h := ipHash.Select(pool, request) -// if h != pool[1] { -// t.Error("Expected ip hash policy host to be the second host.") -// } -// request.RemoteAddr = "172.0.0.2:80" -// h = ipHash.Select(pool, request) -// if h != pool[1] { -// t.Error("Expected ip hash policy host to be the second host.") -// } -// request.RemoteAddr = "172.0.0.3:80" -// h = ipHash.Select(pool, request) -// if h != pool[2] { -// t.Error("Expected ip hash policy host to be the third host.") -// } -// request.RemoteAddr = "172.0.0.4:80" -// h = ipHash.Select(pool, request) -// if h != pool[1] { -// t.Error("Expected ip hash policy host to be the second host.") -// } - -// // we should get the same results without a port -// request.RemoteAddr = "172.0.0.1" -// h = ipHash.Select(pool, request) -// if h != pool[1] { -// t.Error("Expected ip hash policy host to be the second host.") -// } -// request.RemoteAddr = "172.0.0.2" -// h = ipHash.Select(pool, request) -// if h != pool[1] { -// t.Error("Expected ip hash policy host to be the second host.") -// } -// request.RemoteAddr = "172.0.0.3" -// h = ipHash.Select(pool, request) -// if h != pool[2] { -// t.Error("Expected ip hash policy host to be the third host.") -// } -// request.RemoteAddr = "172.0.0.4" -// h = ipHash.Select(pool, request) -// if h != pool[1] { -// t.Error("Expected ip hash policy host to be the second host.") -// } - -// // we should get a healthy host if the original host is unhealthy and a -// // healthy host is available -// request.RemoteAddr = "172.0.0.1" -// pool[1].Unhealthy = 1 -// h = ipHash.Select(pool, request) -// if h != pool[2] { -// t.Error("Expected ip hash policy host to be the third host.") -// } - -// request.RemoteAddr = "172.0.0.2" -// h = ipHash.Select(pool, request) -// if h != pool[2] { -// t.Error("Expected ip hash policy host to be the third host.") -// } -// pool[1].Unhealthy = 0 - -// request.RemoteAddr = "172.0.0.3" -// pool[2].Unhealthy = 1 -// h = ipHash.Select(pool, request) -// if h != pool[0] { -// t.Error("Expected ip hash policy host to be the first host.") -// } -// request.RemoteAddr = "172.0.0.4" -// h = ipHash.Select(pool, request) -// if h != pool[1] { -// t.Error("Expected ip hash policy host to be the second host.") -// } - -// // We should be able to resize the host pool and still be able to predict -// // where a request will be routed with the same IP's used above -// pool = []*UpstreamHost{ -// { -// Name: workableServer.URL, // this should resolve (healthcheck test) -// }, -// { -// Name: "http://localhost:99998", // this shouldn't -// }, -// } -// pool = HostPool(pool) -// request.RemoteAddr = "172.0.0.1:80" -// h = ipHash.Select(pool, request) -// if h != pool[0] { -// t.Error("Expected ip hash policy host to be the first host.") -// } -// request.RemoteAddr = "172.0.0.2:80" -// h = ipHash.Select(pool, request) -// if h != pool[1] { -// t.Error("Expected ip hash policy host to be the second host.") -// } -// request.RemoteAddr = "172.0.0.3:80" -// h = ipHash.Select(pool, request) -// if h != pool[0] { -// t.Error("Expected ip hash policy host to be the first host.") -// } -// request.RemoteAddr = "172.0.0.4:80" -// h = ipHash.Select(pool, request) -// if h != pool[1] { -// t.Error("Expected ip hash policy host to be the second host.") -// } - -// // We should get nil when there are no healthy hosts -// pool[0].Unhealthy = 1 -// pool[1].Unhealthy = 1 -// h = ipHash.Select(pool, request) -// if h != nil { -// t.Error("Expected ip hash policy host to be nil.") -// } -// } - -// func TestFirstPolicy(t *testing.T) { -// pool := testPool() -// firstPolicy := &First{} -// req := httptest.NewRequest(http.MethodGet, "/", nil) - -// h := firstPolicy.Select(pool, req) -// if h != pool[0] { -// t.Error("Expected first policy host to be the first host.") -// } - -// pool[0].Unhealthy = 1 -// h = firstPolicy.Select(pool, req) -// if h != pool[1] { -// t.Error("Expected first policy host to be the second host.") -// } -// } - -// func TestUriPolicy(t *testing.T) { -// pool := testPool() -// uriPolicy := &URIHash{} - -// request := httptest.NewRequest(http.MethodGet, "/test", nil) -// h := uriPolicy.Select(pool, request) -// if h != pool[0] { -// t.Error("Expected uri policy host to be the first host.") -// } - -// pool[0].Unhealthy = 1 -// h = uriPolicy.Select(pool, request) -// if h != pool[1] { -// t.Error("Expected uri policy host to be the first host.") -// } - -// request = httptest.NewRequest(http.MethodGet, "/test_2", nil) -// h = uriPolicy.Select(pool, request) -// if h != pool[1] { -// t.Error("Expected uri policy host to be the second host.") -// } - -// // We should be able to resize the host pool and still be able to predict -// // where a request will be routed with the same URI's used above -// pool = []*UpstreamHost{ -// { -// Name: workableServer.URL, // this should resolve (healthcheck test) -// }, -// { -// Name: "http://localhost:99998", // this shouldn't -// }, -// } - -// request = httptest.NewRequest(http.MethodGet, "/test", nil) -// h = uriPolicy.Select(pool, request) -// if h != pool[0] { -// t.Error("Expected uri policy host to be the first host.") -// } - -// pool[0].Unhealthy = 1 -// h = uriPolicy.Select(pool, request) -// if h != pool[1] { -// t.Error("Expected uri policy host to be the first host.") -// } - -// request = httptest.NewRequest(http.MethodGet, "/test_2", nil) -// h = uriPolicy.Select(pool, request) -// if h != pool[1] { -// t.Error("Expected uri policy host to be the second host.") -// } - -// pool[0].Unhealthy = 1 -// pool[1].Unhealthy = 1 -// h = uriPolicy.Select(pool, request) -// if h != nil { -// t.Error("Expected uri policy policy host to be nil.") -// } -// } - -// func TestHeaderPolicy(t *testing.T) { -// pool := testPool() -// tests := []struct { -// Name string -// Policy *Header -// RequestHeaderName string -// RequestHeaderValue string -// NilHost bool -// HostIndex int -// }{ -// {"empty config", &Header{""}, "", "", true, 0}, -// {"empty config+header+value", &Header{""}, "Affinity", "somevalue", true, 0}, -// {"empty config+header", &Header{""}, "Affinity", "", true, 0}, - -// {"no header(fallback to roundrobin)", &Header{"Affinity"}, "", "", false, 1}, -// {"no header(fallback to roundrobin)", &Header{"Affinity"}, "", "", false, 2}, -// {"no header(fallback to roundrobin)", &Header{"Affinity"}, "", "", false, 0}, - -// {"hash route to host", &Header{"Affinity"}, "Affinity", "somevalue", false, 1}, -// {"hash route to host", &Header{"Affinity"}, "Affinity", "somevalue2", false, 0}, -// {"hash route to host", &Header{"Affinity"}, "Affinity", "somevalue3", false, 2}, -// {"hash route with empty value", &Header{"Affinity"}, "Affinity", "", false, 1}, -// } - -// for idx, test := range tests { -// request, _ := http.NewRequest("GET", "/", nil) -// if test.RequestHeaderName != "" { -// request.Header.Add(test.RequestHeaderName, test.RequestHeaderValue) -// } - -// host := test.Policy.Select(pool, request) -// if test.NilHost && host != nil { -// t.Errorf("%d: Expected host to be nil", idx) -// } -// if !test.NilHost && host == nil { -// t.Errorf("%d: Did not expect host to be nil", idx) -// } -// if !test.NilHost && host != pool[test.HostIndex] { -// t.Errorf("%d: Expected Header policy to be host %d", idx, test.HostIndex) -// } -// } -// } +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func testPool() UpstreamPool { + return UpstreamPool{ + {Host: new(upstreamHost)}, + {Host: new(upstreamHost)}, + {Host: new(upstreamHost)}, + } +} + +func TestRoundRobinPolicy(t *testing.T) { + pool := testPool() + rrPolicy := new(RoundRobinSelection) + req, _ := http.NewRequest("GET", "/", nil) + + h := rrPolicy.Select(pool, req) + // First selected host is 1, because counter starts at 0 + // and increments before host is selected + if h != pool[1] { + t.Error("Expected first round robin host to be second host in the pool.") + } + h = rrPolicy.Select(pool, req) + if h != pool[2] { + t.Error("Expected second round robin host to be third host in the pool.") + } + h = rrPolicy.Select(pool, req) + if h != pool[0] { + t.Error("Expected third round robin host to be first host in the pool.") + } + // mark host as down + pool[1].SetHealthy(false) + h = rrPolicy.Select(pool, req) + if h != pool[2] { + t.Error("Expected to skip down host.") + } + // mark host as up + pool[1].SetHealthy(true) + + h = rrPolicy.Select(pool, req) + if h == pool[2] { + t.Error("Expected to balance evenly among healthy hosts") + } + // mark host as full + pool[1].CountRequest(1) + pool[1].MaxRequests = 1 + h = rrPolicy.Select(pool, req) + if h != pool[2] { + t.Error("Expected to skip full host.") + } +} + +func TestLeastConnPolicy(t *testing.T) { + pool := testPool() + lcPolicy := new(LeastConnSelection) + req, _ := http.NewRequest("GET", "/", nil) + + pool[0].CountRequest(10) + pool[1].CountRequest(10) + h := lcPolicy.Select(pool, req) + if h != pool[2] { + t.Error("Expected least connection host to be third host.") + } + pool[2].CountRequest(100) + h = lcPolicy.Select(pool, req) + if h != pool[0] && h != pool[1] { + t.Error("Expected least connection host to be first or second host.") + } +} + +func TestIPHashPolicy(t *testing.T) { + pool := testPool() + ipHash := new(IPHashSelection) + req, _ := http.NewRequest("GET", "/", nil) + + // We should be able to predict where every request is routed. + req.RemoteAddr = "172.0.0.1:80" + h := ipHash.Select(pool, req) + if h != pool[1] { + t.Error("Expected ip hash policy host to be the second host.") + } + req.RemoteAddr = "172.0.0.2:80" + h = ipHash.Select(pool, req) + if h != pool[1] { + t.Error("Expected ip hash policy host to be the second host.") + } + req.RemoteAddr = "172.0.0.3:80" + h = ipHash.Select(pool, req) + if h != pool[2] { + t.Error("Expected ip hash policy host to be the third host.") + } + req.RemoteAddr = "172.0.0.4:80" + h = ipHash.Select(pool, req) + if h != pool[1] { + t.Error("Expected ip hash policy host to be the second host.") + } + + // we should get the same results without a port + req.RemoteAddr = "172.0.0.1" + h = ipHash.Select(pool, req) + if h != pool[1] { + t.Error("Expected ip hash policy host to be the second host.") + } + req.RemoteAddr = "172.0.0.2" + h = ipHash.Select(pool, req) + if h != pool[1] { + t.Error("Expected ip hash policy host to be the second host.") + } + req.RemoteAddr = "172.0.0.3" + h = ipHash.Select(pool, req) + if h != pool[2] { + t.Error("Expected ip hash policy host to be the third host.") + } + req.RemoteAddr = "172.0.0.4" + h = ipHash.Select(pool, req) + if h != pool[1] { + t.Error("Expected ip hash policy host to be the second host.") + } + + // we should get a healthy host if the original host is unhealthy and a + // healthy host is available + req.RemoteAddr = "172.0.0.1" + pool[1].SetHealthy(false) + h = ipHash.Select(pool, req) + if h != pool[2] { + t.Error("Expected ip hash policy host to be the third host.") + } + + req.RemoteAddr = "172.0.0.2" + h = ipHash.Select(pool, req) + if h != pool[2] { + t.Error("Expected ip hash policy host to be the third host.") + } + pool[1].SetHealthy(true) + + req.RemoteAddr = "172.0.0.3" + pool[2].SetHealthy(false) + h = ipHash.Select(pool, req) + if h != pool[0] { + t.Error("Expected ip hash policy host to be the first host.") + } + req.RemoteAddr = "172.0.0.4" + h = ipHash.Select(pool, req) + if h != pool[1] { + t.Error("Expected ip hash policy host to be the second host.") + } + + // We should be able to resize the host pool and still be able to predict + // where a req will be routed with the same IP's used above + pool = UpstreamPool{ + {Host: new(upstreamHost)}, + {Host: new(upstreamHost)}, + } + req.RemoteAddr = "172.0.0.1:80" + h = ipHash.Select(pool, req) + if h != pool[0] { + t.Error("Expected ip hash policy host to be the first host.") + } + req.RemoteAddr = "172.0.0.2:80" + h = ipHash.Select(pool, req) + if h != pool[1] { + t.Error("Expected ip hash policy host to be the second host.") + } + req.RemoteAddr = "172.0.0.3:80" + h = ipHash.Select(pool, req) + if h != pool[0] { + t.Error("Expected ip hash policy host to be the first host.") + } + req.RemoteAddr = "172.0.0.4:80" + h = ipHash.Select(pool, req) + if h != pool[1] { + t.Error("Expected ip hash policy host to be the second host.") + } + + // We should get nil when there are no healthy hosts + pool[0].SetHealthy(false) + pool[1].SetHealthy(false) + h = ipHash.Select(pool, req) + if h != nil { + t.Error("Expected ip hash policy host to be nil.") + } +} + +func TestFirstPolicy(t *testing.T) { + pool := testPool() + firstPolicy := new(FirstSelection) + req := httptest.NewRequest(http.MethodGet, "/", nil) + + h := firstPolicy.Select(pool, req) + if h != pool[0] { + t.Error("Expected first policy host to be the first host.") + } + + pool[0].SetHealthy(false) + h = firstPolicy.Select(pool, req) + if h != pool[1] { + t.Error("Expected first policy host to be the second host.") + } +} + +func TestURIHashPolicy(t *testing.T) { + pool := testPool() + uriPolicy := new(URIHashSelection) + + request := httptest.NewRequest(http.MethodGet, "/test", nil) + h := uriPolicy.Select(pool, request) + if h != pool[0] { + t.Error("Expected uri policy host to be the first host.") + } + + pool[0].SetHealthy(false) + h = uriPolicy.Select(pool, request) + if h != pool[1] { + t.Error("Expected uri policy host to be the first host.") + } + + request = httptest.NewRequest(http.MethodGet, "/test_2", nil) + h = uriPolicy.Select(pool, request) + if h != pool[1] { + t.Error("Expected uri policy host to be the second host.") + } + + // We should be able to resize the host pool and still be able to predict + // where a request will be routed with the same URI's used above + pool = UpstreamPool{ + {Host: new(upstreamHost)}, + {Host: new(upstreamHost)}, + } + + request = httptest.NewRequest(http.MethodGet, "/test", nil) + h = uriPolicy.Select(pool, request) + if h != pool[0] { + t.Error("Expected uri policy host to be the first host.") + } + + pool[0].SetHealthy(false) + h = uriPolicy.Select(pool, request) + if h != pool[1] { + t.Error("Expected uri policy host to be the first host.") + } + + request = httptest.NewRequest(http.MethodGet, "/test_2", nil) + h = uriPolicy.Select(pool, request) + if h != pool[1] { + t.Error("Expected uri policy host to be the second host.") + } + + pool[0].SetHealthy(false) + pool[1].SetHealthy(false) + h = uriPolicy.Select(pool, request) + if h != nil { + t.Error("Expected uri policy policy host to be nil.") + } +} -- cgit v1.2.3