| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 1 | /* |
| 2 | * |
| 3 | * Copyright 2017 gRPC authors. |
| 4 | * |
| 5 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | * you may not use this file except in compliance with the License. |
| 7 | * You may obtain a copy of the License at |
| 8 | * |
| 9 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | * |
| 11 | * Unless required by applicable law or agreed to in writing, software |
| 12 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | * See the License for the specific language governing permissions and |
| 15 | * limitations under the License. |
| 16 | * |
| 17 | */ |
| 18 | |
| 19 | package grpc |
| 20 | |
| 21 | import ( |
| 22 | "encoding/json" |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 23 | "errors" |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 24 | "fmt" |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 25 | "reflect" |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 26 | "time" |
| 27 | |
| 28 | "google.golang.org/grpc/balancer" |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 29 | "google.golang.org/grpc/balancer/pickfirst" |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 30 | "google.golang.org/grpc/codes" |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 31 | "google.golang.org/grpc/internal" |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 32 | "google.golang.org/grpc/internal/balancer/gracefulswitch" |
| 33 | internalserviceconfig "google.golang.org/grpc/internal/serviceconfig" |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 34 | "google.golang.org/grpc/serviceconfig" |
| 35 | ) |
| 36 | |
| 37 | const maxInt = int(^uint(0) >> 1) |
| 38 | |
| 39 | // MethodConfig defines the configuration recommended by the service providers for a |
| 40 | // particular method. |
| 41 | // |
| 42 | // Deprecated: Users should not use this struct. Service config should be received |
| 43 | // through name resolver, as specified here |
| 44 | // https://github.com/grpc/grpc/blob/master/doc/service_config.md |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 45 | type MethodConfig = internalserviceconfig.MethodConfig |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 46 | |
| 47 | // ServiceConfig is provided by the service provider and contains parameters for how |
| 48 | // clients that connect to the service should behave. |
| 49 | // |
| 50 | // Deprecated: Users should not use this struct. Service config should be received |
| 51 | // through name resolver, as specified here |
| 52 | // https://github.com/grpc/grpc/blob/master/doc/service_config.md |
| 53 | type ServiceConfig struct { |
| 54 | serviceconfig.Config |
| 55 | |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 56 | // lbConfig is the service config's load balancing configuration. If |
| 57 | // lbConfig and LB are both present, lbConfig will be used. |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 58 | lbConfig serviceconfig.LoadBalancingConfig |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 59 | |
| 60 | // Methods contains a map for the methods in this service. If there is an |
| 61 | // exact match for a method (i.e. /service/method) in the map, use the |
| 62 | // corresponding MethodConfig. If there's no exact match, look for the |
| 63 | // default config for the service (/service/) and use the corresponding |
| 64 | // MethodConfig if it exists. Otherwise, the method has no MethodConfig to |
| 65 | // use. |
| 66 | Methods map[string]MethodConfig |
| 67 | |
| 68 | // If a retryThrottlingPolicy is provided, gRPC will automatically throttle |
| 69 | // retry attempts and hedged RPCs when the client’s ratio of failures to |
| 70 | // successes exceeds a threshold. |
| 71 | // |
| 72 | // For each server name, the gRPC client will maintain a token_count which is |
| 73 | // initially set to maxTokens, and can take values between 0 and maxTokens. |
| 74 | // |
| 75 | // Every outgoing RPC (regardless of service or method invoked) will change |
| 76 | // token_count as follows: |
| 77 | // |
| 78 | // - Every failed RPC will decrement the token_count by 1. |
| 79 | // - Every successful RPC will increment the token_count by tokenRatio. |
| 80 | // |
| 81 | // If token_count is less than or equal to maxTokens / 2, then RPCs will not |
| 82 | // be retried and hedged RPCs will not be sent. |
| 83 | retryThrottling *retryThrottlingPolicy |
| 84 | // healthCheckConfig must be set as one of the requirement to enable LB channel |
| 85 | // health check. |
| 86 | healthCheckConfig *healthCheckConfig |
| 87 | // rawJSONString stores service config json string that get parsed into |
| 88 | // this service config struct. |
| 89 | rawJSONString string |
| 90 | } |
| 91 | |
| 92 | // healthCheckConfig defines the go-native version of the LB channel health check config. |
| 93 | type healthCheckConfig struct { |
| 94 | // serviceName is the service name to use in the health-checking request. |
| 95 | ServiceName string |
| 96 | } |
| 97 | |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 98 | type jsonRetryPolicy struct { |
| 99 | MaxAttempts int |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 100 | InitialBackoff internalserviceconfig.Duration |
| 101 | MaxBackoff internalserviceconfig.Duration |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 102 | BackoffMultiplier float64 |
| 103 | RetryableStatusCodes []codes.Code |
| 104 | } |
| 105 | |
| 106 | // retryThrottlingPolicy defines the go-native version of the retry throttling |
| 107 | // policy defined by the service config here: |
| 108 | // https://github.com/grpc/proposal/blob/master/A6-client-retries.md#integration-with-service-config |
| 109 | type retryThrottlingPolicy struct { |
| 110 | // The number of tokens starts at maxTokens. The token_count will always be |
| 111 | // between 0 and maxTokens. |
| 112 | // |
| 113 | // This field is required and must be greater than zero. |
| 114 | MaxTokens float64 |
| 115 | // The amount of tokens to add on each successful RPC. Typically this will |
| 116 | // be some number between 0 and 1, e.g., 0.1. |
| 117 | // |
| 118 | // This field is required and must be greater than zero. Up to 3 decimal |
| 119 | // places are supported. |
| 120 | TokenRatio float64 |
| 121 | } |
| 122 | |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 123 | type jsonName struct { |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 124 | Service string |
| 125 | Method string |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 126 | } |
| 127 | |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 128 | var ( |
| 129 | errDuplicatedName = errors.New("duplicated name") |
| 130 | errEmptyServiceNonEmptyMethod = errors.New("cannot combine empty 'service' and non-empty 'method'") |
| 131 | ) |
| 132 | |
| 133 | func (j jsonName) generatePath() (string, error) { |
| 134 | if j.Service == "" { |
| 135 | if j.Method != "" { |
| 136 | return "", errEmptyServiceNonEmptyMethod |
| 137 | } |
| 138 | return "", nil |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 139 | } |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 140 | res := "/" + j.Service + "/" |
| 141 | if j.Method != "" { |
| 142 | res += j.Method |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 143 | } |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 144 | return res, nil |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 145 | } |
| 146 | |
| 147 | // TODO(lyuxuan): delete this struct after cleaning up old service config implementation. |
| 148 | type jsonMC struct { |
| 149 | Name *[]jsonName |
| 150 | WaitForReady *bool |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 151 | Timeout *internalserviceconfig.Duration |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 152 | MaxRequestMessageBytes *int64 |
| 153 | MaxResponseMessageBytes *int64 |
| 154 | RetryPolicy *jsonRetryPolicy |
| 155 | } |
| 156 | |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 157 | // TODO(lyuxuan): delete this struct after cleaning up old service config implementation. |
| 158 | type jsonSC struct { |
| 159 | LoadBalancingPolicy *string |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 160 | LoadBalancingConfig *json.RawMessage |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 161 | MethodConfig *[]jsonMC |
| 162 | RetryThrottling *retryThrottlingPolicy |
| 163 | HealthCheckConfig *healthCheckConfig |
| 164 | } |
| 165 | |
| 166 | func init() { |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 167 | internal.ParseServiceConfig = func(js string) *serviceconfig.ParseResult { |
| 168 | return parseServiceConfig(js, defaultMaxCallAttempts) |
| 169 | } |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 170 | } |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 171 | |
| 172 | func parseServiceConfig(js string, maxAttempts int) *serviceconfig.ParseResult { |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 173 | if len(js) == 0 { |
| khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 174 | return &serviceconfig.ParseResult{Err: fmt.Errorf("no JSON service config provided")} |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 175 | } |
| 176 | var rsc jsonSC |
| 177 | err := json.Unmarshal([]byte(js), &rsc) |
| 178 | if err != nil { |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 179 | logger.Warningf("grpc: unmarshalling service config %s: %v", js, err) |
| khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 180 | return &serviceconfig.ParseResult{Err: err} |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 181 | } |
| 182 | sc := ServiceConfig{ |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 183 | Methods: make(map[string]MethodConfig), |
| 184 | retryThrottling: rsc.RetryThrottling, |
| 185 | healthCheckConfig: rsc.HealthCheckConfig, |
| 186 | rawJSONString: js, |
| 187 | } |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 188 | c := rsc.LoadBalancingConfig |
| 189 | if c == nil { |
| 190 | name := pickfirst.Name |
| 191 | if rsc.LoadBalancingPolicy != nil { |
| 192 | name = *rsc.LoadBalancingPolicy |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 193 | } |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 194 | if balancer.Get(name) == nil { |
| 195 | name = pickfirst.Name |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 196 | } |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 197 | cfg := []map[string]any{{name: struct{}{}}} |
| 198 | strCfg, err := json.Marshal(cfg) |
| 199 | if err != nil { |
| 200 | return &serviceconfig.ParseResult{Err: fmt.Errorf("unexpected error marshaling simple LB config: %w", err)} |
| 201 | } |
| 202 | r := json.RawMessage(strCfg) |
| 203 | c = &r |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 204 | } |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 205 | cfg, err := gracefulswitch.ParseConfig(*c) |
| 206 | if err != nil { |
| 207 | return &serviceconfig.ParseResult{Err: err} |
| 208 | } |
| 209 | sc.lbConfig = cfg |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 210 | |
| 211 | if rsc.MethodConfig == nil { |
| khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 212 | return &serviceconfig.ParseResult{Config: &sc} |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 213 | } |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 214 | |
| 215 | paths := map[string]struct{}{} |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 216 | for _, m := range *rsc.MethodConfig { |
| 217 | if m.Name == nil { |
| 218 | continue |
| 219 | } |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 220 | |
| 221 | mc := MethodConfig{ |
| 222 | WaitForReady: m.WaitForReady, |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 223 | Timeout: (*time.Duration)(m.Timeout), |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 224 | } |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 225 | if mc.RetryPolicy, err = convertRetryPolicy(m.RetryPolicy, maxAttempts); err != nil { |
| 226 | logger.Warningf("grpc: unmarshalling service config %s: %v", js, err) |
| khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 227 | return &serviceconfig.ParseResult{Err: err} |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 228 | } |
| 229 | if m.MaxRequestMessageBytes != nil { |
| 230 | if *m.MaxRequestMessageBytes > int64(maxInt) { |
| 231 | mc.MaxReqSize = newInt(maxInt) |
| 232 | } else { |
| 233 | mc.MaxReqSize = newInt(int(*m.MaxRequestMessageBytes)) |
| 234 | } |
| 235 | } |
| 236 | if m.MaxResponseMessageBytes != nil { |
| 237 | if *m.MaxResponseMessageBytes > int64(maxInt) { |
| 238 | mc.MaxRespSize = newInt(maxInt) |
| 239 | } else { |
| 240 | mc.MaxRespSize = newInt(int(*m.MaxResponseMessageBytes)) |
| 241 | } |
| 242 | } |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 243 | for i, n := range *m.Name { |
| 244 | path, err := n.generatePath() |
| 245 | if err != nil { |
| 246 | logger.Warningf("grpc: error unmarshalling service config %s due to methodConfig[%d]: %v", js, i, err) |
| 247 | return &serviceconfig.ParseResult{Err: err} |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 248 | } |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 249 | |
| 250 | if _, ok := paths[path]; ok { |
| 251 | err = errDuplicatedName |
| 252 | logger.Warningf("grpc: error unmarshalling service config %s due to methodConfig[%d]: %v", js, i, err) |
| 253 | return &serviceconfig.ParseResult{Err: err} |
| 254 | } |
| 255 | paths[path] = struct{}{} |
| 256 | sc.Methods[path] = mc |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 257 | } |
| 258 | } |
| 259 | |
| 260 | if sc.retryThrottling != nil { |
| 261 | if mt := sc.retryThrottling.MaxTokens; mt <= 0 || mt > 1000 { |
| khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 262 | return &serviceconfig.ParseResult{Err: fmt.Errorf("invalid retry throttling config: maxTokens (%v) out of range (0, 1000]", mt)} |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 263 | } |
| 264 | if tr := sc.retryThrottling.TokenRatio; tr <= 0 { |
| khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 265 | return &serviceconfig.ParseResult{Err: fmt.Errorf("invalid retry throttling config: tokenRatio (%v) may not be negative", tr)} |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 266 | } |
| 267 | } |
| khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 268 | return &serviceconfig.ParseResult{Config: &sc} |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 269 | } |
| 270 | |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 271 | func isValidRetryPolicy(jrp *jsonRetryPolicy) bool { |
| 272 | return jrp.MaxAttempts > 1 && |
| 273 | jrp.InitialBackoff > 0 && |
| 274 | jrp.MaxBackoff > 0 && |
| 275 | jrp.BackoffMultiplier > 0 && |
| 276 | len(jrp.RetryableStatusCodes) > 0 |
| 277 | } |
| 278 | |
| 279 | func convertRetryPolicy(jrp *jsonRetryPolicy, maxAttempts int) (p *internalserviceconfig.RetryPolicy, err error) { |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 280 | if jrp == nil { |
| 281 | return nil, nil |
| 282 | } |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 283 | |
| 284 | if !isValidRetryPolicy(jrp) { |
| 285 | return nil, fmt.Errorf("invalid retry policy (%+v): ", jrp) |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 286 | } |
| 287 | |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 288 | if jrp.MaxAttempts < maxAttempts { |
| 289 | maxAttempts = jrp.MaxAttempts |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 290 | } |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 291 | rp := &internalserviceconfig.RetryPolicy{ |
| 292 | MaxAttempts: maxAttempts, |
| 293 | InitialBackoff: time.Duration(jrp.InitialBackoff), |
| 294 | MaxBackoff: time.Duration(jrp.MaxBackoff), |
| 295 | BackoffMultiplier: jrp.BackoffMultiplier, |
| 296 | RetryableStatusCodes: make(map[codes.Code]bool), |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 297 | } |
| 298 | for _, code := range jrp.RetryableStatusCodes { |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 299 | rp.RetryableStatusCodes[code] = true |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 300 | } |
| 301 | return rp, nil |
| 302 | } |
| 303 | |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 304 | func minPointers(a, b *int) *int { |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 305 | if *a < *b { |
| 306 | return a |
| 307 | } |
| 308 | return b |
| 309 | } |
| 310 | |
| 311 | func getMaxSize(mcMax, doptMax *int, defaultVal int) *int { |
| 312 | if mcMax == nil && doptMax == nil { |
| 313 | return &defaultVal |
| 314 | } |
| 315 | if mcMax != nil && doptMax != nil { |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 316 | return minPointers(mcMax, doptMax) |
| Scott Baker | 2c1c482 | 2019-10-16 11:02:41 -0700 | [diff] [blame] | 317 | } |
| 318 | if mcMax != nil { |
| 319 | return mcMax |
| 320 | } |
| 321 | return doptMax |
| 322 | } |
| 323 | |
| 324 | func newInt(b int) *int { |
| 325 | return &b |
| 326 | } |
| Abhay Kumar | 40252eb | 2025-10-13 13:25:53 +0000 | [diff] [blame] | 327 | |
| 328 | func init() { |
| 329 | internal.EqualServiceConfigForTesting = equalServiceConfig |
| 330 | } |
| 331 | |
| 332 | // equalServiceConfig compares two configs. The rawJSONString field is ignored, |
| 333 | // because they may diff in white spaces. |
| 334 | // |
| 335 | // If any of them is NOT *ServiceConfig, return false. |
| 336 | func equalServiceConfig(a, b serviceconfig.Config) bool { |
| 337 | if a == nil && b == nil { |
| 338 | return true |
| 339 | } |
| 340 | aa, ok := a.(*ServiceConfig) |
| 341 | if !ok { |
| 342 | return false |
| 343 | } |
| 344 | bb, ok := b.(*ServiceConfig) |
| 345 | if !ok { |
| 346 | return false |
| 347 | } |
| 348 | aaRaw := aa.rawJSONString |
| 349 | aa.rawJSONString = "" |
| 350 | bbRaw := bb.rawJSONString |
| 351 | bb.rawJSONString = "" |
| 352 | defer func() { |
| 353 | aa.rawJSONString = aaRaw |
| 354 | bb.rawJSONString = bbRaw |
| 355 | }() |
| 356 | // Using reflect.DeepEqual instead of cmp.Equal because many balancer |
| 357 | // configs are unexported, and cmp.Equal cannot compare unexported fields |
| 358 | // from unexported structs. |
| 359 | return reflect.DeepEqual(aa, bb) |
| 360 | } |