Compile down LB policy to disco chain nodes

This commit is contained in:
freddygv 2020-08-28 13:11:04 -06:00
parent ff56a64b08
commit 81115b6eaa
3 changed files with 209 additions and 0 deletions

View File

@ -707,6 +707,7 @@ func (c *compiler) getSplitterNode(sid structs.ServiceID) (*structs.DiscoveryGra
// sanely if there is some sort of graph loop below. // sanely if there is some sort of graph loop below.
c.recordNode(splitNode) c.recordNode(splitNode)
var hasLB bool
for _, split := range splitter.Splits { for _, split := range splitter.Splits {
compiledSplit := &structs.DiscoverySplit{ compiledSplit := &structs.DiscoverySplit{
Weight: split.Weight, Weight: split.Weight,
@ -739,6 +740,15 @@ func (c *compiler) getSplitterNode(sid structs.ServiceID) (*structs.DiscoveryGra
return nil, err return nil, err
} }
compiledSplit.NextNode = node.MapKey() compiledSplit.NextNode = node.MapKey()
// There exists the possibility that a splitter may split between two distinct service names
// with distinct hash-based load balancer configs specified in their service resolvers.
// We cannot apply multiple hash policies to a splitter node's route action.
// Therefore, we attach the first hash-based load balancer config we encounter.
if !hasLB && node.LoadBalancer.IsHashBased() {
splitNode.LoadBalancer = node.LoadBalancer
hasLB = true
}
} }
c.usesAdvancedRoutingFeatures = true c.usesAdvancedRoutingFeatures = true
@ -851,6 +861,7 @@ RESOLVE_AGAIN:
Target: target.ID, Target: target.ID,
ConnectTimeout: connectTimeout, ConnectTimeout: connectTimeout,
}, },
LoadBalancer: resolver.LoadBalancer,
} }
target.Subset = resolver.Subsets[target.ServiceSubset] target.Subset = resolver.Subsets[target.ServiceSubset]

View File

