Skip to content

Commit b6fb61c

Browse files
authored
Merge pull request #1911 from sap-contributions/hbtest
Implement hash based routing test
2 parents eb66da3 + 65bd36d commit b6fb61c

2 files changed

Lines changed: 67 additions & 7 deletions

File tree

assets/dora/route_options_manifest.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ applications:
88
- route: ((leastconnhost)).((domain))
99
options:
1010
loadbalancing: least-connection
11+
- route: ((hashbasedroutinghost)).((domain))
12+
options:
13+
loadbalancing: hash
14+
hash_header: X-Hash-Header
1115
processes:
1216
- type: web
1317
instances: 2

routing/per_route_options.go

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package routing
22

33
import (
4+
"crypto/rand"
45
"fmt"
56
"path/filepath"
67
"regexp"
@@ -9,6 +10,7 @@ import (
910

1011
"github.com/cloudfoundry/cf-test-helpers/v2/cf"
1112
"github.com/cloudfoundry/cf-test-helpers/v2/helpers"
13+
"github.com/cloudfoundry/cf-test-helpers/v2/workflowhelpers"
1214

1315
. "github.com/cloudfoundry/cf-acceptance-tests/cats_suite_helpers"
1416
"github.com/cloudfoundry/cf-acceptance-tests/helpers/app_helpers"
@@ -26,25 +28,36 @@ var (
2628

2729
var _ = RoutingDescribe("Per-Route Options", func() {
2830
var (
29-
appName string
30-
appId string
31-
instanceIds [2]string
32-
leastConnHost string
33-
roundRobinHost string
31+
appName string
32+
appId string
33+
instanceIds [2]string
34+
leastConnHost string
35+
roundRobinHost string
36+
hashBasedRoutingHost string
3437
)
3538

39+
// Helper function to build URL for a given host
40+
buildUrl := func(host string) string {
41+
return fmt.Sprintf("%s%s.%s", Config.Protocol(), host, Config.GetAppsDomain())
42+
}
43+
3644
Context("when an app sets the loadbalancing algorithm", func() {
3745
BeforeEach(func() {
46+
workflowhelpers.AsUser(TestSetup.AdminUserContext(), TestSetup.ShortTimeout(), func() {
47+
Expect(cf.Cf("enable-feature-flag", "hash_based_routing").Wait()).To(Exit(0))
48+
})
3849
appName = random_name.CATSRandomName("APP")
3950
asset := assets.NewAssets()
4051
leastConnHost = random_name.CATSRandomName("dora-lc")
4152
roundRobinHost = random_name.CATSRandomName("dora-rr")
53+
hashBasedRoutingHost = random_name.CATSRandomName("dora-hash")
4254
Expect(cf.Cf("push",
4355
appName,
4456
"-b", Config.GetRubyBuildpackName(),
4557
"-m", DEFAULT_MEMORY_LIMIT,
4658
"-p", asset.Dora,
4759
"--var", fmt.Sprintf("domain=%s", Config.GetAppsDomain()),
60+
"--var", fmt.Sprintf("hashbasedroutinghost=%s", hashBasedRoutingHost),
4861
"--var", fmt.Sprintf("leastconnhost=%s", leastConnHost),
4962
"--var", fmt.Sprintf("roundrobinhost=%s", roundRobinHost),
5063
"-f", filepath.Join(asset.Dora, "route_options_manifest.yml"),
@@ -55,6 +68,7 @@ var _ = RoutingDescribe("Per-Route Options", func() {
5568
fmt.Fprintf(GinkgoWriter, "Waiting for app instance %d to start...\n", i)
5669
curl := helpers.Curl(Config, Config.Protocol()+leastConnHost+"."+Config.GetAppsDomain()+"/id", "-H", fmt.Sprintf("X-Cf-App-Instance: %s:%d", appId, i)).Wait()
5770
id := string(curl.Out.Contents())
71+
fmt.Fprintf(GinkgoWriter, "App instance %s\n", id)
5872
if appInstanceRegex.MatchString(id) {
5973
instanceIds[i] = id
6074
fmt.Fprintf(GinkgoWriter, "App instance %d has started. Instance ID: %s.\n", i, id)
@@ -70,11 +84,14 @@ var _ = RoutingDescribe("Per-Route Options", func() {
7084
AfterEach(func() {
7185
app_helpers.AppReport(appName)
7286
Expect(cf.Cf("delete", appName, "-f", "-r").Wait()).To(Exit(0))
87+
workflowhelpers.AsUser(TestSetup.AdminUserContext(), TestSetup.ShortTimeout(), func() {
88+
Expect(cf.Cf("disable-feature-flag", "hash_based_routing").Wait()).To(Exit(0))
89+
})
7390
})
7491

7592
Context("when it's set to round-robin", func() {
7693
It("distributes requests evenly", func() {
77-
doraUrl := fmt.Sprintf("%s%s.%s", Config.Protocol(), roundRobinHost, Config.GetAppsDomain())
94+
doraUrl := buildUrl(roundRobinHost)
7895
var wg sync.WaitGroup
7996
for i := 0; i < 10; i++ {
8097
wg.Add(1)
@@ -100,7 +117,7 @@ var _ = RoutingDescribe("Per-Route Options", func() {
100117

101118
Context("when it's set to least-connection", func() {
102119
It("always sends the request to the instance with less active connections", func() {
103-
doraUrl := fmt.Sprintf("%s%s.%s", Config.Protocol(), leastConnHost, Config.GetAppsDomain())
120+
doraUrl := buildUrl(leastConnHost)
104121
var wg sync.WaitGroup
105122
for i := 0; i < 10; i++ {
106123
wg.Add(1)
@@ -123,5 +140,44 @@ var _ = RoutingDescribe("Per-Route Options", func() {
123140
wg.Wait()
124141
})
125142
})
143+
Context("when it's set to hash", func() {
144+
Context("when the requests contain the same hash header", func() {
145+
It("routes requests to the same instance", func() {
146+
doraUrl := buildUrl(hashBasedRoutingHost)
147+
hashHeader := "X-Hash-Header: 1"
148+
149+
reqCount := [2]int{0, 0}
150+
for i := 0; i < 20; i++ {
151+
id := helpers.Curl(Config, fmt.Sprintf("%s/id", doraUrl), "-H", hashHeader).Wait().Out.Contents()
152+
reqCount[slices.Index(instanceIds[:], string(id))] += 1
153+
}
154+
155+
// All requests with the same hash should go to the same instance
156+
Expect(reqCount[0] == 20 || reqCount[1] == 20).To(BeTrue(), "All 20 requests should be routed to the same instance")
157+
})
158+
})
159+
Context("when the requests contain the different hash headers", func() {
160+
It("distributes requests evenly", func() {
161+
doraUrl := buildUrl(hashBasedRoutingHost)
162+
163+
reqCount := [2]int{0, 0}
164+
requestsToSend := 100
165+
for i := 0; i < requestsToSend; i++ {
166+
// Generate random hash header
167+
uuid := make([]byte, 16)
168+
rand.Read(uuid)
169+
randomHashValue := fmt.Sprintf("%x", uuid)
170+
171+
id := helpers.Curl(Config, fmt.Sprintf("%s/id", doraUrl), "-H", fmt.Sprintf("X-Hash-Header: %s", randomHashValue)).Wait().Out.Contents()
172+
reqCount[slices.Index(instanceIds[:], string(id))] += 1
173+
}
174+
175+
// allow for some wiggle-room
176+
tolerance := 10
177+
Expect(reqCount[0]).To(BeNumerically(">=", (requestsToSend/2)-tolerance), "Approximately half of requests should be routed to the first instance")
178+
Expect(reqCount[1]).To(BeNumerically(">=", (requestsToSend/2)-tolerance), "Approximately half of requests should be routed to the second instance")
179+
})
180+
})
181+
})
126182
})
127183
})

0 commit comments

Comments
 (0)