Skip to content

Commit 3db3235

Browse files
authored
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 grpc#12733
1 parent 0b62dce commit 3db3235

14 files changed

Lines changed: 188 additions & 231 deletions

core/src/main/java/io/grpc/internal/JsonUtil.java

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -121,33 +121,6 @@ public static Double getNumberAsDouble(Map<String, ?> obj, String key) {
121121
String.format("value '%s' for key '%s' in '%s' is not a number", value, key, obj));
122122
}
123123

124-
/**
125-
* Gets a number from an object for the given key. If the key is not present, this returns null.
126-
* If the value does not represent a float, throws an exception.
127-
*/
128-
@Nullable
129-
public static Float getNumberAsFloat(Map<String, ?> obj, String key) {
130-
assert key != null;
131-
if (!obj.containsKey(key)) {
132-
return null;
133-
}
134-
Object value = obj.get(key);
135-
if (value instanceof Float) {
136-
return (Float) value;
137-
}
138-
if (value instanceof String) {
139-
try {
140-
return Float.parseFloat((String) value);
141-
} catch (NumberFormatException e) {
142-
throw new IllegalArgumentException(
143-
String.format("string value '%s' for key '%s' cannot be parsed as a float", value,
144-
key));
145-
}
146-
}
147-
throw new IllegalArgumentException(
148-
String.format("value %s for key '%s' is not a float", value, key));
149-
}
150-
151124
/**
152125
* Gets a number from an object for the given key, casted to an integer. If the key is not
153126
* present, this returns null. If the value does not represent an integer, throws an exception.

core/src/test/java/io/grpc/internal/JsonUtilTest.java

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ public void getNumber() {
5050
assertThat(JsonUtil.getNumberAsLong(map, "key_number_1")).isEqualTo(1L);
5151

5252
assertThat(JsonUtil.getNumberAsDouble(map, "key_string_2.0")).isEqualTo(2D);
53-
assertThat(JsonUtil.getNumberAsFloat(map, "key_string_2.0")).isEqualTo(2F);
5453
try {
5554
JsonUtil.getNumberAsInteger(map, "key_string_2.0");
5655
fail("expecting to throw but did not");
@@ -69,10 +68,8 @@ public void getNumber() {
6968
assertThat(JsonUtil.getNumberAsDouble(map, "key_string_3")).isEqualTo(3D);
7069
assertThat(JsonUtil.getNumberAsInteger(map, "key_string_3")).isEqualTo(3);
7170
assertThat(JsonUtil.getNumberAsLong(map, "key_string_3")).isEqualTo(3L);
72-
assertThat(JsonUtil.getNumberAsFloat(map, "key_string_3")).isEqualTo(3F);
7371

7472
assertThat(JsonUtil.getNumberAsDouble(map, "key_string_nan")).isNaN();
75-
assertThat(JsonUtil.getNumberAsFloat(map, "key_string_nan")).isNaN();
7673
try {
7774
JsonUtil.getNumberAsInteger(map, "key_string_nan");
7875
fail("expecting to throw but did not");
@@ -123,41 +120,18 @@ public void getNumber() {
123120
assertThat(e).hasMessageThat().isEqualTo(
124121
"value 'six' for key 'key_string_six' is not a long integer");
125122
}
126-
try {
127-
JsonUtil.getNumberAsFloat(map, "key_string_six");
128-
fail("expecting to throw but did not");
129-
} catch (RuntimeException e) {
130-
assertThat(e).hasMessageThat().isEqualTo(
131-
"string value 'six' for key 'key_string_six' cannot be parsed as a float");
132-
}
133-
134-
assertThat(JsonUtil.getNumberAsFloat(map, "key_number_7")).isEqualTo(7F);
135123

136124
assertThat(JsonUtil.getNumberAsDouble(map, "key_string_infinity")).isPositiveInfinity();
137125
assertThat(JsonUtil.getNumberAsDouble(map, "key_string_minus_infinity")).isNegativeInfinity();
138126
assertThat(JsonUtil.getNumberAsDouble(map, "key_string_exponent")).isEqualTo(2.998e8D);
139127

140-
assertThat(JsonUtil.getNumberAsFloat(map, "key_string_infinity")).isPositiveInfinity();
141-
assertThat(JsonUtil.getNumberAsFloat(map, "key_string_minus_infinity")).isNegativeInfinity();
142-
assertThat(JsonUtil.getNumberAsFloat(map, "key_string_exponent")).isEqualTo(2.998e8F);
143-
144128
assertThat(JsonUtil.getNumberAsDouble(map, "key_string_minus_zero")).isZero();
145129
assertThat(JsonUtil.getNumberAsInteger(map, "key_string_minus_zero")).isEqualTo(0);
146130
assertThat(JsonUtil.getNumberAsLong(map, "key_string_minus_zero")).isEqualTo(0L);
147-
assertThat(JsonUtil.getNumberAsFloat(map, "key_string_minus_zero")).isZero();
148131

149132
assertThat(JsonUtil.getNumberAsDouble(map, "key_nonexistent")).isNull();
150133
assertThat(JsonUtil.getNumberAsInteger(map, "key_nonexistent")).isNull();
151134
assertThat(JsonUtil.getNumberAsLong(map, "key_nonexistent")).isNull();
152-
assertThat(JsonUtil.getNumberAsFloat(map, "key_nonexistent")).isNull();
153-
154-
try {
155-
JsonUtil.getNumberAsFloat(map, "key_string_boolean");
156-
fail("expecting to throw but did not");
157-
} catch (RuntimeException e) {
158-
assertThat(e).hasMessageThat().isEqualTo(
159-
"value true for key 'key_string_boolean' is not a float");
160-
}
161135
}
162136

163137
@Test

xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import io.grpc.LoadBalancer;
3333
import io.grpc.LoadBalancerProvider;
3434
import io.grpc.LoadBalancerRegistry;
35-
import io.grpc.NameResolver;
3635
import io.grpc.Status;
3736
import io.grpc.StatusOr;
3837
import io.grpc.internal.GrpcUtil;
@@ -126,24 +125,12 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
126125
}
127126
XdsClusterConfig clusterConfig = clusterConfigOr.getValue();
128127

129-
NameResolver.ConfigOrError configOrError;
130128
if (clusterConfig.getChildren() instanceof EndpointConfig) {
131-
// The LB policy config is provided in service_config.proto/JSON format.
132-
configOrError =
133-
GracefulSwitchLoadBalancer.parseLoadBalancingPolicyConfig(
134-
Arrays.asList(clusterConfig.getClusterResource().lbPolicyConfig()),
135-
lbRegistry);
136-
if (configOrError.getError() != null) {
137-
// Should be impossible, because XdsClusterResource validated this
138-
return fail(Status.INTERNAL.withDescription(
139-
errorPrefix() + "Unable to parse the LB config: " + configOrError.getError()));
140-
}
141-
142129
StatusOr<EdsUpdate> edsUpdate = getEdsUpdate(xdsConfig, clusterName);
143130
StatusOr<ClusterResolutionResult> statusOrResult = clusterState.edsUpdateToResult(
144131
clusterName,
145132
clusterConfig.getClusterResource(),
146-
configOrError.getConfig(),
133+
clusterConfig.getClusterResource().lbPolicyConfig(),
147134
edsUpdate);
148135
if (!statusOrResult.hasValue()) {
149136
Status status = Status.UNAVAILABLE

xds/src/main/java/io/grpc/xds/LeastRequestLoadBalancer.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,20 @@ static final class LeastRequestConfig {
299299
this.choiceCount = Math.min(choiceCount, MAX_CHOICE_COUNT);
300300
}
301301

302+
@Override
303+
public boolean equals(Object o) {
304+
if (!(o instanceof LeastRequestConfig)) {
305+
return false;
306+
}
307+
LeastRequestConfig that = (LeastRequestConfig) o;
308+
return this.choiceCount == that.choiceCount;
309+
}
310+
311+
@Override
312+
public int hashCode() {
313+
return choiceCount;
314+
}
315+
302316
@Override
303317
public String toString() {
304318
return MoreObjects.toStringHelper(this)

xds/src/main/java/io/grpc/xds/LoadBalancerConfigFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ class LoadBalancerConfigFactory {
155155
configBuilder.put(WEIGHT_UPDATE_PERIOD, weightUpdatePeriod);
156156
}
157157
if (errorUtilizationPenalty != null) {
158-
configBuilder.put(ERROR_UTILIZATION_PENALTY, errorUtilizationPenalty);
158+
configBuilder.put(ERROR_UTILIZATION_PENALTY, errorUtilizationPenalty.doubleValue());
159159
}
160160
if (metricNamesForComputingUtilization != null
161161
&& !metricNamesForComputingUtilization.isEmpty()) {

xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,19 @@ public int hashCode() {
828828
parsedMetricNamesForComputingUtilization);
829829
}
830830

831+
@Override
832+
public String toString() {
833+
return MoreObjects.toStringHelper(this)
834+
.add("blackoutPeriodNanos", blackoutPeriodNanos)
835+
.add("weightExpirationPeriodNanos", weightExpirationPeriodNanos)
836+
.add("enableOobLoadReport", enableOobLoadReport)
837+
.add("oobReportingPeriodNanos", oobReportingPeriodNanos)
838+
.add("weightUpdatePeriodNanos", weightUpdatePeriodNanos)
839+
.add("errorUtilizationPenalty", errorUtilizationPenalty)
840+
.add("parsedMetricNamesForComputingUtilization", parsedMetricNamesForComputingUtilization)
841+
.toString();
842+
}
843+
831844
static final class Builder {
832845
long blackoutPeriodNanos = 10_000_000_000L; // 10s
833846
long weightExpirationPeriodNanos = 180_000_000_000L; // 3min

xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ private ConfigOrError parseLoadBalancingPolicyConfigInternal(Map<String, ?> rawC
7979
Long oobReportingPeriodNanos = JsonUtil.getStringAsDuration(rawConfig, "oobReportingPeriod");
8080
Boolean enableOobLoadReport = JsonUtil.getBoolean(rawConfig, "enableOobLoadReport");
8181
Long weightUpdatePeriodNanos = JsonUtil.getStringAsDuration(rawConfig, "weightUpdatePeriod");
82-
Float errorUtilizationPenalty = JsonUtil.getNumberAsFloat(rawConfig, "errorUtilizationPenalty");
82+
Double errorUtilizationPenalty =
83+
JsonUtil.getNumberAsDouble(rawConfig, "errorUtilizationPenalty");
8384
List<String> metricNamesForComputingUtilization = JsonUtil.getListOfStrings(rawConfig,
8485
"metricNamesForComputingUtilization");
8586

@@ -104,7 +105,7 @@ private ConfigOrError parseLoadBalancingPolicyConfigInternal(Map<String, ?> rawC
104105
}
105106
}
106107
if (errorUtilizationPenalty != null) {
107-
configBuilder.setErrorUtilizationPenalty(errorUtilizationPenalty);
108+
configBuilder.setErrorUtilizationPenalty(errorUtilizationPenalty.floatValue());
108109
}
109110
if (metricNamesForComputingUtilization != null
110111
&& GrpcUtil.getFlag("GRPC_EXPERIMENTAL_WRR_CUSTOM_METRICS", false)) {

xds/src/main/java/io/grpc/xds/XdsClusterResource.java

Lines changed: 73 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@
4343
import io.grpc.LoadBalancerRegistry;
4444
import io.grpc.NameResolver;
4545
import io.grpc.internal.GrpcUtil;
46-
import io.grpc.internal.ServiceConfigUtil;
47-
import io.grpc.internal.ServiceConfigUtil.LbConfig;
46+
import io.grpc.util.GracefulSwitchLoadBalancer;
4847
import io.grpc.xds.EnvoyServerProtoData.OutlierDetection;
4948
import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext;
5049
import io.grpc.xds.XdsClusterResource.CdsUpdate;
@@ -165,18 +164,16 @@ static CdsUpdate processCluster(Cluster cluster,
165164
ImmutableMap<String, ?> lbPolicyConfig = LoadBalancerConfigFactory.newConfig(cluster,
166165
enableLeastRequest);
167166

168-
// Validate the LB config by trying to parse it with the corresponding LB provider.
169-
LbConfig lbConfig = ServiceConfigUtil.unwrapLoadBalancingConfig(lbPolicyConfig);
170-
NameResolver.ConfigOrError configOrError = loadBalancerRegistry.getProvider(
171-
lbConfig.getPolicyName()).parseLoadBalancingPolicyConfig(
172-
lbConfig.getRawConfigValue());
167+
NameResolver.ConfigOrError configOrError
168+
= GracefulSwitchLoadBalancer.parseLoadBalancingPolicyConfig(
169+
ImmutableList.of(lbPolicyConfig), loadBalancerRegistry);
173170
if (configOrError.getError() != null) {
174171
throw new ResourceInvalidException(
175172
"Failed to parse lb config for cluster '" + cluster.getName() + "': "
176173
+ configOrError.getError());
177174
}
178175

179-
updateBuilder.lbPolicyConfig(lbPolicyConfig);
176+
updateBuilder.lbPolicyConfig(configOrError.getConfig(), lbPolicyConfig);
180177
updateBuilder.filterMetadata(
181178
ImmutableMap.copyOf(cluster.getMetadata().getFilterMetadataMap()));
182179

@@ -582,16 +579,22 @@ abstract static class CdsUpdate implements ResourceUpdate {
582579

583580
abstract ClusterType clusterType();
584581

585-
abstract ImmutableMap<String, ?> lbPolicyConfig();
586-
587-
// Only valid if lbPolicy is "ring_hash_experimental".
588-
abstract long minRingSize();
582+
/** Graceful switch configuration. */
583+
Object lbPolicyConfig() {
584+
return internalLbPolicyConfig().getValue();
585+
}
589586