@ -51,6 +51,7 @@ func TestCompile(t *testing.T) {
"default resolver with external sni": testcase_DefaultResolver_ExternalSNI(), "default resolver with external sni": testcase_DefaultResolver_ExternalSNI(),
"resolver with no entries and inferring defaults": testcase_DefaultResolver(), "resolver with no entries and inferring defaults": testcase_DefaultResolver(),
"default resolver with proxy defaults": testcase_DefaultResolver_WithProxyDefaults(), "default resolver with proxy defaults": testcase_DefaultResolver_WithProxyDefaults(),
"loadbalancer config": testcase_LBConfig(),
"service redirect to service with default resolver is not a default chain": testcase_RedirectToDefaultResolverIsNotDefaultChain(), "service redirect to service with default resolver is not a default chain": testcase_RedirectToDefaultResolverIsNotDefaultChain(),
"all the bells and whistles": testcase_AllBellsAndWhistles(), "all the bells and whistles": testcase_AllBellsAndWhistles(),
@ -1760,6 +1761,17 @@ func testcase_AllBellsAndWhistles() compileTestCase {
"prod": {Filter: "ServiceMeta.env == prod"}, "prod": {Filter: "ServiceMeta.env == prod"},
"qa": {Filter: "ServiceMeta.env == qa"}, "qa": {Filter: "ServiceMeta.env == qa"},
}, },
LoadBalancer: structs.LoadBalancer{
Policy: "ring_hash",
RingHashConfig: structs.RingHashConfig{
MaximumRingSize: 100,
},
HashPolicies: []structs.HashPolicy{
{
SourceAddress: true,
},
},
},
}, },
&structs.ServiceResolverConfigEntry{ &structs.ServiceResolverConfigEntry{
Kind: "service-resolver", Kind: "service-resolver",
@ -1821,6 +1833,17 @@ func testcase_AllBellsAndWhistles() compileTestCase {
NextNode: "resolver:v3.main.default.dc1", NextNode: "resolver:v3.main.default.dc1",
}, },
}, },
LoadBalancer: structs.LoadBalancer{
Policy: "ring_hash",
RingHashConfig: structs.RingHashConfig{
MaximumRingSize: 100,
},
HashPolicies: []structs.HashPolicy{
{
SourceAddress: true,
},
},
},
}, },
"resolver:prod.redirected.default.dc1": { "resolver:prod.redirected.default.dc1": {
Type: structs.DiscoveryGraphNodeTypeResolver, Type: structs.DiscoveryGraphNodeTypeResolver,
@ -1829,6 +1852,17 @@ func testcase_AllBellsAndWhistles() compileTestCase {
ConnectTimeout: 5 * time.Second, ConnectTimeout: 5 * time.Second,
Target: "prod.redirected.default.dc1", Target: "prod.redirected.default.dc1",
}, },
LoadBalancer: structs.LoadBalancer{
Policy: "ring_hash",
RingHashConfig: structs.RingHashConfig{
MaximumRingSize: 100,
},
HashPolicies: []structs.HashPolicy{
{
SourceAddress: true,
},
},
},
}, },
"resolver:v1.main.default.dc1": { "resolver:v1.main.default.dc1": {
Type: structs.DiscoveryGraphNodeTypeResolver, Type: structs.DiscoveryGraphNodeTypeResolver,
@ -2219,6 +2253,167 @@ func testcase_CircularSplit() compileTestCase {
} }
} }
func testcase_LBConfig() compileTestCase {
entries := newEntries()
setServiceProtocol(entries, "foo", "http")
setServiceProtocol(entries, "bar", "http")
setServiceProtocol(entries, "baz", "http")
entries.AddSplitters(
&structs.ServiceSplitterConfigEntry{
Kind: "service-splitter",
Name: "main",
Splits: []structs.ServiceSplit{
{Weight: 60, Service: "foo"},
{Weight: 20, Service: "bar"},
{Weight: 20, Service: "baz"},
},
},
)
entries.AddResolvers(
&structs.ServiceResolverConfigEntry{
Kind: "service-resolver",
Name: "foo",
LoadBalancer: structs.LoadBalancer{
Policy: "least_request",
LeastRequestConfig: structs.LeastRequestConfig{
ChoiceCount: 3,
},
},
},
&structs.ServiceResolverConfigEntry{
Kind: "service-resolver",
Name: "bar",
LoadBalancer: structs.LoadBalancer{
Policy: "ring_hash",
RingHashConfig: structs.RingHashConfig{
MaximumRingSize: 101,
},
HashPolicies: []structs.HashPolicy{
{
SourceAddress: true,
},
},
},
},
&structs.ServiceResolverConfigEntry{
Kind: "service-resolver",
Name: "baz",
LoadBalancer: structs.LoadBalancer{
Policy: "maglev",
HashPolicies: []structs.HashPolicy{
{
Field: "cookie",
FieldMatchValue: "chocolate-chip",
Terminal: true,
},
},
},
},
)
expect := &structs.CompiledDiscoveryChain{
Protocol: "http",
StartNode: "splitter:main.default",
Nodes: map[string]*structs.DiscoveryGraphNode{
"splitter:main.default": {
Type: structs.DiscoveryGraphNodeTypeSplitter,
Name: "main.default",
Splits: []*structs.DiscoverySplit{
{
Weight: 60,
NextNode: "resolver:foo.default.dc1",
},
{
Weight: 20,
NextNode: "resolver:bar.default.dc1",
},
{
Weight: 20,
NextNode: "resolver:baz.default.dc1",
},
},
// The LB config from bar is attached because splitters only care about hash-based policies,
// and it's the config from bar not baz because we pick the first one we encounter in the Splits.
LoadBalancer: structs.LoadBalancer{
Policy: "ring_hash",
RingHashConfig: structs.RingHashConfig{
MaximumRingSize: 101,
},
HashPolicies: []structs.HashPolicy{
{
SourceAddress: true,
},
},
},
},
// Each service's LB config is passed down from the service-resolver to the resolver node
"resolver:foo.default.dc1": {
Type: structs.DiscoveryGraphNodeTypeResolver,
Name: "foo.default.dc1",
Resolver: &structs.DiscoveryResolver{
Default: true,
ConnectTimeout: 5 * time.Second,
Target: "foo.default.dc1",
},
LoadBalancer: structs.LoadBalancer{
Policy: "least_request",
LeastRequestConfig: structs.LeastRequestConfig{
ChoiceCount: 3,
},
},
},
"resolver:bar.default.dc1": {
Type: structs.DiscoveryGraphNodeTypeResolver,
Name: "bar.default.dc1",
Resolver: &structs.DiscoveryResolver{
Default: true,
ConnectTimeout: 5 * time.Second,
Target: "bar.default.dc1",
},
LoadBalancer: structs.LoadBalancer{
Policy: "ring_hash",
RingHashConfig: structs.RingHashConfig{
MaximumRingSize: 101,
},
HashPolicies: []structs.HashPolicy{
{
SourceAddress: true,
},
},
},
},
"resolver:baz.default.dc1": {
Type: structs.DiscoveryGraphNodeTypeResolver,
Name: "baz.default.dc1",
Resolver: &structs.DiscoveryResolver{
Default: true,
ConnectTimeout: 5 * time.Second,
Target: "baz.default.dc1",
},
LoadBalancer: structs.LoadBalancer{
Policy: "maglev",
HashPolicies: []structs.HashPolicy{
{
Field: "cookie",
FieldMatchValue: "chocolate-chip",
Terminal: true,
},
},
},
},
},
Targets: map[string]*structs.DiscoveryTarget{
"foo.default.dc1": newTarget("foo", "", "default", "dc1", nil),
"bar.default.dc1": newTarget("bar", "", "default", "dc1", nil),
"baz.default.dc1": newTarget("baz", "", "default", "dc1", nil),
},
}
return compileTestCase{entries: entries, expect: expect}
}
func newSimpleRoute(name string, muts ...func(*structs.ServiceRoute)) structs.ServiceRoute { func newSimpleRoute(name string, muts ...func(*structs.ServiceRoute)) structs.ServiceRoute {
r := structs.ServiceRoute{ r := structs.ServiceRoute{
Match: &structs.ServiceRouteMatch{ Match: &structs.ServiceRouteMatch{

View File

@ -107,6 +107,9 @@ type DiscoveryGraphNode struct {
// fields for Type==resolver // fields for Type==resolver
Resolver *DiscoveryResolver `json:",omitempty"` Resolver *DiscoveryResolver `json:",omitempty"`
// shared by Type==resolver || Type==splitter
LoadBalancer LoadBalancer `json:",omitempty"`
} }
func (s *DiscoveryGraphNode) IsRouter() bool { func (s *DiscoveryGraphNode) IsRouter() bool {