From f50830e8b6df611d94d2034a904ddcad3167cf37 Mon Sep 17 00:00:00 2001 From: Alexander Nicke Date: Fri, 29 Aug 2025 12:13:37 +0200 Subject: [PATCH 1/5] feat(gorouter): Implement Hash-based routing Registry Message fields See: https://github.com/cloudfoundry/community/blob/main/toc/rfc/rfc-0042-hash-based-routing.md See: https://github.com/cloudfoundry/routing-release/issues/505 Co-authored-by: Tamara Boehm --- .../gorouter/config/config.go | 12 +- .../gorouter/config/config_test.go | 36 ++- .../gorouter/mbus/subscriber.go | 6 +- .../gorouter/mbus/subscriber_test.go | 206 +++++++++--- .../gorouter/route/pool.go | 40 ++- .../gorouter/route/pool_test.go | 300 ++++++++++++------ 6 files changed, 436 insertions(+), 164 deletions(-) diff --git a/src/code.cloudfoundry.org/gorouter/config/config.go b/src/code.cloudfoundry.org/gorouter/config/config.go index ad2893eec..b24ba84c4 100644 --- a/src/code.cloudfoundry.org/gorouter/config/config.go +++ b/src/code.cloudfoundry.org/gorouter/config/config.go @@ -24,6 +24,7 @@ import ( const ( LOAD_BALANCE_RR string = "round-robin" LOAD_BALANCE_LC string = "least-connection" + LOAD_BALANCE_HB string = "hash" AZ_PREF_NONE string = "none" AZ_PREF_LOCAL string = "locally-optimistic" SHARD_ALL string = "all" @@ -38,7 +39,8 @@ const ( ) var ( - LoadBalancingStrategies = []string{LOAD_BALANCE_RR, LOAD_BALANCE_LC} + GlobalLoadBalancingAlgorithms = []string{LOAD_BALANCE_RR, LOAD_BALANCE_LC} // These strategies can be set globally via config + LoadBalancingStrategies = append(GlobalLoadBalancingAlgorithms, LOAD_BALANCE_HB) AZPreferences = []string{AZ_PREF_NONE, AZ_PREF_LOCAL} AllowedShardingModes = []string{SHARD_ALL, SHARD_SEGMENTS, SHARD_SHARED_AND_SEGMENTS} AllowedForwardedClientCertModes = []string{ALWAYS_FORWARD, FORWARD, SANITIZE_SET} @@ -595,6 +597,10 @@ func DefaultConfig() (*Config, error) { return &c, nil } +func IsGlobalLoadBalancingAlgorithmValid(lbAlgo string) bool { + return slices.Contains(GlobalLoadBalancingAlgorithms, lbAlgo) +} + func IsLoadBalancingAlgorithmValid(lbAlgo string) bool { return slices.Contains(LoadBalancingStrategies, lbAlgo) } @@ -755,8 +761,8 @@ func (c *Config) Process() error { c.RouteServiceEnabled = true } - if !IsLoadBalancingAlgorithmValid(c.LoadBalance) { - return fmt.Errorf("Invalid load balancing algorithm %s. Allowed values are %s", c.LoadBalance, LoadBalancingStrategies) + if !IsGlobalLoadBalancingAlgorithmValid(c.LoadBalance) { + return fmt.Errorf("Invalid global load balancing algorithm %s. Allowed values are %s", c.LoadBalance, GlobalLoadBalancingAlgorithms) } validAZPref := false diff --git a/src/code.cloudfoundry.org/gorouter/config/config_test.go b/src/code.cloudfoundry.org/gorouter/config/config_test.go index 30ed6a503..8b893f85b 100644 --- a/src/code.cloudfoundry.org/gorouter/config/config_test.go +++ b/src/code.cloudfoundry.org/gorouter/config/config_test.go @@ -47,13 +47,31 @@ zone: meow-zone }) }) + Context("Global Load Balancing Algorithm Validator", func() { + It("Returns false if the value is not in the list of configured global load balancing strategies", func() { + Expect(IsGlobalLoadBalancingAlgorithmValid("wrong.algo")).To(BeFalse()) + }) + + It("Returns true if the value is in the list of configured global load balancing strategies", func() { + Expect(IsGlobalLoadBalancingAlgorithmValid(LOAD_BALANCE_RR)).To(BeTrue()) + }) + + It("Returns false for hash load balancing algorithm as global setting", func() { + Expect(IsGlobalLoadBalancingAlgorithmValid(LOAD_BALANCE_HB)).To(BeFalse()) + }) + }) + Context("Load Balancing Algorithm Validator", func() { It("Returns false if the value is not in the list of configured load balancing strategies", func() { - Expect(IsLoadBalancingAlgorithmValid("wrong.algo")).To(Equal(false)) + Expect(IsLoadBalancingAlgorithmValid("wrong.algo")).To(BeFalse()) }) It("Returns true if the value is in the list of configured load balancing strategies", func() { - Expect(IsLoadBalancingAlgorithmValid(LOAD_BALANCE_RR)).To(Equal(true)) + Expect(IsLoadBalancingAlgorithmValid(LOAD_BALANCE_RR)).To(BeTrue()) + }) + + It("Returns true for hash load balancing algorithm", func() { + Expect(IsLoadBalancingAlgorithmValid(LOAD_BALANCE_HB)).To(BeTrue()) }) }) @@ -73,12 +91,22 @@ balancing_algorithm: least-connection Expect(cfg.LoadBalance).To(Equal(LOAD_BALANCE_LC)) }) + It("can NOT override the load balance strategy to hash (not available as global default)", func() { + cfg, err := DefaultConfig() + Expect(err).ToNot(HaveOccurred()) + var b = []byte(` +balancing_algorithm: hash +`) + cfg.Initialize(b) + Expect(cfg.Process()).To(MatchError("Invalid global load balancing algorithm hash. Allowed values are [round-robin least-connection]")) + }) + It("does not allow an invalid load balance strategy", func() { cfg, err := DefaultConfig() Expect(err).ToNot(HaveOccurred()) cfgForSnippet.LoadBalance = "foo-bar" cfg.Initialize(createYMLSnippet(cfgForSnippet)) - Expect(cfg.Process()).To(MatchError("Invalid load balancing algorithm foo-bar. Allowed values are [round-robin least-connection]")) + Expect(cfg.Process()).To(MatchError("Invalid global load balancing algorithm foo-bar. Allowed values are [round-robin least-connection]")) }) }) @@ -1805,7 +1833,7 @@ load_balancer_healthy_threshold: 10s }) It("setting hop_by_hop_headers_to_filter succeeds", func() { err := config.Initialize(createYMLSnippet(cfgForSnippet)) - Expect(err).NotTo(HaveOccurred()) + Expect(err).ToNot(HaveOccurred()) Expect(config.Process()).To(Succeed()) Expect(config.HopByHopHeadersToFilter).To(Equal([]string{"X-ME", "X-Foo"})) }) diff --git a/src/code.cloudfoundry.org/gorouter/mbus/subscriber.go b/src/code.cloudfoundry.org/gorouter/mbus/subscriber.go index 22ff66511..711662bff 100644 --- a/src/code.cloudfoundry.org/gorouter/mbus/subscriber.go +++ b/src/code.cloudfoundry.org/gorouter/mbus/subscriber.go @@ -41,7 +41,9 @@ type RegistryMessage struct { } type RegistryMessageOpts struct { - LoadBalancingAlgorithm string `json:"loadbalancing"` + LoadBalancingAlgorithm string `json:"loadbalancing"` + HashHeader string `json:"hash_header"` + HashBalance float64 `json:"hash_balance"` } func (rm *RegistryMessage) makeEndpoint(http2Enabled bool) (*route.Endpoint, error) { @@ -76,6 +78,8 @@ func (rm *RegistryMessage) makeEndpoint(http2Enabled bool) (*route.Endpoint, err UseTLS: useTLS, UpdatedAt: updatedAt, LoadBalancingAlgorithm: rm.Options.LoadBalancingAlgorithm, + HashHeader: rm.Options.HashHeader, + HashBalance: rm.Options.HashBalance, }), nil } diff --git a/src/code.cloudfoundry.org/gorouter/mbus/subscriber_test.go b/src/code.cloudfoundry.org/gorouter/mbus/subscriber_test.go index 36ada05f0..8f42cf9fd 100644 --- a/src/code.cloudfoundry.org/gorouter/mbus/subscriber_test.go +++ b/src/code.cloudfoundry.org/gorouter/mbus/subscriber_test.go @@ -476,6 +476,82 @@ var _ = Describe("Subscriber", func() { Expect(originalEndpoint).To(Equal(expectedEndpoint)) }) + Context("when HTTP/2 is disabled and the protocol is http2", func() { + BeforeEach(func() { + cfg.EnableHTTP2 = false + }) + It("constructs the endpoint with protocol 'http1'", func() { + msg := mbus.RegistryMessage{ + Host: "host", + App: "app", + Protocol: "http2", + Uris: []route.Uri{"test.example.com"}, + } + + data, err := json.Marshal(msg) + Expect(err).NotTo(HaveOccurred()) + + err = natsClient.Publish("router.register", data) + Expect(err).ToNot(HaveOccurred()) + + Eventually(registry.RegisterCallCount).Should(Equal(1)) + _, originalEndpoint := registry.RegisterArgsForCall(0) + expectedEndpoint := route.NewEndpoint(&route.EndpointOpts{ + Host: "host", + AppId: "app", + Protocol: "http1", + }) + + Expect(originalEndpoint).To(Equal(expectedEndpoint)) + }) + }) + }) + + Context("when the message contains a tls port for route", func() { + BeforeEach(func() { + sub = mbus.NewSubscriber(natsClient, registry, cfg, reconnected, logger.Logger) + process = ifrit.Invoke(sub) + Eventually(process.Ready()).Should(BeClosed()) + }) + It("endpoint is constructed with tls port instead of http", func() { + msg := mbus.RegistryMessage{ + Host: "host", + App: "app", + TLSPort: 1999, + ServerCertDomainSAN: "san", + PrivateInstanceID: "id", + PrivateInstanceIndex: "index", + StaleThresholdInSeconds: 120, + Uris: []route.Uri{"test.example.com"}, + Tags: map[string]string{"key": "value"}, + } + + data, err := json.Marshal(msg) + Expect(err).NotTo(HaveOccurred()) + + err = natsClient.Publish("router.register", data) + Expect(err).ToNot(HaveOccurred()) + + Eventually(registry.RegisterCallCount).Should(Equal(1)) + _, originalEndpoint := registry.RegisterArgsForCall(0) + expectedEndpoint := route.NewEndpoint(&route.EndpointOpts{ + Host: "host", + AppId: "app", + Port: 1999, + Protocol: "http1", + UseTLS: true, + ServerCertDomainSAN: "san", + PrivateInstanceId: "id", + PrivateInstanceIndex: "index", + StaleThresholdInSeconds: 120, + Tags: map[string]string{"key": "value"}, + }) + + Expect(originalEndpoint).To(Equal(expectedEndpoint)) + }) + }) + + Context("when the message contains per-route options", func() { Context("when the message contains load balancing algorithm option", func() { JustBeforeEach(func() { sub = mbus.NewSubscriber(natsClient, registry, cfg, reconnected, logger.Logger) @@ -498,7 +574,7 @@ var _ = Describe("Subscriber", func() { err = natsClient.Publish("router.register", data) Expect(err).ToNot(HaveOccurred()) - Eventually(registry.RegisterCallCount).Should(Equal(2)) + Eventually(registry.RegisterCallCount).Should(Equal(1)) _, originalEndpoint := registry.RegisterArgsForCall(0) expectedEndpoint := route.NewEndpoint(&route.EndpointOpts{ Host: "host", @@ -531,7 +607,7 @@ var _ = Describe("Subscriber", func() { err = natsClient.Publish("router.register", data) Expect(err).ToNot(HaveOccurred()) - Eventually(registry.RegisterCallCount).Should(Equal(2)) + Eventually(registry.RegisterCallCount).Should(Equal(1)) _, originalEndpoint := registry.RegisterArgsForCall(0) expectedEndpoint := route.NewEndpoint(&route.EndpointOpts{ Host: "host", @@ -544,18 +620,26 @@ var _ = Describe("Subscriber", func() { }) - Context("when HTTP/2 is disabled and the protocol is http2", func() { - BeforeEach(func() { - cfg.EnableHTTP2 = false + Context("when the message contains hash-based load balancing options", func() { + JustBeforeEach(func() { + sub = mbus.NewSubscriber(natsClient, registry, cfg, reconnected, logger.Logger) + process = ifrit.Invoke(sub) + Eventually(process.Ready()).Should(BeClosed()) }) - It("constructs the endpoint with protocol 'http1'", func() { - msg := mbus.RegistryMessage{ + It("constructs an endpoint with the correct load balancing algorithm (HashBalance is set)", func() { + var expectedLBAlgo = "hash" + + var msg = mbus.RegistryMessage{ Host: "host", App: "app", Protocol: "http2", Uris: []route.Uri{"test.example.com"}, + Options: mbus.RegistryMessageOpts{ + LoadBalancingAlgorithm: expectedLBAlgo, + HashHeader: "X-Header", + HashBalance: 1.5, + }, } - data, err := json.Marshal(msg) Expect(err).NotTo(HaveOccurred()) @@ -565,57 +649,80 @@ var _ = Describe("Subscriber", func() { Eventually(registry.RegisterCallCount).Should(Equal(1)) _, originalEndpoint := registry.RegisterArgsForCall(0) expectedEndpoint := route.NewEndpoint(&route.EndpointOpts{ - Host: "host", - AppId: "app", - Protocol: "http1", + Host: "host", + AppId: "app", + Protocol: "http2", + LoadBalancingAlgorithm: expectedLBAlgo, + HashHeader: "X-Header", + HashBalance: 1.5, }) Expect(originalEndpoint).To(Equal(expectedEndpoint)) }) - }) - }) - Context("when the message contains a tls port for route", func() { - BeforeEach(func() { - sub = mbus.NewSubscriber(natsClient, registry, cfg, reconnected, logger.Logger) - process = ifrit.Invoke(sub) - Eventually(process.Ready()).Should(BeClosed()) - }) - It("endpoint is constructed with tls port instead of http", func() { - msg := mbus.RegistryMessage{ - Host: "host", - App: "app", - TLSPort: 1999, - ServerCertDomainSAN: "san", - PrivateInstanceID: "id", - PrivateInstanceIndex: "index", - StaleThresholdInSeconds: 120, - Uris: []route.Uri{"test.example.com"}, - Tags: map[string]string{"key": "value"}, - } + It("constructs an endpoint with the correct load balancing algorithm (HashBalance is not set)", func() { + var expectedLBAlgo = "hash" - data, err := json.Marshal(msg) - Expect(err).NotTo(HaveOccurred()) + var msg = mbus.RegistryMessage{ + Host: "host", + App: "app", + Protocol: "http2", + Uris: []route.Uri{"test.example.com"}, + Options: mbus.RegistryMessageOpts{ + LoadBalancingAlgorithm: expectedLBAlgo, + HashHeader: "X-Header", + }, + } + data, err := json.Marshal(msg) + Expect(err).NotTo(HaveOccurred()) - err = natsClient.Publish("router.register", data) - Expect(err).ToNot(HaveOccurred()) + err = natsClient.Publish("router.register", data) + Expect(err).ToNot(HaveOccurred()) - Eventually(registry.RegisterCallCount).Should(Equal(1)) - _, originalEndpoint := registry.RegisterArgsForCall(0) - expectedEndpoint := route.NewEndpoint(&route.EndpointOpts{ - Host: "host", - AppId: "app", - Port: 1999, - Protocol: "http1", - UseTLS: true, - ServerCertDomainSAN: "san", - PrivateInstanceId: "id", - PrivateInstanceIndex: "index", - StaleThresholdInSeconds: 120, - Tags: map[string]string{"key": "value"}, + Eventually(registry.RegisterCallCount).Should(Equal(1)) + _, originalEndpoint := registry.RegisterArgsForCall(0) + expectedEndpoint := route.NewEndpoint(&route.EndpointOpts{ + Host: "host", + AppId: "app", + Protocol: "http2", + LoadBalancingAlgorithm: expectedLBAlgo, + HashHeader: "X-Header", + HashBalance: 0.0, + }) + Expect(expectedEndpoint.HashRoutingProperties).ToNot(BeNil()) + Expect(originalEndpoint).To(Equal(expectedEndpoint)) }) - Expect(originalEndpoint).To(Equal(expectedEndpoint)) + It("ignores HashRoutingProperties if load balancing algorithm is not 'hash'", func() { + var expectedLBAlgo = "round-robin" + + var msg = mbus.RegistryMessage{ + Host: "host", + App: "app", + Protocol: "http2", + Uris: []route.Uri{"test.example.com"}, + Options: mbus.RegistryMessageOpts{ + LoadBalancingAlgorithm: expectedLBAlgo, + HashHeader: "X-Header", + }, + } + data, err := json.Marshal(msg) + Expect(err).NotTo(HaveOccurred()) + + err = natsClient.Publish("router.register", data) + Expect(err).ToNot(HaveOccurred()) + + Eventually(registry.RegisterCallCount).Should(Equal(1)) + _, originalEndpoint := registry.RegisterArgsForCall(0) + expectedEndpoint := route.NewEndpoint(&route.EndpointOpts{ + Host: "host", + AppId: "app", + Protocol: "http2", + LoadBalancingAlgorithm: expectedLBAlgo, + }) + Expect(expectedEndpoint.HashRoutingProperties).To(BeNil()) + Expect(originalEndpoint).To(Equal(expectedEndpoint)) + }) }) }) @@ -839,5 +946,4 @@ var _ = Describe("Subscriber", func() { } }) }) - }) diff --git a/src/code.cloudfoundry.org/gorouter/route/pool.go b/src/code.cloudfoundry.org/gorouter/route/pool.go index 0d9fd8f2d..a549aca58 100644 --- a/src/code.cloudfoundry.org/gorouter/route/pool.go +++ b/src/code.cloudfoundry.org/gorouter/route/pool.go @@ -74,6 +74,21 @@ type ProxyRoundTripper interface { CancelRequest(*http.Request) } +type HashRoutingProperties struct { + Header string + BalanceFactor float64 +} + +func (hrp *HashRoutingProperties) Equal(hrp2 *HashRoutingProperties) bool { + if hrp == nil && hrp2 == nil { + return true + } + if hrp == nil || hrp2 == nil { + return false + } + return hrp.Header == hrp2.Header && hrp.BalanceFactor == hrp2.BalanceFactor +} + type Endpoint struct { ApplicationId string AvailabilityZone string @@ -94,6 +109,7 @@ type Endpoint struct { UpdatedAt time.Time RoundTripperInit sync.Once LoadBalancingAlgorithm string + HashRoutingProperties *HashRoutingProperties } func (e *Endpoint) RoundTripper() ProxyRoundTripper { @@ -123,6 +139,7 @@ func (e *Endpoint) Equal(e2 *Endpoint) bool { if e2 == nil { return false } + return e.ApplicationId == e2.ApplicationId && e.addr == e2.addr && e.Protocol == e2.Protocol && @@ -136,6 +153,7 @@ func (e *Endpoint) Equal(e2 *Endpoint) bool { e.useTls == e2.useTls && e.UpdatedAt.Equal(e2.UpdatedAt) && e.LoadBalancingAlgorithm == e2.LoadBalancingAlgorithm && + e.HashRoutingProperties.Equal(e2.HashRoutingProperties) && maps.Equal(e.Tags, e2.Tags) } @@ -200,10 +218,12 @@ type EndpointOpts struct { UseTLS bool UpdatedAt time.Time LoadBalancingAlgorithm string + HashHeader string + HashBalance float64 } func NewEndpoint(opts *EndpointOpts) *Endpoint { - return &Endpoint{ + endpoint := &Endpoint{ ApplicationId: opts.AppId, AvailabilityZone: opts.AvailabilityZone, addr: fmt.Sprintf("%s:%d", opts.Host, opts.Port), @@ -221,6 +241,16 @@ func NewEndpoint(opts *EndpointOpts) *Endpoint { UpdatedAt: opts.UpdatedAt, LoadBalancingAlgorithm: opts.LoadBalancingAlgorithm, } + + // TODO: Log debug? warning when HashHeader is set but LoadBalancingAlgorithm is not LOAD_BALANCE_HB? + if opts.LoadBalancingAlgorithm == config.LOAD_BALANCE_HB && opts.HashHeader != "" { // BalanceFactor is optional + endpoint.HashRoutingProperties = &HashRoutingProperties{ + Header: opts.HashHeader, + BalanceFactor: opts.HashBalance, + } + } + + return endpoint } func (e *Endpoint) IsTLS() bool { @@ -587,6 +617,8 @@ func (e *Endpoint) MarshalJSON() ([]byte, error) { PrivateInstanceId string `json:"private_instance_id,omitempty"` ServerCertDomainSAN string `json:"server_cert_domain_san,omitempty"` LoadBalancingAlgorithm string `json:"load_balancing_algorithm,omitempty"` + HashHeader string `json:"hash_header,omitempty"` + HashBalance float64 `json:"hash_balance,omitempty"` } jsonObj.Address = e.addr @@ -600,6 +632,12 @@ func (e *Endpoint) MarshalJSON() ([]byte, error) { jsonObj.PrivateInstanceId = e.PrivateInstanceId jsonObj.ServerCertDomainSAN = e.ServerCertDomainSAN jsonObj.LoadBalancingAlgorithm = e.LoadBalancingAlgorithm + + if e.HashRoutingProperties != nil { + jsonObj.HashHeader = e.HashRoutingProperties.Header + jsonObj.HashBalance = e.HashRoutingProperties.BalanceFactor + } + return json.Marshal(jsonObj) } diff --git a/src/code.cloudfoundry.org/gorouter/route/pool_test.go b/src/code.cloudfoundry.org/gorouter/route/pool_test.go index 7d0655141..2e2b1f408 100644 --- a/src/code.cloudfoundry.org/gorouter/route/pool_test.go +++ b/src/code.cloudfoundry.org/gorouter/route/pool_test.go @@ -39,6 +39,70 @@ var _ = Describe("Endpoint", func() { }) }) }) + + Context("Hash-based Load Balancing", func() { + Context("when endpoint is created with hash options", func() { + var endpoint *route.Endpoint + BeforeEach(func() { + endpoint = route.NewEndpoint(&route.EndpointOpts{ + AppId: "test-app", + Host: "localhost", + Port: 8080, + LoadBalancingAlgorithm: "hash", + HashHeader: "X-Header", + HashBalance: 1.15, + }) + }) + It("should have the correct hash header", func() { + Expect(endpoint.HashRoutingProperties.Header).To(Equal("X-Header")) + }) + It("should have the correct hash balance", func() { + Expect(endpoint.HashRoutingProperties.BalanceFactor).To(Equal(1.15)) + }) + It("should have the correct load balancing algorithm", func() { + Expect(endpoint.LoadBalancingAlgorithm).To(Equal("hash")) + }) + }) + + Context("when comparing endpoints with hash options", func() { + var endpoint1, endpoint2, endpoint3 *route.Endpoint + BeforeEach(func() { + opts1 := &route.EndpointOpts{ + AppId: "test-app", + Host: "localhost", + Port: 8080, + LoadBalancingAlgorithm: "hash", + HashHeader: "X-Header", + HashBalance: 1.15, + } + opts2 := &route.EndpointOpts{ + AppId: "test-app", + Host: "localhost", + Port: 8080, + LoadBalancingAlgorithm: "hash", + HashHeader: "X-Header", + HashBalance: 1.15, + } + opts3 := &route.EndpointOpts{ + AppId: "test-app", + Host: "localhost", + Port: 8080, + LoadBalancingAlgorithm: "hash", + HashHeader: "X-Header", + HashBalance: 2.25, + } + endpoint1 = route.NewEndpoint(opts1) + endpoint2 = route.NewEndpoint(opts2) + endpoint3 = route.NewEndpoint(opts3) + }) + It("should return true when endpoints have same hash options", func() { + Expect(endpoint1.Equal(endpoint2)).To(BeTrue()) + }) + It("should return false when endpoints have different hash options", func() { + Expect(endpoint1.Equal(endpoint3)).To(BeFalse()) + }) + }) + }) }) var _ = Describe("EndpointPool", func() { @@ -232,134 +296,158 @@ var _ = Describe("EndpointPool", func() { }) }) - Context("Load Balancing Algorithm of a pool", func() { - It("has a value specified in the pool options", func() { - poolWithLBAlgo := route.NewPool(&route.PoolOpts{ - Logger: logger.Logger, - LoadBalancingAlgorithm: config.LOAD_BALANCE_RR, + Context("Customizable Per Route Load Balancing", func() { + + Context("Load Balancing Algorithm of a pool", func() { + It("has a value specified in the pool options", func() { + poolWithLBAlgo := route.NewPool(&route.PoolOpts{ + Logger: logger.Logger, + LoadBalancingAlgorithm: config.LOAD_BALANCE_RR, + }) + Expect(poolWithLBAlgo.LoadBalancingAlgorithm).To(Equal(config.LOAD_BALANCE_RR)) }) - Expect(poolWithLBAlgo.LoadBalancingAlgorithm).To(Equal(config.LOAD_BALANCE_RR)) - }) - It("has an invalid value specified in the pool options", func() { - poolWithLBAlgo2 := route.NewPool(&route.PoolOpts{ - Logger: logger.Logger, - LoadBalancingAlgorithm: "wrong-lb-algo", + It("has an invalid value specified in the pool options", func() { + poolWithLBAlgo2 := route.NewPool(&route.PoolOpts{ + Logger: logger.Logger, + LoadBalancingAlgorithm: "wrong-lb-algo", + }) + iterator := poolWithLBAlgo2.Endpoints(logger.Logger, "", false, "none", "zone") + Expect(iterator).To(BeAssignableToTypeOf(&route.RoundRobin{})) + Eventually(logger).Should(gbytes.Say(`invalid-pool-load-balancing-algorithm`)) }) - iterator := poolWithLBAlgo2.Endpoints(logger.Logger, "", false, "none", "zone") - Expect(iterator).To(BeAssignableToTypeOf(&route.RoundRobin{})) - Eventually(logger).Should(gbytes.Say(`invalid-pool-load-balancing-algorithm`)) - }) - It("is correctly propagated to the newly created endpoints LOAD_BALANCE_LC ", func() { - poolWithLBAlgoLC := route.NewPool(&route.PoolOpts{ - Logger: logger.Logger, - LoadBalancingAlgorithm: config.LOAD_BALANCE_LC, + It("is correctly propagated to the newly created endpoints LOAD_BALANCE_LC ", func() { + poolWithLBAlgoLC := route.NewPool(&route.PoolOpts{ + Logger: logger.Logger, + LoadBalancingAlgorithm: config.LOAD_BALANCE_LC, + }) + iterator := poolWithLBAlgoLC.Endpoints(logger.Logger, "", false, "none", "az") + Expect(iterator).To(BeAssignableToTypeOf(&route.LeastConnection{})) + Eventually(logger).Should(gbytes.Say(`endpoint-iterator-with-least-connection-lb-algo`)) }) - iterator := poolWithLBAlgoLC.Endpoints(logger.Logger, "", false, "none", "az") - Expect(iterator).To(BeAssignableToTypeOf(&route.LeastConnection{})) - Eventually(logger).Should(gbytes.Say(`endpoint-iterator-with-least-connection-lb-algo`)) - }) - It("is correctly propagated to the newly created endpoints LOAD_BALANCE_RR ", func() { - poolWithLBAlgoLC := route.NewPool(&route.PoolOpts{ - Logger: logger.Logger, - LoadBalancingAlgorithm: config.LOAD_BALANCE_RR, + It("is correctly propagated to the newly created endpoints LOAD_BALANCE_RR ", func() { + poolWithLBAlgoLC := route.NewPool(&route.PoolOpts{ + Logger: logger.Logger, + LoadBalancingAlgorithm: config.LOAD_BALANCE_RR, + }) + iterator := poolWithLBAlgoLC.Endpoints(logger.Logger, "", false, "none", "az") + Expect(iterator).To(BeAssignableToTypeOf(&route.RoundRobin{})) + Eventually(logger).Should(gbytes.Say(`endpoint-iterator-with-round-robin-lb-algo`)) }) - iterator := poolWithLBAlgoLC.Endpoints(logger.Logger, "", false, "none", "az") - Expect(iterator).To(BeAssignableToTypeOf(&route.RoundRobin{})) - Eventually(logger).Should(gbytes.Say(`endpoint-iterator-with-round-robin-lb-algo`)) + }) - }) - Context("Load balancing algorithm of a newly added endpoint", func() { + Context("Load balancing algorithm of a newly added endpoint", func() { - It("is valid and will overwrite the load balancing algorithm of a pool", func() { - pool := route.NewPool(&route.PoolOpts{ - Logger: logger.Logger, - LoadBalancingAlgorithm: config.LOAD_BALANCE_RR, - }) - expectedLBAlgo := config.LOAD_BALANCE_LC - endpoint := route.NewEndpoint(&route.EndpointOpts{ - Host: "host-1", Port: 1234, - RouteServiceUrl: "url", - LoadBalancingAlgorithm: expectedLBAlgo, + It("is valid and will overwrite the load balancing algorithm of a pool", func() { + pool := route.NewPool(&route.PoolOpts{ + Logger: logger.Logger, + LoadBalancingAlgorithm: config.LOAD_BALANCE_RR, + }) + expectedLBAlgo := config.LOAD_BALANCE_LC + endpoint := route.NewEndpoint(&route.EndpointOpts{ + Host: "host-1", Port: 1234, + RouteServiceUrl: "url", + LoadBalancingAlgorithm: expectedLBAlgo, + }) + pool.Put(endpoint) + Expect(pool.LoadBalancingAlgorithm).To(Equal(expectedLBAlgo)) + Eventually(logger).Should(gbytes.Say(`setting-pool-load-balancing-algorithm-to-that-of-an-endpoint`)) }) - pool.Put(endpoint) - Expect(pool.LoadBalancingAlgorithm).To(Equal(expectedLBAlgo)) - Eventually(logger).Should(gbytes.Say(`setting-pool-load-balancing-algorithm-to-that-of-an-endpoint`)) - }) - It("is an empty string and the load balancing algorithm of a pool is kept", func() { - expectedLBAlgo := config.LOAD_BALANCE_RR - pool := route.NewPool(&route.PoolOpts{ - Logger: logger.Logger, - LoadBalancingAlgorithm: expectedLBAlgo, - }) - endpoint := route.NewEndpoint(&route.EndpointOpts{ - Host: "host-1", Port: 1234, - RouteServiceUrl: "url", + It("is an empty string and the load balancing algorithm of a pool is kept", func() { + expectedLBAlgo := config.LOAD_BALANCE_RR + pool := route.NewPool(&route.PoolOpts{ + Logger: logger.Logger, + LoadBalancingAlgorithm: expectedLBAlgo, + }) + endpoint := route.NewEndpoint(&route.EndpointOpts{ + Host: "host-1", Port: 1234, + RouteServiceUrl: "url", + }) + pool.Put(endpoint) + Expect(pool.LoadBalancingAlgorithm).To(Equal(expectedLBAlgo)) }) - pool.Put(endpoint) - Expect(pool.LoadBalancingAlgorithm).To(Equal(expectedLBAlgo)) - }) - It("is not specified in the endpoint options and the load balancing algorithm of a pool is kept", func() { - expectedLBAlgo := config.LOAD_BALANCE_RR - pool := route.NewPool(&route.PoolOpts{ - Logger: logger.Logger, - LoadBalancingAlgorithm: expectedLBAlgo, - }) - endpoint := route.NewEndpoint(&route.EndpointOpts{ - Host: "host-1", Port: 1234, - RouteServiceUrl: "url", + It("is not specified in the endpoint options and the load balancing algorithm of a pool is kept", func() { + expectedLBAlgo := config.LOAD_BALANCE_RR + pool := route.NewPool(&route.PoolOpts{ + Logger: logger.Logger, + LoadBalancingAlgorithm: expectedLBAlgo, + }) + endpoint := route.NewEndpoint(&route.EndpointOpts{ + Host: "host-1", Port: 1234, + RouteServiceUrl: "url", + }) + pool.Put(endpoint) + Expect(pool.LoadBalancingAlgorithm).To(Equal(expectedLBAlgo)) }) - pool.Put(endpoint) - Expect(pool.LoadBalancingAlgorithm).To(Equal(expectedLBAlgo)) - }) - It("is an invalid value and the load balancing algorithm of a pool is kept", func() { - expectedLBAlgo := config.LOAD_BALANCE_RR - pool := route.NewPool(&route.PoolOpts{ - Logger: logger.Logger, - LoadBalancingAlgorithm: expectedLBAlgo, - }) - endpoint := route.NewEndpoint(&route.EndpointOpts{ - Host: "host-1", Port: 1234, - RouteServiceUrl: "url", - LoadBalancingAlgorithm: "invalid-lb-algo", + It("is an invalid value and the load balancing algorithm of a pool is kept", func() { + expectedLBAlgo := config.LOAD_BALANCE_RR + pool := route.NewPool(&route.PoolOpts{ + Logger: logger.Logger, + LoadBalancingAlgorithm: expectedLBAlgo, + }) + endpoint := route.NewEndpoint(&route.EndpointOpts{ + Host: "host-1", Port: 1234, + RouteServiceUrl: "url", + LoadBalancingAlgorithm: "invalid-lb-algo", + }) + pool.Put(endpoint) + Expect(pool.LoadBalancingAlgorithm).To(Equal(expectedLBAlgo)) + Eventually(logger).Should(gbytes.Say(`invalid-endpoint-load-balancing-algorithm-provided-keeping-pool-lb-algo`)) }) - pool.Put(endpoint) - Expect(pool.LoadBalancingAlgorithm).To(Equal(expectedLBAlgo)) - Eventually(logger).Should(gbytes.Say(`invalid-endpoint-load-balancing-algorithm-provided-keeping-pool-lb-algo`)) }) - }) - Context("Load balancing algorithm of a updated endpoint", func() { - It("is will overwrite the load balancing algorithm of the endpoint and pool", func() { - pool := route.NewPool(&route.PoolOpts{ - Logger: logger.Logger, - LoadBalancingAlgorithm: config.LOAD_BALANCE_RR, - }) + Context("Load balancing algorithm of a updated endpoint", func() { + It("will overwrite the load balancing algorithm of the endpoint and pool", func() { + pool := route.NewPool(&route.PoolOpts{ + Logger: logger.Logger, + LoadBalancingAlgorithm: config.LOAD_BALANCE_RR, + }) + + endpointOpts := route.EndpointOpts{ + Host: "host-1", + Port: 1234, + RouteServiceUrl: "url", + LoadBalancingAlgorithm: config.LOAD_BALANCE_LC, + } + + initalEndpoint := route.NewEndpoint(&endpointOpts) - endpointOpts := route.EndpointOpts{ - Host: "host-1", - Port: 1234, - RouteServiceUrl: "url", - LoadBalancingAlgorithm: config.LOAD_BALANCE_LC, - } + pool.Put(initalEndpoint) + Expect(pool.LoadBalancingAlgorithm).To(Equal(config.LOAD_BALANCE_LC)) - initalEndpoint := route.NewEndpoint(&endpointOpts) + endpointOpts.LoadBalancingAlgorithm = config.LOAD_BALANCE_RR + updatedEndpoint := route.NewEndpoint(&endpointOpts) + + pool.Put(updatedEndpoint) + Expect(pool.LoadBalancingAlgorithm).To(Equal(config.LOAD_BALANCE_RR)) + }) + }) - pool.Put(initalEndpoint) - Expect(pool.LoadBalancingAlgorithm).To(Equal(config.LOAD_BALANCE_LC)) + Context("Hash-Based load balancing algorithm", func() { - endpointOpts.LoadBalancingAlgorithm = config.LOAD_BALANCE_RR - updatedEndpoint := route.NewEndpoint(&endpointOpts) + It("correctly propagates the load balancing algorithm and HashRoutingProperties of the newly created endpoint to the other endpoints of the pool ", func() { + // TODO: route.HashBased will be implemented at a later date + // poolWithLBAlgoHB := route.NewPool(&route.PoolOpts{ + // Logger: logger.Logger, + // LoadBalancingAlgorithm: config.LOAD_BALANCE_HB, + // //HashRoutingProperties: + // }) + // iterator := poolWithLBAlgoHB.Endpoints(logger.Logger, "", false, "none", "az") + // Expect(iterator).To(BeAssignableToTypeOf(&route.HashBased{})) + // Eventually(logger).Should(gbytes.Say(`endpoint-iterator-with-round-robin-lb-algo`)) + }) - pool.Put(updatedEndpoint) - Expect(pool.LoadBalancingAlgorithm).To(Equal(config.LOAD_BALANCE_RR)) + It("overwrites the load balancing algorithm and HashRoutingProperties of the endpoint and pool", func() { + // TODO + }) }) + }) Context("RouteServiceUrl", func() { @@ -848,7 +936,9 @@ var _ = Describe("EndpointPool", func() { ServerCertDomainSAN: "pvt_test_san", PrivateInstanceId: "pvt_test_instance_id", UseTLS: true, - LoadBalancingAlgorithm: "lb-meow", + LoadBalancingAlgorithm: "hash", + HashHeader: "X-Header", + HashBalance: 1.25, }) pool.Put(e) @@ -857,7 +947,7 @@ var _ = Describe("EndpointPool", func() { json, err := pool.MarshalJSON() Expect(err).ToNot(HaveOccurred()) - Expect(string(json)).To(Equal(`[{"address":"1.2.3.4:5678","availability_zone":"az-meow","protocol":"http1","tls":false,"ttl":-1,"route_service_url":"https://my-rs.com","tags":null},{"address":"5.6.7.8:5678","availability_zone":"","protocol":"http2","tls":true,"ttl":-1,"tags":null,"private_instance_id":"pvt_test_instance_id","server_cert_domain_san":"pvt_test_san","load_balancing_algorithm":"lb-meow"}]`)) + Expect(string(json)).To(Equal(`[{"address":"1.2.3.4:5678","availability_zone":"az-meow","protocol":"http1","tls":false,"ttl":-1,"route_service_url":"https://my-rs.com","tags":null},{"address":"5.6.7.8:5678","availability_zone":"","protocol":"http2","tls":true,"ttl":-1,"tags":null,"private_instance_id":"pvt_test_instance_id","server_cert_domain_san":"pvt_test_san","load_balancing_algorithm":"hash","hash_header":"X-Header","hash_balance":1.25}]`)) }) Context("when endpoints do not have empty tags", func() { From a4b3da777ecc57b483f086f10840c0624f15e4ba Mon Sep 17 00:00:00 2001 From: Tamara Boehm Date: Tue, 2 Sep 2025 14:38:59 +0200 Subject: [PATCH 2/5] Apply review feedback --- .../gorouter/config/config.go | 4 ++-- .../gorouter/mbus/subscriber.go | 4 ++-- .../gorouter/route/pool.go | 18 ++++++++++------- .../gorouter/route/pool_test.go | 20 ------------------- 4 files changed, 15 insertions(+), 31 deletions(-) diff --git a/src/code.cloudfoundry.org/gorouter/config/config.go b/src/code.cloudfoundry.org/gorouter/config/config.go index b24ba84c4..1159bb49a 100644 --- a/src/code.cloudfoundry.org/gorouter/config/config.go +++ b/src/code.cloudfoundry.org/gorouter/config/config.go @@ -39,8 +39,8 @@ const ( ) var ( - GlobalLoadBalancingAlgorithms = []string{LOAD_BALANCE_RR, LOAD_BALANCE_LC} // These strategies can be set globally via config - LoadBalancingStrategies = append(GlobalLoadBalancingAlgorithms, LOAD_BALANCE_HB) + GlobalLoadBalancingAlgorithms = []string{LOAD_BALANCE_RR, LOAD_BALANCE_LC} // These strategies can be set globally via routing-release config + LoadBalancingStrategies = append(GlobalLoadBalancingAlgorithms, LOAD_BALANCE_HB) // These strategies can be set for individual routes (per-route options) AZPreferences = []string{AZ_PREF_NONE, AZ_PREF_LOCAL} AllowedShardingModes = []string{SHARD_ALL, SHARD_SEGMENTS, SHARD_SHARED_AND_SEGMENTS} AllowedForwardedClientCertModes = []string{ALWAYS_FORWARD, FORWARD, SANITIZE_SET} diff --git a/src/code.cloudfoundry.org/gorouter/mbus/subscriber.go b/src/code.cloudfoundry.org/gorouter/mbus/subscriber.go index 711662bff..cb41c4952 100644 --- a/src/code.cloudfoundry.org/gorouter/mbus/subscriber.go +++ b/src/code.cloudfoundry.org/gorouter/mbus/subscriber.go @@ -42,7 +42,7 @@ type RegistryMessage struct { type RegistryMessageOpts struct { LoadBalancingAlgorithm string `json:"loadbalancing"` - HashHeader string `json:"hash_header"` + HashHeaderName string `json:"hash_header"` HashBalance float64 `json:"hash_balance"` } @@ -78,7 +78,7 @@ func (rm *RegistryMessage) makeEndpoint(http2Enabled bool) (*route.Endpoint, err UseTLS: useTLS, UpdatedAt: updatedAt, LoadBalancingAlgorithm: rm.Options.LoadBalancingAlgorithm, - HashHeader: rm.Options.HashHeader, + HashHeaderName: rm.Options.HashHeaderName, HashBalance: rm.Options.HashBalance, }), nil } diff --git a/src/code.cloudfoundry.org/gorouter/route/pool.go b/src/code.cloudfoundry.org/gorouter/route/pool.go index a549aca58..088d56d09 100644 --- a/src/code.cloudfoundry.org/gorouter/route/pool.go +++ b/src/code.cloudfoundry.org/gorouter/route/pool.go @@ -75,7 +75,12 @@ type ProxyRoundTripper interface { } type HashRoutingProperties struct { - Header string + // HeaderName is a name of the HTTP header that will be used for hash routing + HeaderName string + + // BalanceFactor is a number, used to prevent endpoint overload. + // If BalanceFactor is not provided, it defaults to 0 and the overload situation will not be considered + // See https://github.com/cloudfoundry/community/blob/main/toc/rfc/rfc-0042-hash-based-routing.md#considering-a-balance-factor BalanceFactor float64 } @@ -86,7 +91,7 @@ func (hrp *HashRoutingProperties) Equal(hrp2 *HashRoutingProperties) bool { if hrp == nil || hrp2 == nil { return false } - return hrp.Header == hrp2.Header && hrp.BalanceFactor == hrp2.BalanceFactor + return hrp.HeaderName == hrp2.HeaderName && hrp.BalanceFactor == hrp2.BalanceFactor } type Endpoint struct { @@ -218,7 +223,7 @@ type EndpointOpts struct { UseTLS bool UpdatedAt time.Time LoadBalancingAlgorithm string - HashHeader string + HashHeaderName string HashBalance float64 } @@ -242,10 +247,9 @@ func NewEndpoint(opts *EndpointOpts) *Endpoint { LoadBalancingAlgorithm: opts.LoadBalancingAlgorithm, } - // TODO: Log debug? warning when HashHeader is set but LoadBalancingAlgorithm is not LOAD_BALANCE_HB? - if opts.LoadBalancingAlgorithm == config.LOAD_BALANCE_HB && opts.HashHeader != "" { // BalanceFactor is optional + if opts.LoadBalancingAlgorithm == config.LOAD_BALANCE_HB && opts.HashHeaderName != "" { // BalanceFactor is optional endpoint.HashRoutingProperties = &HashRoutingProperties{ - Header: opts.HashHeader, + HeaderName: opts.HashHeaderName, BalanceFactor: opts.HashBalance, } } @@ -634,7 +638,7 @@ func (e *Endpoint) MarshalJSON() ([]byte, error) { jsonObj.LoadBalancingAlgorithm = e.LoadBalancingAlgorithm if e.HashRoutingProperties != nil { - jsonObj.HashHeader = e.HashRoutingProperties.Header + jsonObj.HashHeader = e.HashRoutingProperties.HeaderName jsonObj.HashBalance = e.HashRoutingProperties.BalanceFactor } diff --git a/src/code.cloudfoundry.org/gorouter/route/pool_test.go b/src/code.cloudfoundry.org/gorouter/route/pool_test.go index 2e2b1f408..c422d9649 100644 --- a/src/code.cloudfoundry.org/gorouter/route/pool_test.go +++ b/src/code.cloudfoundry.org/gorouter/route/pool_test.go @@ -428,26 +428,6 @@ var _ = Describe("EndpointPool", func() { Expect(pool.LoadBalancingAlgorithm).To(Equal(config.LOAD_BALANCE_RR)) }) }) - - Context("Hash-Based load balancing algorithm", func() { - - It("correctly propagates the load balancing algorithm and HashRoutingProperties of the newly created endpoint to the other endpoints of the pool ", func() { - // TODO: route.HashBased will be implemented at a later date - // poolWithLBAlgoHB := route.NewPool(&route.PoolOpts{ - // Logger: logger.Logger, - // LoadBalancingAlgorithm: config.LOAD_BALANCE_HB, - // //HashRoutingProperties: - // }) - // iterator := poolWithLBAlgoHB.Endpoints(logger.Logger, "", false, "none", "az") - // Expect(iterator).To(BeAssignableToTypeOf(&route.HashBased{})) - // Eventually(logger).Should(gbytes.Say(`endpoint-iterator-with-round-robin-lb-algo`)) - }) - - It("overwrites the load balancing algorithm and HashRoutingProperties of the endpoint and pool", func() { - // TODO - }) - }) - }) Context("RouteServiceUrl", func() { From 21dd5e4abe94c5c6fefb7f13a60ec9f72da9ec5e Mon Sep 17 00:00:00 2001 From: Tamara Boehm <34028368+b1tamara@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:41:05 +0200 Subject: [PATCH 3/5] Update src/code.cloudfoundry.org/gorouter/config/config.go Co-authored-by: Maximilian Moehl <44866320+maxmoehl@users.noreply.github.com> --- src/code.cloudfoundry.org/gorouter/config/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/code.cloudfoundry.org/gorouter/config/config.go b/src/code.cloudfoundry.org/gorouter/config/config.go index 1159bb49a..670526dea 100644 --- a/src/code.cloudfoundry.org/gorouter/config/config.go +++ b/src/code.cloudfoundry.org/gorouter/config/config.go @@ -39,8 +39,8 @@ const ( ) var ( - GlobalLoadBalancingAlgorithms = []string{LOAD_BALANCE_RR, LOAD_BALANCE_LC} // These strategies can be set globally via routing-release config - LoadBalancingStrategies = append(GlobalLoadBalancingAlgorithms, LOAD_BALANCE_HB) // These strategies can be set for individual routes (per-route options) + GlobalLoadBalancingAlgorithms = []string{LOAD_BALANCE_RR, LOAD_BALANCE_LC} + LoadBalancingStrategies = []string{LOAD_BALANCE_RR, LOAD_BALANCE_LC, LOAD_BALANCE_HB} AZPreferences = []string{AZ_PREF_NONE, AZ_PREF_LOCAL} AllowedShardingModes = []string{SHARD_ALL, SHARD_SEGMENTS, SHARD_SHARED_AND_SEGMENTS} AllowedForwardedClientCertModes = []string{ALWAYS_FORWARD, FORWARD, SANITIZE_SET} From 04e67dae86b85433ffde8a56597b7a945d80ee31 Mon Sep 17 00:00:00 2001 From: Tamara Boehm Date: Mon, 8 Sep 2025 10:14:24 +0200 Subject: [PATCH 4/5] remove hashBasedProperties struct --- .../gorouter/config/config.go | 6 +-- .../gorouter/mbus/subscriber.go | 2 +- .../gorouter/route/pool.go | 41 ++++--------------- .../gorouter/route/pool_test.go | 24 +++++------ 4 files changed, 25 insertions(+), 48 deletions(-) diff --git a/src/code.cloudfoundry.org/gorouter/config/config.go b/src/code.cloudfoundry.org/gorouter/config/config.go index 670526dea..8b988349b 100644 --- a/src/code.cloudfoundry.org/gorouter/config/config.go +++ b/src/code.cloudfoundry.org/gorouter/config/config.go @@ -39,7 +39,7 @@ const ( ) var ( - GlobalLoadBalancingAlgorithms = []string{LOAD_BALANCE_RR, LOAD_BALANCE_LC} + GlobalLoadBalancingStrategies = []string{LOAD_BALANCE_RR, LOAD_BALANCE_LC} LoadBalancingStrategies = []string{LOAD_BALANCE_RR, LOAD_BALANCE_LC, LOAD_BALANCE_HB} AZPreferences = []string{AZ_PREF_NONE, AZ_PREF_LOCAL} AllowedShardingModes = []string{SHARD_ALL, SHARD_SEGMENTS, SHARD_SHARED_AND_SEGMENTS} @@ -598,7 +598,7 @@ func DefaultConfig() (*Config, error) { } func IsGlobalLoadBalancingAlgorithmValid(lbAlgo string) bool { - return slices.Contains(GlobalLoadBalancingAlgorithms, lbAlgo) + return slices.Contains(GlobalLoadBalancingStrategies, lbAlgo) } func IsLoadBalancingAlgorithmValid(lbAlgo string) bool { @@ -762,7 +762,7 @@ func (c *Config) Process() error { } if !IsGlobalLoadBalancingAlgorithmValid(c.LoadBalance) { - return fmt.Errorf("Invalid global load balancing algorithm %s. Allowed values are %s", c.LoadBalance, GlobalLoadBalancingAlgorithms) + return fmt.Errorf("Invalid global load balancing algorithm %s. Allowed values are %s", c.LoadBalance, GlobalLoadBalancingStrategies) } validAZPref := false diff --git a/src/code.cloudfoundry.org/gorouter/mbus/subscriber.go b/src/code.cloudfoundry.org/gorouter/mbus/subscriber.go index cb41c4952..af17a19b4 100644 --- a/src/code.cloudfoundry.org/gorouter/mbus/subscriber.go +++ b/src/code.cloudfoundry.org/gorouter/mbus/subscriber.go @@ -79,7 +79,7 @@ func (rm *RegistryMessage) makeEndpoint(http2Enabled bool) (*route.Endpoint, err UpdatedAt: updatedAt, LoadBalancingAlgorithm: rm.Options.LoadBalancingAlgorithm, HashHeaderName: rm.Options.HashHeaderName, - HashBalance: rm.Options.HashBalance, + HashBalanceFactor: rm.Options.HashBalance, }), nil } diff --git a/src/code.cloudfoundry.org/gorouter/route/pool.go b/src/code.cloudfoundry.org/gorouter/route/pool.go index 088d56d09..f089fc15b 100644 --- a/src/code.cloudfoundry.org/gorouter/route/pool.go +++ b/src/code.cloudfoundry.org/gorouter/route/pool.go @@ -74,26 +74,6 @@ type ProxyRoundTripper interface { CancelRequest(*http.Request) } -type HashRoutingProperties struct { - // HeaderName is a name of the HTTP header that will be used for hash routing - HeaderName string - - // BalanceFactor is a number, used to prevent endpoint overload. - // If BalanceFactor is not provided, it defaults to 0 and the overload situation will not be considered - // See https://github.com/cloudfoundry/community/blob/main/toc/rfc/rfc-0042-hash-based-routing.md#considering-a-balance-factor - BalanceFactor float64 -} - -func (hrp *HashRoutingProperties) Equal(hrp2 *HashRoutingProperties) bool { - if hrp == nil && hrp2 == nil { - return true - } - if hrp == nil || hrp2 == nil { - return false - } - return hrp.HeaderName == hrp2.HeaderName && hrp.BalanceFactor == hrp2.BalanceFactor -} - type Endpoint struct { ApplicationId string AvailabilityZone string @@ -114,7 +94,8 @@ type Endpoint struct { UpdatedAt time.Time RoundTripperInit sync.Once LoadBalancingAlgorithm string - HashRoutingProperties *HashRoutingProperties + HashHeaderName string + HashBalanceFactor float64 } func (e *Endpoint) RoundTripper() ProxyRoundTripper { @@ -158,7 +139,8 @@ func (e *Endpoint) Equal(e2 *Endpoint) bool { e.useTls == e2.useTls && e.UpdatedAt.Equal(e2.UpdatedAt) && e.LoadBalancingAlgorithm == e2.LoadBalancingAlgorithm && - e.HashRoutingProperties.Equal(e2.HashRoutingProperties) && + e.HashHeaderName == e2.HashHeaderName && + e.HashBalanceFactor == e2.HashBalanceFactor && maps.Equal(e.Tags, e2.Tags) } @@ -224,7 +206,7 @@ type EndpointOpts struct { UpdatedAt time.Time LoadBalancingAlgorithm string HashHeaderName string - HashBalance float64 + HashBalanceFactor float64 } func NewEndpoint(opts *EndpointOpts) *Endpoint { @@ -248,10 +230,8 @@ func NewEndpoint(opts *EndpointOpts) *Endpoint { } if opts.LoadBalancingAlgorithm == config.LOAD_BALANCE_HB && opts.HashHeaderName != "" { // BalanceFactor is optional - endpoint.HashRoutingProperties = &HashRoutingProperties{ - HeaderName: opts.HashHeaderName, - BalanceFactor: opts.HashBalance, - } + endpoint.HashHeaderName = opts.HashHeaderName + endpoint.HashBalanceFactor = opts.HashBalanceFactor } return endpoint @@ -636,11 +616,8 @@ func (e *Endpoint) MarshalJSON() ([]byte, error) { jsonObj.PrivateInstanceId = e.PrivateInstanceId jsonObj.ServerCertDomainSAN = e.ServerCertDomainSAN jsonObj.LoadBalancingAlgorithm = e.LoadBalancingAlgorithm - - if e.HashRoutingProperties != nil { - jsonObj.HashHeader = e.HashRoutingProperties.HeaderName - jsonObj.HashBalance = e.HashRoutingProperties.BalanceFactor - } + jsonObj.HashHeader = e.HashHeaderName + jsonObj.HashBalance = e.HashBalanceFactor return json.Marshal(jsonObj) } diff --git a/src/code.cloudfoundry.org/gorouter/route/pool_test.go b/src/code.cloudfoundry.org/gorouter/route/pool_test.go index c422d9649..31da6c8d7 100644 --- a/src/code.cloudfoundry.org/gorouter/route/pool_test.go +++ b/src/code.cloudfoundry.org/gorouter/route/pool_test.go @@ -49,15 +49,15 @@ var _ = Describe("Endpoint", func() { Host: "localhost", Port: 8080, LoadBalancingAlgorithm: "hash", - HashHeader: "X-Header", - HashBalance: 1.15, + HashHeaderName: "X-Header", + HashBalanceFactor: 1.15, }) }) It("should have the correct hash header", func() { - Expect(endpoint.HashRoutingProperties.Header).To(Equal("X-Header")) + Expect(endpoint.HashHeaderName).To(Equal("X-Header")) }) It("should have the correct hash balance", func() { - Expect(endpoint.HashRoutingProperties.BalanceFactor).To(Equal(1.15)) + Expect(endpoint.HashBalanceFactor).To(Equal(1.15)) }) It("should have the correct load balancing algorithm", func() { Expect(endpoint.LoadBalancingAlgorithm).To(Equal("hash")) @@ -72,24 +72,24 @@ var _ = Describe("Endpoint", func() { Host: "localhost", Port: 8080, LoadBalancingAlgorithm: "hash", - HashHeader: "X-Header", - HashBalance: 1.15, + HashHeaderName: "X-Header", + HashBalanceFactor: 1.15, } opts2 := &route.EndpointOpts{ AppId: "test-app", Host: "localhost", Port: 8080, LoadBalancingAlgorithm: "hash", - HashHeader: "X-Header", - HashBalance: 1.15, + HashHeaderName: "X-Header", + HashBalanceFactor: 1.15, } opts3 := &route.EndpointOpts{ AppId: "test-app", Host: "localhost", Port: 8080, LoadBalancingAlgorithm: "hash", - HashHeader: "X-Header", - HashBalance: 2.25, + HashHeaderName: "X-Header", + HashBalanceFactor: 2.25, } endpoint1 = route.NewEndpoint(opts1) endpoint2 = route.NewEndpoint(opts2) @@ -917,8 +917,8 @@ var _ = Describe("EndpointPool", func() { PrivateInstanceId: "pvt_test_instance_id", UseTLS: true, LoadBalancingAlgorithm: "hash", - HashHeader: "X-Header", - HashBalance: 1.25, + HashHeaderName: "X-Header", + HashBalanceFactor: 1.25, }) pool.Put(e) From 59cd565561a786a9d45001948fcd0d9ad16d8277 Mon Sep 17 00:00:00 2001 From: Tamara Boehm Date: Mon, 22 Sep 2025 09:40:51 +0200 Subject: [PATCH 5/5] Fix the subscriber tests --- .../gorouter/mbus/subscriber_test.go | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/code.cloudfoundry.org/gorouter/mbus/subscriber_test.go b/src/code.cloudfoundry.org/gorouter/mbus/subscriber_test.go index 8f42cf9fd..ef6fe7967 100644 --- a/src/code.cloudfoundry.org/gorouter/mbus/subscriber_test.go +++ b/src/code.cloudfoundry.org/gorouter/mbus/subscriber_test.go @@ -636,7 +636,7 @@ var _ = Describe("Subscriber", func() { Uris: []route.Uri{"test.example.com"}, Options: mbus.RegistryMessageOpts{ LoadBalancingAlgorithm: expectedLBAlgo, - HashHeader: "X-Header", + HashHeaderName: "X-Header", HashBalance: 1.5, }, } @@ -653,8 +653,8 @@ var _ = Describe("Subscriber", func() { AppId: "app", Protocol: "http2", LoadBalancingAlgorithm: expectedLBAlgo, - HashHeader: "X-Header", - HashBalance: 1.5, + HashHeaderName: "X-Header", + HashBalanceFactor: 1.5, }) Expect(originalEndpoint).To(Equal(expectedEndpoint)) @@ -670,7 +670,7 @@ var _ = Describe("Subscriber", func() { Uris: []route.Uri{"test.example.com"}, Options: mbus.RegistryMessageOpts{ LoadBalancingAlgorithm: expectedLBAlgo, - HashHeader: "X-Header", + HashHeaderName: "X-Header", }, } data, err := json.Marshal(msg) @@ -686,10 +686,11 @@ var _ = Describe("Subscriber", func() { AppId: "app", Protocol: "http2", LoadBalancingAlgorithm: expectedLBAlgo, - HashHeader: "X-Header", - HashBalance: 0.0, + HashHeaderName: "X-Header", + HashBalanceFactor: 0.0, }) - Expect(expectedEndpoint.HashRoutingProperties).ToNot(BeNil()) + Expect(expectedEndpoint.HashHeaderName).To(Equal("X-Header")) + Expect(expectedEndpoint.HashBalanceFactor).To(Equal(0.0)) Expect(originalEndpoint).To(Equal(expectedEndpoint)) }) @@ -703,7 +704,7 @@ var _ = Describe("Subscriber", func() { Uris: []route.Uri{"test.example.com"}, Options: mbus.RegistryMessageOpts{ LoadBalancingAlgorithm: expectedLBAlgo, - HashHeader: "X-Header", + HashHeaderName: "X-Header", }, } data, err := json.Marshal(msg) @@ -720,7 +721,8 @@ var _ = Describe("Subscriber", func() { Protocol: "http2", LoadBalancingAlgorithm: expectedLBAlgo, }) - Expect(expectedEndpoint.HashRoutingProperties).To(BeNil()) + Expect(expectedEndpoint.HashHeaderName).To(BeEmpty()) + Expect(expectedEndpoint.HashBalanceFactor).To(Equal(0.0)) Expect(originalEndpoint).To(Equal(expectedEndpoint)) }) })