590-
// Only valid if lbPolicy is "ring_hash_experimental".
591-
abstract long maxRingSize();
587+
/**
588+
* Use {@link #lbPolicyConfig()} instead. This avoids using the LB policy configs' equals() when
589+
* XdsClient squelches config updates that are identical to the current value. LB policies are
590+
* not required to implement equals for their configs. Instead, {link
591+
* #internalLbPolicyConfigJson()} is used to detect changes.
592+
*/
593+
abstract IgnoreEquals<Object> internalLbPolicyConfig();
592594

593-
// Only valid if lbPolicy is "least_request_experimental".
594-
abstract int choiceCount();
595+
/** Use {@code lbPolicyConfig} instead. */
596+
@Nullable
597+
abstract ImmutableMap<String, ?> internalLbPolicyConfigJson();
595598

596599
// Alternative resource name to be used in EDS requests.
597600
/// Only valid for EDS cluster.
@@ -640,9 +643,6 @@ abstract static class CdsUpdate implements ResourceUpdate {
640643
private static Builder newBuilder(String clusterName) {
641644
return new AutoValue_XdsClusterResource_CdsUpdate.Builder()
642645
.clusterName(clusterName)
643-
.minRingSize(0)
644-
.maxRingSize(0)
645-
.choiceCount(0)
646646
.filterMetadata(ImmutableMap.of())
647647
.parsedMetadata(ImmutableMap.of())
648648
.isHttp11ProxyAvailable(false)
@@ -703,10 +703,7 @@ public final String toString() {
703703
return MoreObjects.toStringHelper(this)
704704
.add("clusterName", clusterName())
705705
.add("clusterType", clusterType())
706-
.add("lbPolicyConfig", lbPolicyConfig())
707-
.add("minRingSize", minRingSize())
708-
.add("maxRingSize", maxRingSize())
709-
.add("choiceCount", choiceCount())
706+
.add("lbPolicyConfigJson", internalLbPolicyConfigJson())
710707
.add("edsServiceName", edsServiceName())
711708
.add("dnsHostName", dnsHostName())
712709
.add("lrsServerInfo", lrsServerInfo())
@@ -725,31 +722,31 @@ abstract static class Builder {
725722
// Private, use one of the static factory methods instead.
726723
protected abstract Builder clusterType(ClusterType clusterType);
727724

728-
protected abstract Builder lbPolicyConfig(ImmutableMap<String, ?> lbPolicyConfig);
729-
730-
Builder roundRobinLbPolicy() {
731-
return this.lbPolicyConfig(ImmutableMap.of("round_robin", ImmutableMap.of()));
732-
}
733-
734-
Builder ringHashLbPolicy(Long minRingSize, Long maxRingSize) {
735-
return this.lbPolicyConfig(ImmutableMap.of("ring_hash_experimental",
736-
ImmutableMap.of("minRingSize", minRingSize.doubleValue(), "maxRingSize",
737-
maxRingSize.doubleValue())));
738-
}
739-
740-
Builder leastRequestLbPolicy(Integer choiceCount) {
741-
return this.lbPolicyConfig(ImmutableMap.of("least_request_experimental",
742-
ImmutableMap.of("choiceCount", choiceCount.doubleValue())));
725+
/**
726+
* The config to use, and the JSON representation that produced that config. The JSON
727+
* representation is only used to detect if the configuration changed, since LB policies don't
728+
* have to implement equals() for their parsed configs.
729+
*/
730+
protected Builder lbPolicyConfig(
731+
Object gracefulSwitchConfig, ImmutableMap<String, ?> jsonConfig) {
732+
return internalLbPolicyConfig(new IgnoreEquals<>(gracefulSwitchConfig))
733+
.internalLbPolicyConfigJson(jsonConfig);
734+
}
735+
736+
protected Builder lbPolicyConfigJsonForTesting(ImmutableMap<String, ?> jsonConfig) {
737+
NameResolver.ConfigOrError result =
738+
GracefulSwitchLoadBalancer.parseLoadBalancingPolicyConfig(
739+
ImmutableList.of(jsonConfig));
740+
if (result.getError() != null) {
741+
throw new IllegalArgumentException(
742+
"Bad JSON config: " + result.getError() + " json: " + jsonConfig);
743+
}
744+
return lbPolicyConfig(result.getConfig(), jsonConfig);
743745
}
744746

745-
// Private, use leastRequestLbPolicy(int).
746-
protected abstract Builder choiceCount(int choiceCount);
747-
748-
// Private, use ringHashLbPolicy(long, long).
749-
protected abstract Builder minRingSize(long minRingSize);
747+
protected abstract Builder internalLbPolicyConfig(IgnoreEquals<Object> gracefulSwitchConfig);
750748

751-
// Private, use ringHashLbPolicy(long, long).
752-
protected abstract Builder maxRingSize(long maxRingSize);
749+
protected abstract Builder internalLbPolicyConfigJson(ImmutableMap<String, ?> jsonConfig);
753750

754751
// Private, use CdsUpdate.forEds() instead.
755752
protected abstract Builder edsServiceName(String edsServiceName);
@@ -783,4 +780,35 @@ protected abstract Builder backendMetricPropagation(
783780
abstract CdsUpdate build();
784781
}
785782
}
783+
784+
/** Always equal to this same type. */
785+
static final class IgnoreEquals<V> {
786+
private final V value;
787+
788+
IgnoreEquals(V value) {
789+
this.value = value;
790+
}
791+
792+
public V getValue() {
793+
return value;
794+
}
795+
796+
@Override
797+
public boolean equals(Object o) {
798+
if (!(o instanceof IgnoreEquals)) {
799+
return false;
800+
}
801+
return true;
802+
}
803+
804+
@Override
805+
public int hashCode() {
806+
return 0;
807+
}
808+
809+
@Override
810+
public String toString() {
811+
return "IgnoreEquals{" + value + "}";
812+
}
813+
}
786814
}

0 commit comments

Comments
 (0)