Skip to content

Commit a880d85

Browse files
committed
Add GRPCRoute weighted backendRefs test
1 parent 51488fa commit a880d85

File tree

2 files changed

+177
-0
lines changed

2 files changed

+177
-0
lines changed

conformance/tests/grpcroute-weight.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package tests
18+
19+
import (
20+
"cmp"
21+
"errors"
22+
"fmt"
23+
"math"
24+
"slices"
25+
"strings"
26+
"sync"
27+
"testing"
28+
29+
"golang.org/x/sync/errgroup"
30+
"google.golang.org/grpc/codes"
31+
"k8s.io/apimachinery/pkg/types"
32+
v1 "sigs.k8s.io/gateway-api/apis/v1"
33+
pb "sigs.k8s.io/gateway-api/conformance/echo-basic/grpcechoserver"
34+
35+
"sigs.k8s.io/gateway-api/conformance/utils/grpc"
36+
"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
37+
"sigs.k8s.io/gateway-api/conformance/utils/suite"
38+
"sigs.k8s.io/gateway-api/pkg/features"
39+
)
40+
41+
func init() {
42+
ConformanceTests = append(ConformanceTests, GRPCRouteWeight)
43+
}
44+
45+
var GRPCRouteWeight = suite.ConformanceTest{
46+
ShortName: "GRPCRouteWeight",
47+
Description: "An GRPCRoute with weighted backends",
48+
Manifests: []string{"tests/grpcroute-weight.yaml"},
49+
Features: []features.FeatureName{
50+
features.SupportGateway,
51+
features.SupportGRPCRoute,
52+
},
53+
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
54+
var (
55+
ns = "gateway-conformance-infra"
56+
routeNN = types.NamespacedName{Name: "weighted-backends", Namespace: ns}
57+
gwNN = types.NamespacedName{Name: "same-namespace", Namespace: ns}
58+
gwAddr = kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &v1.GRPCRoute{}, true, routeNN)
59+
)
60+
61+
t.Run("Requests should have a distribution that matches the weight", func(t *testing.T) {
62+
expected := grpc.ExpectedResponse{
63+
EchoRequest: &pb.EchoRequest{},
64+
Response: grpc.Response{Code: codes.OK},
65+
Namespace: "gateway-conformance-infra",
66+
}
67+
68+
// Assert request succeeds before doing our distribution check
69+
grpc.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.GRPCClient, suite.TimeoutConfig, gwAddr, expected)
70+
71+
for i := 0; i < 10; i++ {
72+
if err := testGRPCDistribution(t, suite, gwAddr, expected); err != nil {
73+
t.Logf("Traffic distribution test failed (%d/10): %s", i+1, err)
74+
} else {
75+
return
76+
}
77+
}
78+
t.Fatal("Weighted distribution tests failed")
79+
})
80+
},
81+
}
82+
83+
func testGRPCDistribution(t *testing.T, suite *suite.ConformanceTestSuite, gwAddr string, expected grpc.ExpectedResponse) error {
84+
const (
85+
concurrentRequests = 10
86+
tolerancePercentage = 0.05
87+
totalRequests = 500.0
88+
)
89+
var (
90+
grpcClient = suite.GRPCClient
91+
g errgroup.Group
92+
seenMutex sync.Mutex
93+
seen = make(map[string]float64, 3 /* number of backends */)
94+
expectedWeights = map[string]float64{
95+
"grpc-infra-backend-v1": 0.7,
96+
"grpc-infra-backend-v2": 0.3,
97+
"grpc-infra-backend-v3": 0.0,
98+
}
99+
)
100+
g.SetLimit(concurrentRequests)
101+
for i := 0.0; i < totalRequests; i++ {
102+
g.Go(func() error {
103+
resp, err := grpcClient.SendRPC(t, gwAddr, expected, suite.TimeoutConfig.MaxTimeToConsistency)
104+
if err != nil {
105+
return fmt.Errorf("failed to send gRPC request: %w", err)
106+
}
107+
if resp.Code != codes.OK {
108+
return fmt.Errorf("expected OK response, got %v", resp.Code)
109+
}
110+
111+
seenMutex.Lock()
112+
defer seenMutex.Unlock()
113+
114+
podName := resp.Response.GetAssertions().GetContext().GetPod()
115+
for expectedBackend := range expectedWeights {
116+
if strings.HasPrefix(podName, expectedBackend) {
117+
seen[expectedBackend]++
118+
return nil
119+
}
120+
}
121+
122+
return fmt.Errorf("request was handled by an unexpected pod %q", podName)
123+
})
124+
}
125+
126+
if err := g.Wait(); err != nil {
127+
return fmt.Errorf("error while sending requests: %w", err)
128+
}
129+
130+
var errs []error
131+
if len(seen) != 2 {
132+
errs = append(errs, fmt.Errorf("expected only two backends to receive traffic"))
133+
}
134+
135+
for wantBackend, wantPercent := range expectedWeights {
136+
gotCount, ok := seen[wantBackend]
137+
138+
if !ok && wantPercent != 0.0 {
139+
errs = append(errs, fmt.Errorf("expect traffic to hit backend %q - but none was received", wantBackend))
140+
continue
141+
}
142+
143+
gotPercent := gotCount / totalRequests
144+
145+
if math.Abs(gotPercent-wantPercent) > tolerancePercentage {
146+
errs = append(errs, fmt.Errorf("backend %q weighted traffic of %v not within tolerance %v (+/-%f)",
147+
wantBackend,
148+
gotPercent,
149+
wantPercent,
150+
tolerancePercentage,
151+
))
152+
}
153+
}
154+
slices.SortFunc(errs, func(a, b error) int {
155+
return cmp.Compare(a.Error(), b.Error())
156+
})
157+
return errors.Join(errs...)
158+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
apiVersion: gateway.networking.k8s.io/v1
2+
kind: GRPCRoute
3+
metadata:
4+
name: weighted-backends
5+
namespace: gateway-conformance-infra
6+
spec:
7+
parentRefs:
8+
- name: same-namespace
9+
rules:
10+
- backendRefs:
11+
- name: grpc-infra-backend-v1
12+
port: 8080
13+
weight: 70
14+
- name: grpc-infra-backend-v2
15+
port: 8080
16+
weight: 30
17+
- name: grpc-infra-backend-v3
18+
port: 8080
19+
weight: 0

0 commit comments

Comments
 (0)