11package routing
22
33import (
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
2729var _ = 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