summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/caddyhttp/reverseproxy/selectionpolicies.go29
-rw-r--r--modules/caddyhttp/reverseproxy/selectionpolicies_test.go58
2 files changed, 46 insertions, 41 deletions
diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies.go b/modules/caddyhttp/reverseproxy/selectionpolicies.go
index 001f7f8..125a07f 100644
--- a/modules/caddyhttp/reverseproxy/selectionpolicies.go
+++ b/modules/caddyhttp/reverseproxy/selectionpolicies.go
@@ -514,21 +514,26 @@ func leastRequests(upstreams []*Upstream) *Upstream {
return best[weakrand.Intn(len(best))]
}
-// hostByHashing returns an available host
-// from pool based on a hashable string s.
+// hostByHashing returns an available host from pool based on a hashable string s.
func hostByHashing(pool []*Upstream, s string) *Upstream {
- poolLen := uint32(len(pool))
- if poolLen == 0 {
- return nil
- }
- index := hash(s) % poolLen
- for i := uint32(0); i < poolLen; i++ {
- upstream := pool[(index+i)%poolLen]
- if upstream.Available() {
- return upstream
+ // Highest Random Weight (HRW, or "Rendezvous") hashing,
+ // guarantees stability when the list of upstreams changes;
+ // see https://medium.com/i0exception/rendezvous-hashing-8c00e2fb58b0,
+ // https://randorithms.com/2020/12/26/rendezvous-hashing.html,
+ // and https://en.wikipedia.org/wiki/Rendezvous_hashing.
+ var highestHash uint32
+ var upstream *Upstream
+ for _, up := range pool {
+ if !up.Available() {
+ continue
+ }
+ h := hash(s + up.String()) // important to hash key and server together
+ if h > highestHash {
+ highestHash = h
+ upstream = up
}
}
- return nil
+ return upstream
}
// hash calculates a fast hash based on s.
diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies_test.go b/modules/caddyhttp/reverseproxy/selectionpolicies_test.go
index 7175f77..aa001e4 100644
--- a/modules/caddyhttp/reverseproxy/selectionpolicies_test.go
+++ b/modules/caddyhttp/reverseproxy/selectionpolicies_test.go
@@ -22,9 +22,9 @@ import (
func testPool() UpstreamPool {
return UpstreamPool{
- {Host: new(Host)},
- {Host: new(Host)},
- {Host: new(Host)},
+ {Host: new(Host), Dial: "0.0.0.1"},
+ {Host: new(Host), Dial: "0.0.0.2"},
+ {Host: new(Host), Dial: "0.0.0.3"},
}
}
@@ -95,13 +95,13 @@ func TestIPHashPolicy(t *testing.T) {
// We should be able to predict where every request is routed.
req.RemoteAddr = "172.0.0.1:80"
h := ipHash.Select(pool, req, nil)
- if h != pool[1] {
- t.Error("Expected ip hash policy host to be the second host.")
+ 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, nil)
- if h != pool[1] {
- t.Error("Expected ip hash policy host to be the second host.")
+ if h != pool[0] {
+ t.Error("Expected ip hash policy host to be the first host.")
}
req.RemoteAddr = "172.0.0.3:80"
h = ipHash.Select(pool, req, nil)
@@ -117,13 +117,13 @@ func TestIPHashPolicy(t *testing.T) {
// we should get the same results without a port
req.RemoteAddr = "172.0.0.1"
h = ipHash.Select(pool, req, nil)
- if h != pool[1] {
- t.Error("Expected ip hash policy host to be the second host.")
+ if h != pool[0] {
+ t.Error("Expected ip hash policy host to be the first host.")
}
req.RemoteAddr = "172.0.0.2"
h = ipHash.Select(pool, req, nil)
- if h != pool[1] {
- t.Error("Expected ip hash policy host to be the second host.")
+ if h != pool[0] {
+ t.Error("Expected ip hash policy host to be the first host.")
}
req.RemoteAddr = "172.0.0.3"
h = ipHash.Select(pool, req, nil)
@@ -138,7 +138,7 @@ func TestIPHashPolicy(t *testing.T) {
// we should get a healthy host if the original host is unhealthy and a
// healthy host is available
- req.RemoteAddr = "172.0.0.1"
+ req.RemoteAddr = "172.0.0.4"
pool[1].setHealthy(false)
h = ipHash.Select(pool, req, nil)
if h != pool[2] {
@@ -147,16 +147,16 @@ func TestIPHashPolicy(t *testing.T) {
req.RemoteAddr = "172.0.0.2"
h = ipHash.Select(pool, req, nil)
- if h != pool[2] {
- t.Error("Expected ip hash policy host to be the third host.")
+ if h != pool[0] {
+ t.Error("Expected ip hash policy host to be the first host.")
}
pool[1].setHealthy(true)
req.RemoteAddr = "172.0.0.3"
pool[2].setHealthy(false)
h = ipHash.Select(pool, req, nil)
- if h != pool[0] {
- t.Error("Expected ip hash policy host to be the first host.")
+ if h != pool[1] {
+ t.Error("Expected ip hash policy host to be the second host.")
}
req.RemoteAddr = "172.0.0.4"
h = ipHash.Select(pool, req, nil)
@@ -167,29 +167,29 @@ func TestIPHashPolicy(t *testing.T) {
// 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(Host)},
- {Host: new(Host)},
+ {Host: new(Host), Dial: "0.0.0.2"},
+ {Host: new(Host), Dial: "0.0.0.3"},
}
req.RemoteAddr = "172.0.0.1:80"
h = ipHash.Select(pool, req, nil)
- 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, nil)
if h != pool[1] {
t.Error("Expected ip hash policy host to be the second host.")
}
- req.RemoteAddr = "172.0.0.3:80"
+ req.RemoteAddr = "172.0.0.2:80"
h = ipHash.Select(pool, req, nil)
if h != pool[0] {
t.Error("Expected ip hash policy host to be the first host.")
}
- req.RemoteAddr = "172.0.0.4:80"
+ req.RemoteAddr = "172.0.0.3:80"
h = ipHash.Select(pool, req, nil)
if h != pool[1] {
t.Error("Expected ip hash policy host to be the second host.")
}
+ req.RemoteAddr = "172.0.0.4:80"
+ h = ipHash.Select(pool, req, nil)
+ if h != pool[0] {
+ t.Error("Expected ip hash policy host to be the first host.")
+ }
// We should get nil when there are no healthy hosts
pool[0].setHealthy(false)
@@ -252,14 +252,14 @@ func TestURIHashPolicy(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "/test", nil)
h := uriPolicy.Select(pool, request, nil)
- if h != pool[0] {
- t.Error("Expected uri policy host to be the first host.")
+ if h != pool[2] {
+ t.Error("Expected uri policy host to be the third host.")
}
- pool[0].setHealthy(false)
+ pool[2].setHealthy(false)
h = uriPolicy.Select(pool, request, nil)
if h != pool[1] {
- t.Error("Expected uri policy host to be the first host.")
+ t.Error("Expected uri policy host to be the second host.")
}
request = httptest.NewRequest(http.MethodGet, "/test_2", nil)