-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Expand file tree
/
Copy pathserviceconfig.go
More file actions
354 lines (314 loc) · 11.9 KB
/
Copy pathserviceconfig.go
File metadata and controls
354 lines (314 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package resolver
import (
"encoding/json"
"fmt"
"math/bits"
rand "math/rand/v2"
"strings"
"sync"
"time"
xxhash "github.com/cespare/xxhash/v2"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/internal/grpcsync"
"google.golang.org/grpc/internal/grpcutil"
iresolver "google.golang.org/grpc/internal/resolver"
iringhash "google.golang.org/grpc/internal/ringhash"
"google.golang.org/grpc/internal/serviceconfig"
"google.golang.org/grpc/internal/wrr"
"google.golang.org/grpc/internal/xds/balancer/clusterimpl"
"google.golang.org/grpc/internal/xds/balancer/clustermanager"
"google.golang.org/grpc/internal/xds/httpfilter"
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
const (
cdsName = "cds_experimental"
xdsClusterManagerName = "xds_cluster_manager_experimental"
clusterPrefix = "cluster:"
clusterSpecifierPluginPrefix = "cluster_specifier_plugin:"
)
type serviceConfig struct {
LoadBalancingConfig balancerConfig `json:"loadBalancingConfig"`
}
type balancerConfig []map[string]any
func newBalancerConfig(name string, config any) balancerConfig {
return []map[string]any{{name: config}}
}
type cdsBalancerConfig struct {
Cluster string `json:"cluster"`
}
type xdsChildConfig struct {
ChildPolicy balancerConfig `json:"childPolicy"`
}
type xdsClusterManagerConfig struct {
Children map[string]xdsChildConfig `json:"children"`
}
// serviceConfigJSON produces a service config in JSON format that contains LB
// policy config for the "xds_cluster_manager" LB policy, with entries in the
// children map for all active clusters.
func serviceConfigJSON(activeClusters, activePlugins map[string]*grpcsync.RefCounted[*clusterInfo]) []byte {
// Generate children (all entries in activeClusters).
children := make(map[string]xdsChildConfig)
for cluster, ci := range activeClusters {
children[cluster] = ci.Value().cfg
}
for plugin, ci := range activePlugins {
children[plugin] = ci.Value().cfg
}
sc := serviceConfig{
LoadBalancingConfig: newBalancerConfig(
xdsClusterManagerName, xdsClusterManagerConfig{Children: children},
),
}
// This is not expected to fail as we have constructed the service config by
// hand right above, and therefore ok to panic.
bs, err := json.Marshal(sc)
if err != nil {
panic(fmt.Sprintf("failed to marshal service config %+v: %v", sc, err))
}
return bs
}
type virtualHost struct {
// retry policy present in virtual host
retryConfig *xdsresource.RetryConfig
}
// routeCluster holds information about a cluster as referenced by a route.
type routeCluster struct {
name string // Name of the cluster.
interceptor httpfilter.ClientInterceptor // HTTP filters to run for RPCs matching this route.
// info is the resolver-wide entry for this cluster, shared by every route
// that references it. An RPC routed here holds a reference on it until the
// RPC is committed.
info *grpcsync.RefCounted[*clusterInfo]
}
type route struct {
m *xdsresource.CompositeMatcher // converted from route matchers
actionType xdsresource.RouteActionType // holds route action type
clusters wrr.WRR // holds *routeCluster entries
routeClusters []*grpcsync.RefCounted[*routeCluster] // Route clusters belonging to this route
maxStreamDuration time.Duration
retryConfig *xdsresource.RetryConfig
hashPolicies []*xdsresource.HashPolicy
autoHostRewrite bool
}
func (r route) String() string {
return fmt.Sprintf("%s -> { clusters: %v, maxStreamDuration: %v }", r.m.String(), r.clusters, r.maxStreamDuration)
}
// stoppableConfigSelector extends the iresolver.ConfigSelector interface with a
// stop() method. This makes it possible to swap the current config selector
// with an erroring config selector when the LDS or RDS resource is not found on
// the management server.
type stoppableConfigSelector interface {
iresolver.ConfigSelector
stop()
}
// erroringConfigSelector always returns an error, with the xDS node ID included
// in the error message. It is used to swap out the current config selector
// when the LDS or RDS resource is not found on the management server.
type erroringConfigSelector struct {
err error
}
func newErroringConfigSelector(err error, xdsNodeID string) *erroringConfigSelector {
return &erroringConfigSelector{err: annotateErrorWithNodeID(status.Error(codes.Unavailable, err.Error()), xdsNodeID)}
}
func (cs *erroringConfigSelector) SelectConfig(iresolver.RPCInfo) (*iresolver.RPCConfig, error) {
return nil, cs.err
}
func (cs *erroringConfigSelector) stop() {}
type configSelector struct {
channelID uint64 // Static hash when hash policy is HashPolicyTypeChannelID
xdsNodeID string // xDS node ID, for annotating errors.
sendNewServiceConfig func() // Function to send a new service config to gRPC.
// Configuration received from the xDS management server.
virtualHost virtualHost
routes []route
clusters map[string]*grpcsync.RefCounted[*clusterInfo]
plugins map[string]*grpcsync.RefCounted[*clusterInfo]
httpFilterConfig []xdsresource.HTTPFilter
xdsConfig *xdsresource.XDSConfig
}
var errNoMatchedRouteFound = status.Errorf(codes.Unavailable, "no matched route was found")
var errUnsupportedClientRouteAction = status.Errorf(codes.Unavailable, "matched route does not have a supported route action type")
// annotateErrorWithNodeID annotates the given error with the provided xDS node
// ID. This is used by the real config selector when it runs into errors, and
// also by the erroring config selector.
func annotateErrorWithNodeID(err error, nodeID string) error {
return fmt.Errorf("[xDS node id: %s]: %w", nodeID, err)
}
func (cs *configSelector) SelectConfig(rpcInfo iresolver.RPCInfo) (*iresolver.RPCConfig, error) {
var rt *route
md, _ := metadata.FromOutgoingContext(rpcInfo.Context)
if extraMD, ok := grpcutil.ExtraMetadata(rpcInfo.Context); ok {
md = metadata.Join(md, extraMD)
// Remove all binary headers. They are hard to match with. May need
// to add back if asked by users.
for k := range md {
if strings.HasSuffix(k, "-bin") {
delete(md, k)
}
}
}
// Loop through routes in order and select first match.
for _, r := range cs.routes {
if r.m.Match(rpcInfo.Method, md) {
rt = &r
break
}
}
if rt == nil || rt.clusters == nil {
return nil, annotateErrorWithNodeID(errNoMatchedRouteFound, cs.xdsNodeID)
}
if rt.actionType != xdsresource.RouteActionRoute {
return nil, annotateErrorWithNodeID(errUnsupportedClientRouteAction, cs.xdsNodeID)
}
rc, ok := rt.clusters.Next().(*grpcsync.RefCounted[*routeCluster])
if !ok {
return nil, annotateErrorWithNodeID(status.Errorf(codes.Internal, "error retrieving cluster for match: %v (%T)", rc, rc), cs.xdsNodeID)
}
cluster := rc.Value()
lbCtx := clustermanager.SetPickedCluster(rpcInfo.Context, cluster.name)
lbCtx = xdsresource.NewContextWithXDSConfig(lbCtx, cs.xdsConfig)
lbCtx = iringhash.SetXDSRequestHash(lbCtx, cs.generateHash(rpcInfo, rt.hashPolicies))
if rt.autoHostRewrite {
lbCtx = clusterimpl.EnableAutoHostRewrite(lbCtx)
}
config := &iresolver.RPCConfig{
Context: lbCtx,
Interceptor: cluster.interceptor,
}
// Add a ref to the selected cluster to keep the interceptors alive until RPC
// is committed.
rc.Increment()
// Add a ref to the selected cluster or plugin, as this RPC needs it until it
// is committed. Releasing the last reference unsubscribes from the cluster
// or pushes a new service config for a plugin.
cluster.info.Increment()
config.OnCommitted = sync.OnceFunc(func() {
cluster.info.Decrement()
// Decrement the refcount of the route cluster and close the interceptor
// if refcount goes to zero.
rc.Decrement()
})
if rt.maxStreamDuration != 0 {
config.MethodConfig.Timeout = &rt.maxStreamDuration
}
if rt.retryConfig != nil {
config.MethodConfig.RetryPolicy = retryConfigToPolicy(rt.retryConfig)
} else if cs.virtualHost.retryConfig != nil {
config.MethodConfig.RetryPolicy = retryConfigToPolicy(cs.virtualHost.retryConfig)
}
return config, nil
}
func retryConfigToPolicy(config *xdsresource.RetryConfig) *serviceconfig.RetryPolicy {
return &serviceconfig.RetryPolicy{
MaxAttempts: int(config.NumRetries) + 1,
InitialBackoff: config.RetryBackoff.BaseInterval,
MaxBackoff: config.RetryBackoff.MaxInterval,
BackoffMultiplier: 2,
RetryableStatusCodes: config.RetryOn,
}
}
func (cs *configSelector) generateHash(rpcInfo iresolver.RPCInfo, hashPolicies []*xdsresource.HashPolicy) uint64 {
var hash uint64
var generatedHash bool
var md, emd metadata.MD
var mdRead bool
for _, policy := range hashPolicies {
var policyHash uint64
var generatedPolicyHash bool
switch policy.HashPolicyType {
case xdsresource.HashPolicyTypeHeader:
if strings.HasSuffix(policy.HeaderName, "-bin") {
continue
}
if !mdRead {
md, _ = metadata.FromOutgoingContext(rpcInfo.Context)
emd, _ = grpcutil.ExtraMetadata(rpcInfo.Context)
mdRead = true
}
values := emd.Get(policy.HeaderName)
if len(values) == 0 {
// Extra metadata (e.g. the "content-type" header) takes
// precedence over the user's metadata.
values = md.Get(policy.HeaderName)
if len(values) == 0 {
// If the header isn't present at all, this policy is a no-op.
continue
}
}
joinedValues := strings.Join(values, ",")
if policy.Regex != nil {
joinedValues = policy.Regex.ReplaceAllString(joinedValues, policy.RegexSubstitution)
}
policyHash = xxhash.Sum64String(joinedValues)
generatedHash = true
generatedPolicyHash = true
case xdsresource.HashPolicyTypeChannelID:
// Use the static channel ID as the hash for this policy.
policyHash = cs.channelID
generatedHash = true
generatedPolicyHash = true
}
// Deterministically combine the hash policies. Rotating prevents
// duplicate hash policies from cancelling each other out and preserves
// the 64 bits of entropy.
if generatedPolicyHash {
hash = bits.RotateLeft64(hash, 1)
hash = hash ^ policyHash
}
// If terminal policy and a hash has already been generated, ignore the
// rest of the policies and use that hash already generated.
if policy.Terminal && generatedHash {
break
}
}
if generatedHash {
return hash
}
// If no generated hash return a random long. In the grand scheme of things
// this logically will map to choosing a random backend to route request to.
return rand.Uint64()
}
// stop decrements refs of all clusters referenced by this config selector.
func (cs *configSelector) stop() {
// The resolver's old configSelector may be nil. Handle that here.
if cs == nil {
return
}
// Decrement the refcount of all the route clusters associated with this
// config selector and close the interceptors of the route cluster if it's
// refcount goes to zero.
for _, r := range cs.routes {
for _, rc := range r.routeClusters {
rc.Decrement()
}
}
// Release this config selector's reference on each cluster and plugin. If
// any reference count drops to zero, the cleanup registered when the entry
// was created removes it from the resolver's active maps and triggers the
// service config update needed to drop it from the channel's config.
for _, ci := range cs.clusters {
ci.Decrement()
}
for _, ci := range cs.plugins {
ci.Decrement()
}
}