• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #20543

26 Oct 2026 12:31PM UTC coverage: 89.435% (-0.02%) from 89.451%
#20543

push

github

web-flow
core: Reject retryThrottling without maxTokens or tokenRatio clearly (#13131)

## Problem

`ServiceConfigUtil.getThrottlePolicy()` reads the two required fields of
`retryThrottling` with `JsonUtil.getNumberAsDouble(...).floatValue()`.
When
either field is missing, `getNumberAsDouble` returns null and the call
fails
with an NPE:

Cannot invoke "java.lang.Double.floatValue()" because the return value
of
"io.grpc.internal.JsonUtil.getNumberAsDouble(java.util.Map, String)" is
null

`ScParser` catches it and the channel reports `UNKNOWN: failed to parse
service config`, but the cause doesn't say which field is missing. This
was
noted in the code as `TODO(dapengzhang0): check if this is null.`

## Fix

Check both fields with `checkNotNull(..., "<field> cannot be empty")`,
the
same message used for the required `retryPolicy` and `hedgingPolicy`
fields
in `ManagedChannelServiceConfig`. The exception type is still the NPE
that
`ScParser` already handles, so the only change is a clearer message.

## Testing

Added `getThrottlePolicy_failOnMissingMaxTokens` and
`getThrottlePolicy_failOnMissingTokenRatio` to `ServiceConfigUtilTest`.
Both
failed before the fix with the NPE above.

39396 of 44050 relevant lines covered (89.43%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.64
/../core/src/main/java/io/grpc/internal/ServiceConfigUtil.java
1
/*
2
 * Copyright 2018 The gRPC 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 io.grpc.internal;
18

19
import static com.google.common.base.Preconditions.checkNotNull;
20
import static com.google.common.base.Preconditions.checkState;
21
import static com.google.common.base.Verify.verify;
22

23
import com.google.common.annotations.VisibleForTesting;
24
import com.google.common.base.MoreObjects;
25
import com.google.common.base.Objects;
26
import com.google.common.base.VerifyException;
27
import io.grpc.LoadBalancerProvider;
28
import io.grpc.LoadBalancerRegistry;
29
import io.grpc.NameResolver.ConfigOrError;
30
import io.grpc.Status;
31
import io.grpc.internal.RetriableStream.Throttle;
32
import java.util.ArrayList;
33
import java.util.Collections;
34
import java.util.EnumSet;
35
import java.util.List;
36
import java.util.Locale;
37
import java.util.Map;
38
import java.util.Set;
39
import java.util.logging.Level;
40
import java.util.logging.Logger;
41
import javax.annotation.Nullable;
42

43
/**
44
 * Helper utility to work with service configs.
45
 *
46
 * <p>This class contains helper methods to parse service config JSON values into Java types.
47
 */
48
public final class ServiceConfigUtil {
49

50
  private ServiceConfigUtil() {}
51

52
  /**
53
   * Fetches the health-checked service config from service config. {@code null} if can't find one.
54
   */
55
  @Nullable
56
  public static Map<String, ?> getHealthCheckedService(@Nullable  Map<String, ?> serviceConfig) {
57
    if (serviceConfig == null) {
1 ✔
58
      return null;
×
59
    }
60

61
    /* schema as follows
62
    {
63
      "healthCheckConfig": {
64
        // Service name to use in the health-checking request.
65
        "serviceName": string
66
      }
67
    }
68
    */
69
    return JsonUtil.getObject(serviceConfig, "healthCheckConfig");
1 ✔
70
  }
71

72
  /**
73
   * Fetches the health-checked service name from health-checked service config. {@code null} if
74
   * can't find one.
75
   */
76
  @Nullable
77
  public static String getHealthCheckedServiceName(
78
      @Nullable Map<String, ?> healthCheckedServiceConfig) {
79
    if (healthCheckedServiceConfig == null) {
1 ✔
80
      return null;
1 ✔
81
    }
82
    return JsonUtil.getString(healthCheckedServiceConfig, "serviceName");
1 ✔
83
  }
84

85
  @Nullable
86
  static Throttle getThrottlePolicy(@Nullable Map<String, ?> serviceConfig) {
87
    if (serviceConfig == null) {
1 ✔
88
      return null;
×
89
    }
90

91
    /* schema as follows
92
    {
93
      "retryThrottling": {
94
        // The number of tokens starts at maxTokens. The token_count will always be
95
        // between 0 and maxTokens.
96
        //
97
        // This field is required and must be greater than zero.
98
        "maxTokens": number,
99

100
        // The amount of tokens to add on each successful RPC. Typically this will
101
        // be some number between 0 and 1, e.g., 0.1.
102
        //
103
        // This field is required and must be greater than zero. Up to 3 decimal
104
        // places are supported.
105
        "tokenRatio": number
106
      }
107
    }
108
    */
109

110
    Map<String, ?> throttling = JsonUtil.getObject(serviceConfig, "retryThrottling");
1 ✔
111
    if (throttling == null) {
1 ✔
112
      return null;
1 ✔
113
    }
114

115
    float maxTokens = checkNotNull(
1 ✔
116
        JsonUtil.getNumberAsDouble(throttling, "maxTokens"),
1 ✔
117
        "maxTokens cannot be empty").floatValue();
1 ✔
118
    float tokenRatio = checkNotNull(
1 ✔
119
        JsonUtil.getNumberAsDouble(throttling, "tokenRatio"),
1 ✔
120
        "tokenRatio cannot be empty").floatValue();
1 ✔
121
    checkState(maxTokens > 0f, "maxToken should be greater than zero");
1 ✔
122
    checkState(tokenRatio > 0f, "tokenRatio should be greater than zero");
1 ✔
123
    return new Throttle(maxTokens, tokenRatio);
1 ✔
124
  }
125

126
  @Nullable
127
  static Integer getMaxAttemptsFromRetryPolicy(Map<String, ?> retryPolicy) {
128
    return JsonUtil.getNumberAsInteger(retryPolicy, "maxAttempts");
1 ✔
129
  }
130

131
  @Nullable
132
  static Long getInitialBackoffNanosFromRetryPolicy(Map<String, ?> retryPolicy) {
133
    return JsonUtil.getStringAsDuration(retryPolicy, "initialBackoff");
1 ✔
134
  }
135

136
  @Nullable
137
  static Long getMaxBackoffNanosFromRetryPolicy(Map<String, ?> retryPolicy) {
138
    return JsonUtil.getStringAsDuration(retryPolicy, "maxBackoff");
1 ✔
139
  }
140

141
  @Nullable
142
  static Double getBackoffMultiplierFromRetryPolicy(Map<String, ?> retryPolicy) {
143
    return JsonUtil.getNumberAsDouble(retryPolicy, "backoffMultiplier");
1 ✔
144
  }
145

146
  @Nullable
147
  static Long getPerAttemptRecvTimeoutNanosFromRetryPolicy(Map<String, ?> retryPolicy) {
148
    return JsonUtil.getStringAsDuration(retryPolicy, "perAttemptRecvTimeout");
1 ✔
149
  }
150

151
  private static Set<Status.Code> getListOfStatusCodesAsSet(Map<String, ?> obj, String key) {
152
    List<?> statuses = JsonUtil.getList(obj, key);
1 ✔
153
    if (statuses == null) {
1 ✔
154
      return null;
×
155
    }
156
    return getStatusCodesFromList(statuses);
1 ✔
157
  }
158

159
  private static Set<Status.Code> getStatusCodesFromList(List<?> statuses) {
160
    EnumSet<Status.Code> codes = EnumSet.noneOf(Status.Code.class);
1 ✔
161
    for (Object status : statuses) {
1 ✔
162
      Status.Code code;
163
      if (status instanceof Double) {
1 ✔
164
        Double statusD = (Double) status;
×
165
        int codeValue = statusD.intValue();
×
166
        verify((double) codeValue == statusD, "Status code %s is not integral", status);
×
167
        code = Status.fromCodeValue(codeValue).getCode();
×
168
        verify(code.value() == statusD.intValue(), "Status code %s is not valid", status);
×
169
      } else if (status instanceof String) {
1 ✔
170
        try {
171
          code = Status.Code.valueOf((String) status);
1 ✔
172
        } catch (IllegalArgumentException iae) {
×
173
          throw new VerifyException("Status code " + status + " is not valid", iae);
×
174
        }
1 ✔
175
      } else {
176
        throw new VerifyException(
×
177
            "Can not convert status code " + status + " to Status.Code, because its type is "
178
                + status.getClass());
×
179
      }
180
      codes.add(code);
1 ✔
181
    }
1 ✔
182
    return Collections.unmodifiableSet(codes);
1 ✔
183
  }
184

185
  static Set<Status.Code> getRetryableStatusCodesFromRetryPolicy(Map<String, ?> retryPolicy) {
186
    String retryableStatusCodesKey = "retryableStatusCodes";
1 ✔
187
    Set<Status.Code> codes = getListOfStatusCodesAsSet(retryPolicy, retryableStatusCodesKey);
1 ✔
188
    verify(codes != null, "%s is required in retry policy", retryableStatusCodesKey);
1 ✔
189
    verify(!codes.contains(Status.Code.OK), "%s must not contain OK", retryableStatusCodesKey);
1 ✔
190
    return codes;
1 ✔
191
  }
192

193
  @Nullable
194
  static Integer getMaxAttemptsFromHedgingPolicy(Map<String, ?> hedgingPolicy) {
195
    return JsonUtil.getNumberAsInteger(hedgingPolicy, "maxAttempts");
1 ✔
196
  }
197

198
  @Nullable
199
  static Long getHedgingDelayNanosFromHedgingPolicy(Map<String, ?> hedgingPolicy) {
200
    return JsonUtil.getStringAsDuration(hedgingPolicy, "hedgingDelay");
1 ✔
201
  }
202

203
  static Set<Status.Code> getNonFatalStatusCodesFromHedgingPolicy(Map<String, ?> hedgingPolicy) {
204
    String nonFatalStatusCodesKey = "nonFatalStatusCodes";
1 ✔
205
    Set<Status.Code> codes = getListOfStatusCodesAsSet(hedgingPolicy, nonFatalStatusCodesKey);
1 ✔
206
    if (codes == null) {
1 ✔
207
      return Collections.unmodifiableSet(EnumSet.noneOf(Status.Code.class));
×
208
    }
209
    verify(!codes.contains(Status.Code.OK), "%s must not contain OK", nonFatalStatusCodesKey);
1 ✔
210
    return codes;
1 ✔
211
  }
212

213
  @Nullable
214
  static String getServiceFromName(Map<String, ?> name) {
215
    return JsonUtil.getString(name, "service");
1 ✔
216
  }
217

218
  @Nullable
219
  static String getMethodFromName(Map<String, ?> name) {
220
    return JsonUtil.getString(name, "method");
1 ✔
221
  }
222

223
  @Nullable
224
  static Map<String, ?> getRetryPolicyFromMethodConfig(Map<String, ?> methodConfig) {
225
    return JsonUtil.getObject(methodConfig, "retryPolicy");
1 ✔
226
  }
227

228
  @Nullable
229
  static Map<String, ?> getHedgingPolicyFromMethodConfig(Map<String, ?> methodConfig) {
230
    return JsonUtil.getObject(methodConfig, "hedgingPolicy");
1 ✔
231
  }
232

233
  @Nullable
234
  static List<Map<String, ?>> getNameListFromMethodConfig(
235
      Map<String, ?> methodConfig) {
236
    return JsonUtil.getListOfObjects(methodConfig, "name");
1 ✔
237
  }
238

239
  /**
240
   * Returns the number of nanoseconds of timeout for the given method config.
241
   *
242
   * @return duration nanoseconds, or {@code null} if it isn't present.
243
   */
244
  @Nullable
245
  static Long getTimeoutFromMethodConfig(Map<String, ?> methodConfig) {
246
    return JsonUtil.getStringAsDuration(methodConfig, "timeout");
1 ✔
247
  }
248

249
  @Nullable
250
  static Boolean getWaitForReadyFromMethodConfig(Map<String, ?> methodConfig) {
251
    return JsonUtil.getBoolean(methodConfig, "waitForReady");
1 ✔
252
  }
253

254
  @Nullable
255
  static Integer getMaxRequestMessageBytesFromMethodConfig(Map<String, ?> methodConfig) {
256
    return JsonUtil.getNumberAsInteger(methodConfig, "maxRequestMessageBytes");
1 ✔
257
  }
258

259
  @Nullable
260
  static Integer getMaxResponseMessageBytesFromMethodConfig(Map<String, ?> methodConfig) {
261
    return JsonUtil.getNumberAsInteger(methodConfig, "maxResponseMessageBytes");
1 ✔
262
  }
263

264
  @Nullable
265
  static List<Map<String, ?>> getMethodConfigFromServiceConfig(
266
      Map<String, ?> serviceConfig) {
267
    return JsonUtil.getListOfObjects(serviceConfig, "methodConfig");
1 ✔
268
  }
269

270
  /**
271
   * Extracts load balancing configs from a service config.
272
   */
273
  @VisibleForTesting
274
  public static List<Map<String, ?>> getLoadBalancingConfigsFromServiceConfig(
275
      Map<String, ?> serviceConfig) {
276
    /* schema as follows
277
    {
278
      "loadBalancingConfig": [
279
        {"xds" :
280
          {
281
            "childPolicy": [...],
282
            "fallbackPolicy": [...],
283
          }
284
        },
285
        {"round_robin": {}}
286
      ],
287
      "loadBalancingPolicy": "ROUND_ROBIN"  // The deprecated policy key
288
    }
289
    */
290
    List<Map<String, ?>> lbConfigs = new ArrayList<>();
1 ✔
291
    String loadBalancingConfigKey = "loadBalancingConfig";
1 ✔
292
    if (serviceConfig.containsKey(loadBalancingConfigKey)) {
1 ✔
293
      lbConfigs.addAll(JsonUtil.getListOfObjects(
1 ✔
294
          serviceConfig, loadBalancingConfigKey));
295
    }
296
    if (lbConfigs.isEmpty()) {
1 ✔
297
      // No LoadBalancingConfig found.  Fall back to the deprecated LoadBalancingPolicy
298
      String policy = JsonUtil.getString(serviceConfig, "loadBalancingPolicy");
1 ✔
299
      if (policy != null) {
1 ✔
300
        // Convert the policy to a config, so that the caller can handle them in the same way.
301
        policy = policy.toLowerCase(Locale.ROOT);
1 ✔
302
        Map<String, ?> fakeConfig = Collections.singletonMap(policy, Collections.emptyMap());
1 ✔
303
        lbConfigs.add(fakeConfig);
1 ✔
304
      }
305
    }
306
    return Collections.unmodifiableList(lbConfigs);
1 ✔
307
  }
308

309
  /**
310
   * Unwrap a LoadBalancingConfig JSON object into a {@link LbConfig}.  The input is a JSON object
311
   * (map) with exactly one entry, where the key is the policy name and the value is a config object
312
   * for that policy.
313
   */
314
  public static LbConfig unwrapLoadBalancingConfig(Map<String, ?> lbConfig) {
315
    if (lbConfig.size() != 1) {
1 ✔
316
      throw new RuntimeException(
1 ✔
317
          "There are " + lbConfig.size() + " fields in a LoadBalancingConfig object. Exactly one"
1 ✔
318
          + " is expected. Config=" + lbConfig);
319
    }
320
    String key = lbConfig.entrySet().iterator().next().getKey();
1 ✔
321
    return new LbConfig(key, JsonUtil.getObject(lbConfig, key));
1 ✔
322
  }
323

324
  /**
325
   * Given a JSON list of LoadBalancingConfigs, and convert it into a list of LbConfig.
326
   */
327
  public static List<LbConfig> unwrapLoadBalancingConfigList(List<Map<String, ?>> list) {
328
    if (list == null) {
1 ✔
329
      return null;
1 ✔
330
    }
331
    ArrayList<LbConfig> result = new ArrayList<>();
1 ✔
332
    for (Map<String, ?> rawChildPolicy : list) {
1 ✔
333
      result.add(unwrapLoadBalancingConfig(rawChildPolicy));
1 ✔
334
    }
1 ✔
335
    return Collections.unmodifiableList(result);
1 ✔
336
  }
337

338
  /**
339
   * Parses and selects a load balancing policy from a non-empty list of raw configs. If selection
340
   * is successful, the returned ConfigOrError object will include a {@link
341
   * ServiceConfigUtil.PolicySelection} as its config value.
342
   */
343
  public static ConfigOrError selectLbPolicyFromList(
344
      List<LbConfig> lbConfigs, LoadBalancerRegistry lbRegistry) {
345
    List<String> policiesTried = new ArrayList<>();
1 ✔
346
    for (LbConfig lbConfig : lbConfigs) {
1 ✔
347
      String policy = lbConfig.getPolicyName();
1 ✔
348
      LoadBalancerProvider provider = lbRegistry.getProvider(policy);
1 ✔
349
      if (provider == null) {
1 ✔
350
        policiesTried.add(policy);
1 ✔
351
      } else {
352
        if (!policiesTried.isEmpty()) {
1 ✔
353
          Logger.getLogger(ServiceConfigUtil.class.getName()).log(
1 ✔
354
              Level.FINEST,
355
              "{0} specified by Service Config are not available", policiesTried);
356
        }
357
        ConfigOrError parsedLbPolicyConfig =
1 ✔
358
            provider.parseLoadBalancingPolicyConfig(lbConfig.getRawConfigValue());
1 ✔
359
        if (parsedLbPolicyConfig.getError() != null) {
1 ✔
360
          return parsedLbPolicyConfig;
1 ✔
361
        }
362
        return ConfigOrError.fromConfig(
1 ✔
363
            new PolicySelection(provider, parsedLbPolicyConfig.getConfig()));
1 ✔
364
      }
365
    }
1 ✔
366
    return ConfigOrError.fromError(
1 ✔
367
        Status.UNKNOWN.withDescription(
1 ✔
368
            "None of " + policiesTried + " specified by Service Config are available."));
369
  }
370

371
  /**
372
   * A LoadBalancingConfig that includes the policy name (the key) and its raw config value (parsed
373
   * JSON).
374
   */
375
  public static final class LbConfig {
376
    private final String policyName;
377
    private final Map<String, ?> rawConfigValue;
378

379
    public LbConfig(String policyName, Map<String, ?> rawConfigValue) {
1 ✔
380
      this.policyName = checkNotNull(policyName, "policyName");
1 ✔
381
      this.rawConfigValue = checkNotNull(rawConfigValue, "rawConfigValue");
1 ✔
382
    }
1 ✔
383

384
    public String getPolicyName() {
385
      return policyName;
1 ✔
386
    }
387

388
    public Map<String, ?> getRawConfigValue() {
389
      return rawConfigValue;
1 ✔
390
    }
391

392
    @Override
393
    public boolean equals(Object o) {
394
      if (o instanceof LbConfig) {
1 ✔
395
        LbConfig other = (LbConfig) o;
1 ✔
396
        return policyName.equals(other.policyName)
1 ✔
397
            && rawConfigValue.equals(other.rawConfigValue);
1 ✔
398
      }
399
      return false;
×
400
    }
401

402
    @Override
403
    public int hashCode() {
404
      return Objects.hashCode(policyName, rawConfigValue);
×
405
    }
406

407
    @Override
408
    public String toString() {
409
      return MoreObjects.toStringHelper(this)
×
410
          .add("policyName", policyName)
×
411
          .add("rawConfigValue", rawConfigValue)
×
412
          .toString();
×
413
    }
414
  }
415

416
  public static final class PolicySelection {
417
    final LoadBalancerProvider provider;
418
    @Nullable
419
    final Object config;
420

421
    /** Constructs a PolicySelection with selected LB provider and the deeply parsed LB config. */
422
    public PolicySelection(
423
        LoadBalancerProvider provider,
424
        @Nullable Object config) {
1 ✔
425
      this.provider = checkNotNull(provider, "provider");
1 ✔
426
      this.config = config;
1 ✔
427
    }
1 ✔
428

429
    public LoadBalancerProvider getProvider() {
430
      return provider;
1 ✔
431
    }
432

433
    @Nullable
434
    public Object getConfig() {
435
      return config;
1 ✔
436
    }
437

438
    @Override
439
    public boolean equals(Object o) {
440
      if (this == o) {
1 ✔
441
        return true;
×
442
      }
443
      if (o == null || getClass() != o.getClass()) {
1 ✔
444
        return false;
1 ✔
445
      }
446
      PolicySelection that = (PolicySelection) o;
1 ✔
447
      return Objects.equal(provider, that.provider)
1 ✔
448
          && Objects.equal(config, that.config);
1 ✔
449
    }
450

451
    @Override
452
    public int hashCode() {
453
      return Objects.hashCode(provider, config);
×
454
    }
455

456
    @Override
457
    public String toString() {
458
      return MoreObjects.toStringHelper(this)
×
459
          .add("provider", provider)
×
460
          .add("config", config)
×
461
          .toString();
×
462
    }
463
  }
464
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc