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

grpc / grpc-java / #20297

01 Jun 2026 04:04AM UTC coverage: 88.886% (-0.002%) from 88.888%
#20297

push

github

web-flow
xds: Hold parsed service config in CdsUpdate

This avoids re-parsing the config within CdsLB, as the providers could
have changed and the config may no longer be valid.

Many usages of ServiceConfigUtil.unwrapLoadBalancingConfig() were
replaced with public API, which should be less brittle to internal
changes. Similarly, config.equals() was added for LBs least_request,
ring_hash, wrr to use more public APIs in testing. But I've gone out of
my way to avoid using equals for XdsClient change detection, by
preserving the original "JSON" config.

This fixes a bug in WRR config parsing which prevented it from parsing
errorUtilizationPenalty as it assumed it would be a Float, not a Double
like our parser actually generates and our API requires.
JsonUtil.getNumberAsFloat() was added specifically for WRR and has never
worked as JSON Numbers will always be Doubles.

In XdsClusterResource.CdsUpdate, the LB-specific fields like minRingSize
were already not used at all, so this commit deletes them as it seems a
relevant cleanup.

Fixes #12733

36493 of 41056 relevant lines covered (88.89%)

0.89 hits per line

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

83.23
/../core/src/main/java/io/grpc/internal/JsonUtil.java
1
/*
2
 * Copyright 2019 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.math.LongMath.checkedAdd;
20

21
import java.text.ParseException;
22
import java.util.List;
23
import java.util.Locale;
24
import java.util.Map;
25
import java.util.concurrent.TimeUnit;
26
import javax.annotation.Nullable;
27

28
/**
29
 * Helper utility to work with JSON values in Java types. Includes the JSON dialect used by
30
 * Protocol Buffers.
31
 */
32
public class JsonUtil {
1✔
33
  /**
34
   * Gets a list from an object for the given key.  If the key is not present, this returns null.
35
   * If the value is not a List, throws an exception.
36
   */
37
  @Nullable
38
  public static List<?> getList(Map<String, ?> obj, String key) {
39
    assert key != null;
1✔
40
    if (!obj.containsKey(key)) {
1✔
41
      return null;
1✔
42
    }
43
    Object value = obj.get(key);
1✔
44
    if (!(value instanceof List)) {
1✔
45
      throw new ClassCastException(
1✔
46
          String.format("value '%s' for key '%s' in '%s' is not List", value, key, obj));
1✔
47
    }
48
    return (List<?>) value;
1✔
49
  }
50

51
  /**
52
   * Gets a list from an object for the given key, and verifies all entries are objects.  If the key
53
   * is not present, this returns null.  If the value is not a List or an entry is not an object,
54
   * throws an exception.
55
   */
56
  @Nullable
57
  public static List<Map<String, ?>> getListOfObjects(Map<String, ?> obj, String key) {
58
    List<?> list = getList(obj, key);
1✔
59
    if (list == null) {
1✔
60
      return null;
1✔
61
    }
62
    return checkObjectList(list);
1✔
63
  }
64

65
  /**
66
   * Gets a list from an object for the given key, and verifies all entries are strings.  If the key
67
   * is not present, this returns null.  If the value is not a List or an entry is not a string,
68
   * throws an exception.
69
   */
70
  @Nullable
71
  public static List<String> getListOfStrings(Map<String, ?> obj, String key) {
72
    List<?> list = getList(obj, key);
1✔
73
    if (list == null) {
1✔
74
      return null;
1✔
75
    }
76
    return checkStringList(list);
1✔
77
  }
78

79
  /**
80
   * Gets an object from an object for the given key.  If the key is not present, this returns null.
81
   * If the value is not a Map, throws an exception.
82
   */
83
  @SuppressWarnings("unchecked")
84
  @Nullable
85
  public static Map<String, ?> getObject(Map<String, ?> obj, String key) {
86
    assert key != null;
1✔
87
    if (!obj.containsKey(key)) {
1✔
88
      return null;
1✔
89
    }
90
    Object value = obj.get(key);
1✔
91
    if (!(value instanceof Map)) {
1✔
92
      throw new ClassCastException(
1✔
93
          String.format("value '%s' for key '%s' in '%s' is not object", value, key, obj));
1✔
94
    }
95
    return (Map<String, ?>) value;
1✔
96
  }
97

98
  /**
99
   * Gets a number from an object for the given key.  If the key is not present, this returns null.
100
   * If the value does not represent a double, throws an exception.
101
   */
102
  @Nullable
103
  public static Double getNumberAsDouble(Map<String, ?> obj, String key) {
104
    assert key != null;
1✔
105
    if (!obj.containsKey(key)) {
1✔
106
      return null;
1✔
107
    }
108
    Object value = obj.get(key);
1✔
109
    if (value instanceof Double) {
1✔
110
      return (Double) value;
1✔
111
    }
112
    if (value instanceof String) {
1✔
113
      try {
114
        return Double.parseDouble((String) value);
1✔
115
      } catch (NumberFormatException e) {
1✔
116
        throw new IllegalArgumentException(
1✔
117
            String.format("value '%s' for key '%s' is not a double", value, key));
1✔
118
      }
119
    }
120
    throw new IllegalArgumentException(
×
121
        String.format("value '%s' for key '%s' in '%s' is not a number", value, key, obj));
×
122
  }
123

124
  /**
125
   * Gets a number from an object for the given key, casted to an integer.  If the key is not
126
   * present, this returns null.  If the value does not represent an integer, throws an exception.
127
   */
128
  @Nullable
129
  public static Integer getNumberAsInteger(Map<String, ?> obj, String key) {
130
    assert key != null;
1✔
131
    if (!obj.containsKey(key)) {
1✔
132
      return null;
1✔
133
    }
134
    Object value = obj.get(key);
1✔
135
    if (value instanceof Double) {
1✔
136
      Double d = (Double) value;
1✔
137
      int i = d.intValue();
1✔
138
      if (i != d) {
1✔
139
        throw new ClassCastException("Number expected to be integer: " + d);
1✔
140
      }
141
      return i;
1✔
142
    }
143
    if (value instanceof String) {
1✔
144
      try {
145
        return Integer.parseInt((String) value);
1✔
146
      } catch (NumberFormatException e) {
1✔
147
        throw new IllegalArgumentException(
1✔
148
            String.format("value '%s' for key '%s' is not an integer", value, key));
1✔
149
      }
150
    }
151
    throw new IllegalArgumentException(
×
152
        String.format("value '%s' for key '%s' is not an integer", value, key));
×
153
  }
154

155
  /**
156
   * Gets a number from an object for the given key, casted to an long.  If the key is not
157
   * present, this returns null.  If the value does not represent a long integer, throws an
158
   * exception.
159
   */
160
  public static Long getNumberAsLong(Map<String, ?> obj, String key) {
161
    assert key != null;
1✔
162
    if (!obj.containsKey(key)) {
1✔
163
      return null;
1✔
164
    }
165
    Object value = obj.get(key);
1✔
166
    if (value instanceof Double) {
1✔
167
      Double d = (Double) value;
1✔
168
      long l = d.longValue();
1✔
169
      if (l != d) {
1✔
170
        throw new ClassCastException("Number expected to be long: " + d);
1✔
171
      }
172
      return l;
1✔
173
    }
174
    if (value instanceof String) {
1✔
175
      try {
176
        return Long.parseLong((String) value);
1✔
177
      } catch (NumberFormatException e) {
1✔
178
        throw new IllegalArgumentException(
1✔
179
            String.format("value '%s' for key '%s' is not a long integer", value, key));
1✔
180
      }
181
    }
182
    throw new IllegalArgumentException(
×
183
        String.format("value '%s' for key '%s' is not a long integer", value, key));
×
184
  }
185

186
  /**
187
   * Gets a string from an object for the given key.  If the key is not present, this returns null.
188
   * If the value is not a String, throws an exception.
189
   */
190
  @Nullable
191
  public static String getString(Map<String, ?> obj, String key) {
192
    assert key != null;
1✔
193
    if (!obj.containsKey(key)) {
1✔
194
      return null;
1✔
195
    }
196
    Object value = obj.get(key);
1✔
197
    if (!(value instanceof String)) {
1✔
198
      throw new ClassCastException(
1✔
199
          String.format("value '%s' for key '%s' in '%s' is not String", value, key, obj));
1✔
200
    }
201
    return (String) value;
1✔
202
  }
203

204
  /**
205
   * Gets a string from an object for the given key, parsed as a duration (defined by protobuf).  If
206
   * the key is not present, this returns null.  If the value is not a String or not properly
207
   * formatted, throws an exception.
208
   */
209
  public static Long getStringAsDuration(Map<String, ?> obj, String key) {
210
    String value = getString(obj, key);
1✔
211
    if (value == null) {
1✔
212
      return null;
1✔
213
    }
214
    try {
215
      return parseDuration(value);
1✔
216
    } catch (ParseException e) {
1✔
217
      throw new RuntimeException(e);
1✔
218
    }
219
  }
220

221
  /**
222
   * Gets a boolean from an object for the given key.  If the key is not present, this returns null.
223
   * If the value is not a Boolean, throws an exception.
224
   */
225
  @Nullable
226
  public static Boolean getBoolean(Map<String, ?> obj, String key) {
227
    assert key != null;
1✔
228
    if (!obj.containsKey(key)) {
1✔
229
      return null;
1✔
230
    }
231
    Object value = obj.get(key);
1✔
232
    if (!(value instanceof Boolean)) {
1✔
233
      throw new ClassCastException(
×
234
          String.format("value '%s' for key '%s' in '%s' is not Boolean", value, key, obj));
×
235
    }
236
    return (Boolean) value;
1✔
237
  }
238

239
  /**
240
   * Casts a list of unchecked JSON values to a list of checked objects in Java type.
241
   * If the given list contains a value that is not a Map, throws an exception.
242
   */
243
  @SuppressWarnings("unchecked")
244
  public static List<Map<String, ?>> checkObjectList(List<?> rawList) {
245
    for (int i = 0; i < rawList.size(); i++) {
1✔
246
      if (!(rawList.get(i) instanceof Map)) {
1✔
247
        throw new ClassCastException(
1✔
248
            String.format(
1✔
249
                Locale.US, "value %s for idx %d in %s is not object", rawList.get(i), i, rawList));
1✔
250
      }
251
    }
252
    return (List<Map<String, ?>>) rawList;
1✔
253
  }
254

255
  /**
256
   * Casts a list of unchecked JSON values to a list of String. If the given list
257
   * contains a value that is not a String, throws an exception.
258
   */
259
  @SuppressWarnings("unchecked")
260
  public static List<String> checkStringList(List<?> rawList) {
261
    for (int i = 0; i < rawList.size(); i++) {
1✔
262
      if (!(rawList.get(i) instanceof String)) {
1✔
263
        throw new ClassCastException(
×
264
            String.format(
×
265
                Locale.US,
266
                "value '%s' for idx %d in '%s' is not string", rawList.get(i), i, rawList));
×
267
      }
268
    }
269
    return (List<String>) rawList;
1✔
270
  }
271

272
  private static final long DURATION_SECONDS_MIN = -315576000000L;
273
  private static final long DURATION_SECONDS_MAX = 315576000000L;
274

275
  /**
276
   * Parse from a string to produce a duration.  Copy of
277
   * {@link com.google.protobuf.util.Durations#parse}.
278
   *
279
   * @return A Duration parsed from the string.
280
   * @throws ParseException if parsing fails.
281
   */
282
  private static long parseDuration(String value) throws ParseException {
283
    // Must ended with "s".
284
    if (value.isEmpty() || value.charAt(value.length() - 1) != 's') {
1✔
285
      throw new ParseException("Invalid duration string: " + value, 0);
1✔
286
    }
287
    boolean negative = false;
1✔
288
    if (value.charAt(0) == '-') {
1✔
289
      negative = true;
1✔
290
      value = value.substring(1);
1✔
291
    }
292
    String secondValue = value.substring(0, value.length() - 1);
1✔
293
    String nanoValue = "";
1✔
294
    int pointPosition = secondValue.indexOf('.');
1✔
295
    if (pointPosition != -1) {
1✔
296
      nanoValue = secondValue.substring(pointPosition + 1);
1✔
297
      secondValue = secondValue.substring(0, pointPosition);
1✔
298
    }
299
    long seconds = Long.parseLong(secondValue);
1✔
300
    int nanos = nanoValue.isEmpty() ? 0 : parseNanos(nanoValue);
1✔
301
    if (seconds < 0) {
1✔
302
      throw new ParseException("Invalid duration string: " + value, 0);
×
303
    }
304
    if (negative) {
1✔
305
      seconds = -seconds;
1✔
306
      nanos = -nanos;
1✔
307
    }
308
    try {
309
      return normalizedDuration(seconds, nanos);
1✔
310
    } catch (IllegalArgumentException e) {
×
311
      throw new ParseException("Duration value is out of range.", 0);
×
312
    }
313
  }
314

315
  /**
316
   * Copy of {@link com.google.protobuf.util.Timestamps#parseNanos}.
317
   */
318
  private static int parseNanos(String value) throws ParseException {
319
    int result = 0;
1✔
320
    for (int i = 0; i < 9; ++i) {
1✔
321
      result = result * 10;
1✔
322
      if (i < value.length()) {
1✔
323
        if (value.charAt(i) < '0' || value.charAt(i) > '9') {
1✔
324
          throw new ParseException("Invalid nanoseconds.", 0);
×
325
        }
326
        result += value.charAt(i) - '0';
1✔
327
      }
328
    }
329
    return result;
1✔
330
  }
331

332
  private static final int NANOS_PER_SECOND = 1_000_000_000;
333

334
  /**
335
   * Copy of {@link com.google.protobuf.util.Durations#normalizedDuration}.
336
   */
337
  // Math.addExact() requires Android API level 24
338
  @SuppressWarnings({"NarrowingCompoundAssignment", "InlineMeInliner"})
339
  private static long normalizedDuration(long seconds, int nanos) {
340
    if (nanos <= -NANOS_PER_SECOND || nanos >= NANOS_PER_SECOND) {
1✔
341
      seconds = checkedAdd(seconds, nanos / NANOS_PER_SECOND);
×
342
      nanos %= NANOS_PER_SECOND;
×
343
    }
344
    if (seconds > 0 && nanos < 0) {
1✔
345
      nanos += NANOS_PER_SECOND; // no overflow— nanos is negative (and we're adding)
×
346
      seconds--; // no overflow since seconds is positive (and we're decrementing)
×
347
    }
348
    if (seconds < 0 && nanos > 0) {
1✔
349
      nanos -= NANOS_PER_SECOND; // no overflow— nanos is positive (and we're subtracting)
×
350
      seconds++; // no overflow since seconds is negative (and we're incrementing)
×
351
    }
352
    if (!durationIsValid(seconds, nanos)) {
1✔
353
      throw new IllegalArgumentException(String.format(
×
354
          "Duration is not valid. See proto definition for valid values. "
355
              + "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. "
356
              + "Nanos (%s) must be in range [-999,999,999, +999,999,999]. "
357
              + "Nanos must have the same sign as seconds", seconds, nanos));
×
358
    }
359
    return saturatedAdd(TimeUnit.SECONDS.toNanos(seconds), nanos);
1✔
360
  }
361

362
  /**
363
   * Returns true if the given number of seconds and nanos is a valid {@code Duration}. The {@code
364
   * seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos}
365
   * value must be in the range [-999,999,999, +999,999,999].
366
   *
367
   * <p><b>Note:</b> Durations less than one second are represented with a 0 {@code seconds} field
368
   * and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero
369
   * value for the {@code nanos} field must be of the same sign as the {@code seconds} field.
370
   *
371
   * <p>Copy of {@link com.google.protobuf.util.Duration#isValid}.</p>
372
   */
373
  private static boolean durationIsValid(long seconds, int nanos) {
374
    if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) {
1✔
375
      return false;
×
376
    }
377
    if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) {
1✔
378
      return false;
×
379
    }
380
    if (seconds < 0 || nanos < 0) {
1✔
381
      if (seconds > 0 || nanos > 0) {
1✔
382
        return false;
×
383
      }
384
    }
385
    return true;
1✔
386
  }
387

388
  /**
389
   * Returns the sum of {@code a} and {@code b} unless it would overflow or underflow in which case
390
   * {@code Long.MAX_VALUE} or {@code Long.MIN_VALUE} is returned, respectively.
391
   *
392
   * <p>Copy of {@link com.google.common.math.LongMath#saturatedAdd}.</p>
393
   *
394
   */
395
  @SuppressWarnings("ShortCircuitBoolean")
396
  private static long saturatedAdd(long a, long b) {
397
    long naiveSum = a + b;
1✔
398
    if ((a ^ b) < 0 | (a ^ naiveSum) >= 0) {
1✔
399
      // If a and b have different signs or a has the same sign as the result then there was no
400
      // overflow, return.
401
      return naiveSum;
1✔
402
    }
403
    // we did over/under flow, if the sign is negative we should return MAX otherwise MIN
404
    return Long.MAX_VALUE + ((naiveSum >>> (Long.SIZE - 1)) ^ 1);
×
405
  }
406
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc