diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/xds/resource/grpc/AutoValue_Endpoints_DropOverload.java b/dubbo-xds/src/main/java/org/apache/dubbo/xds/resource/grpc/AutoValue_Endpoints_DropOverload.java new file mode 100644 index 00000000000..a872d0270b1 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/xds/resource/grpc/AutoValue_Endpoints_DropOverload.java @@ -0,0 +1,63 @@ +package org.apache.dubbo.xds.resource.grpc; + +import javax.annotation.Generated; + +@Generated("com.google.auto.value.processor.AutoValueProcessor") +final class AutoValue_Endpoints_DropOverload extends Endpoints.DropOverload { + + private final String category; + + private final int dropsPerMillion; + + AutoValue_Endpoints_DropOverload( + String category, + int dropsPerMillion) { + if (category == null) { + throw new NullPointerException("Null category"); + } + this.category = category; + this.dropsPerMillion = dropsPerMillion; + } + + @Override + String category() { + return category; + } + + @Override + int dropsPerMillion() { + return dropsPerMillion; + } + + @Override + public String toString() { + return "DropOverload{" + + "category=" + category + ", " + + "dropsPerMillion=" + dropsPerMillion + + "}"; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o instanceof Endpoints.DropOverload) { + Endpoints.DropOverload that = (Endpoints.DropOverload) o; + return this.category.equals(that.category()) + && this.dropsPerMillion == that.dropsPerMillion(); + } + return false; + } + + @Override + public int hashCode() { + int h$ = 1; + h$ *= 1000003; + h$ ^= category.hashCode(); + h$ *= 1000003; + h$ ^= dropsPerMillion; + return h$; + } + +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/xds/resource/grpc/AutoValue_Endpoints_LbEndpoint.java b/dubbo-xds/src/main/java/org/apache/dubbo/xds/resource/grpc/AutoValue_Endpoints_LbEndpoint.java new file mode 100644 index 00000000000..4262bd21c19 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/xds/resource/grpc/AutoValue_Endpoints_LbEndpoint.java @@ -0,0 +1,78 @@ +package org.apache.dubbo.xds.resource.grpc; + +import io.grpc.EquivalentAddressGroup; + +import javax.annotation.Generated; + +@Generated("com.google.auto.value.processor.AutoValueProcessor") +final class AutoValue_Endpoints_LbEndpoint extends Endpoints.LbEndpoint { + + private final EquivalentAddressGroup eag; + + private final int loadBalancingWeight; + + private final boolean isHealthy; + + AutoValue_Endpoints_LbEndpoint( + EquivalentAddressGroup eag, + int loadBalancingWeight, + boolean isHealthy) { + if (eag == null) { + throw new NullPointerException("Null eag"); + } + this.eag = eag; + this.loadBalancingWeight = loadBalancingWeight; + this.isHealthy = isHealthy; + } + + @Override + EquivalentAddressGroup eag() { + return eag; + } + + @Override + int loadBalancingWeight() { + return loadBalancingWeight; + } + + @Override + boolean isHealthy() { + return isHealthy; + } + + @Override + public String toString() { + return "LbEndpoint{" + + "eag=" + eag + ", " + + "loadBalancingWeight=" + loadBalancingWeight + ", " + + "isHealthy=" + isHealthy + + "}"; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o instanceof Endpoints.LbEndpoint) { + Endpoints.LbEndpoint that = (Endpoints.LbEndpoint) o; + return this.eag.equals(that.eag()) + && this.loadBalancingWeight == that.loadBalancingWeight() + && this.isHealthy == that.isHealthy(); + } + return false; + } + + @Override + public int hashCode() { + int h$ = 1; + h$ *= 1000003; + h$ ^= eag.hashCode(); + h$ *= 1000003; + h$ ^= loadBalancingWeight; + h$ *= 1000003; + h$ ^= isHealthy ? 1231 : 1237; + return h$; + } + +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/xds/resource/grpc/AutoValue_Endpoints_LocalityLbEndpoints.java b/dubbo-xds/src/main/java/org/apache/dubbo/xds/resource/grpc/AutoValue_Endpoints_LocalityLbEndpoints.java new file mode 100644 index 00000000000..bea0744dcb9 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/xds/resource/grpc/AutoValue_Endpoints_LocalityLbEndpoints.java @@ -0,0 +1,78 @@ +package org.apache.dubbo.xds.resource.grpc; + +import com.google.common.collect.ImmutableList; + +import javax.annotation.Generated; + +@Generated("com.google.auto.value.processor.AutoValueProcessor") +final class AutoValue_Endpoints_LocalityLbEndpoints extends Endpoints.LocalityLbEndpoints { + + private final ImmutableList endpoints; + + private final int localityWeight; + + private final int priority; + + AutoValue_Endpoints_LocalityLbEndpoints( + ImmutableList endpoints, + int localityWeight, + int priority) { + if (endpoints == null) { + throw new NullPointerException("Null endpoints"); + } + this.endpoints = endpoints; + this.localityWeight = localityWeight; + this.priority = priority; + } + + @Override + ImmutableList endpoints() { + return endpoints; + } + + @Override + int localityWeight() { + return localityWeight; + } + + @Override + int priority() { + return priority; + } + + @Override + public String toString() { + return "LocalityLbEndpoints{" + + "endpoints=" + endpoints + ", " + + "localityWeight=" + localityWeight + ", " + + "priority=" + priority + + "}"; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o instanceof Endpoints.LocalityLbEndpoints) { + Endpoints.LocalityLbEndpoints that = (Endpoints.LocalityLbEndpoints) o; + return this.endpoints.equals(that.endpoints()) + && this.localityWeight == that.localityWeight() + && this.priority == that.priority(); + } + return false; + } + + @Override + public int hashCode() { + int h$ = 1; + h$ *= 1000003; + h$ ^= endpoints.hashCode(); + h$ *= 1000003; + h$ ^= localityWeight; + h$ *= 1000003; + h$ ^= priority; + return h$; + } + +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/xds/resource/grpc/Endpoints.java b/dubbo-xds/src/main/java/org/apache/dubbo/xds/resource/grpc/Endpoints.java new file mode 100644 index 00000000000..b5f5e8aedc6 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/xds/resource/grpc/Endpoints.java @@ -0,0 +1,90 @@ +/* + * Copyright 2021 The 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 org.apache.dubbo.xds.resource.grpc; + +import java.net.InetSocketAddress; +import java.util.List; + +import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import io.grpc.EquivalentAddressGroup; + +import static com.google.common.base.Preconditions.checkArgument; + +/** Locality and endpoint level load balancing configurations. */ +final class Endpoints { + private Endpoints() {} + + /** Represents a group of endpoints belong to a single locality. */ + @AutoValue + abstract static class LocalityLbEndpoints { + // Endpoints to be load balanced. + abstract ImmutableList endpoints(); + + // Locality's weight for inter-locality load balancing. Guaranteed to be greater than 0. + abstract int localityWeight(); + + // Locality's priority level. + abstract int priority(); + + static LocalityLbEndpoints create(List endpoints, int localityWeight, + int priority) { + checkArgument(localityWeight > 0, "localityWeight must be greater than 0"); + return new AutoValue_Endpoints_LocalityLbEndpoints( + ImmutableList.copyOf(endpoints), localityWeight, priority); + } + } + + /** Represents a single endpoint to be load balanced. */ + @AutoValue + abstract static class LbEndpoint { + // The endpoint address to be connected to. + abstract EquivalentAddressGroup eag(); + + // Endpoint's weight for load balancing. If unspecified, value of 0 is returned. + abstract int loadBalancingWeight(); + + // Whether the endpoint is healthy. + abstract boolean isHealthy(); + + static LbEndpoint create(EquivalentAddressGroup eag, int loadBalancingWeight, + boolean isHealthy) { + return new AutoValue_Endpoints_LbEndpoint(eag, loadBalancingWeight, isHealthy); + } + + // Only for testing. + @VisibleForTesting + static LbEndpoint create( + String address, int port, int loadBalancingWeight, boolean isHealthy) { + return LbEndpoint.create(new EquivalentAddressGroup(new InetSocketAddress(address, port)), + loadBalancingWeight, isHealthy); + } + } + + /** Represents a drop policy. */ + @AutoValue + abstract static class DropOverload { + abstract String category(); + + abstract int dropsPerMillion(); + + static DropOverload create(String category, int dropsPerMillion) { + return new AutoValue_Endpoints_DropOverload(category, dropsPerMillion); + } + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/xds/resource/grpc/XdsEndpointResource.java b/dubbo-xds/src/main/java/org/apache/dubbo/xds/resource/grpc/XdsEndpointResource.java index 0b79b947401..03f4b6284e0 100644 --- a/dubbo-xds/src/main/java/org/apache/dubbo/xds/resource/grpc/XdsEndpointResource.java +++ b/dubbo-xds/src/main/java/org/apache/dubbo/xds/resource/grpc/XdsEndpointResource.java @@ -1,249 +1,250 @@ -///* -// * Copyright 2022 The 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 org.apache.dubbo.xds.resource.grpc; -// -//import org.apache.dubbo.xds.resource.grpc.XdsClient.ResourceUpdate; -//import org.apache.dubbo.xds.resource.grpc.XdsClientImpl.ResourceInvalidException; -//import org.apache.dubbo.xds.resource.grpc.XdsEndpointResource.EdsUpdate; -// -//import com.google.common.annotations.VisibleForTesting; -//import com.google.common.base.MoreObjects; -//import com.google.common.collect.ImmutableList; -//import com.google.protobuf.Message; -//import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment; -//import io.envoyproxy.envoy.type.v3.FractionalPercent; -//import io.grpc.EquivalentAddressGroup; -// -//import javax.annotation.Nullable; -// -//import java.net.InetSocketAddress; -//import java.util.ArrayList; -//import java.util.Collections; -//import java.util.HashMap; -//import java.util.HashSet; -//import java.util.LinkedHashMap; -//import java.util.List; -//import java.util.Map; -//import java.util.Objects; -//import java.util.Set; -// -//import static com.google.common.base.Preconditions.checkNotNull; -// -//class XdsEndpointResource extends XdsResourceType { -// static final String ADS_TYPE_URL_EDS = -// "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment"; -// -// private static final XdsEndpointResource instance = new XdsEndpointResource(); -// -// public static XdsEndpointResource getInstance() { -// return instance; -// } -// -// @Override -// @Nullable -// String extractResourceName(Message unpackedResource) { -// if (!(unpackedResource instanceof ClusterLoadAssignment)) { -// return null; -// } -// return ((ClusterLoadAssignment) unpackedResource).getClusterName(); -// } -// -// @Override -// String typeName() { -// return "EDS"; -// } -// -// @Override -// String typeUrl() { -// return ADS_TYPE_URL_EDS; -// } -// -// @Override -// boolean isFullStateOfTheWorld() { -// return false; -// } -// -// @Override -// Class unpackedClassName() { -// return ClusterLoadAssignment.class; -// } -// -// @Override -// EdsUpdate doParse(Args args, Message unpackedMessage) -// throws ResourceInvalidException { -// if (!(unpackedMessage instanceof ClusterLoadAssignment)) { -// throw new ResourceInvalidException("Invalid message type: " + unpackedMessage.getClass()); -// } -// return processClusterLoadAssignment((ClusterLoadAssignment) unpackedMessage); -// } -// -// private static EdsUpdate processClusterLoadAssignment(ClusterLoadAssignment assignment) -// throws ResourceInvalidException { -// Map> priorities = new HashMap<>(); -// Map localityLbEndpointsMap = new LinkedHashMap<>(); -// List dropOverloads = new ArrayList<>(); -// int maxPriority = -1; -// for (io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints localityLbEndpointsProto -// : assignment.getEndpointsList()) { -// StructOrError structOrError = -// parseLocalityLbEndpoints(localityLbEndpointsProto); -// if (structOrError == null) { -// continue; -// } -// if (structOrError.getErrorDetail() != null) { -// throw new ResourceInvalidException(structOrError.getErrorDetail()); -// } -// -// LocalityLbEndpoints localityLbEndpoints = structOrError.getStruct(); -// int priority = localityLbEndpoints.priority(); -// maxPriority = Math.max(maxPriority, priority); -// // Note endpoints with health status other than HEALTHY and UNKNOWN are still -// // handed over to watching parties. It is watching parties' responsibility to -// // filter out unhealthy endpoints. See EnvoyProtoData.LbEndpoint#isHealthy(). -// Locality locality = parseLocality(localityLbEndpointsProto.getLocality()); -// localityLbEndpointsMap.put(locality, localityLbEndpoints); -// if (!priorities.containsKey(priority)) { -// priorities.put(priority, new HashSet<>()); -// } -// if (!priorities.get(priority).add(locality)) { -// throw new ResourceInvalidException("ClusterLoadAssignment has duplicate locality:" -// + locality + " for priority:" + priority); -// } -// } -// if (priorities.size() != maxPriority + 1) { -// throw new ResourceInvalidException("ClusterLoadAssignment has sparse priorities"); -// } -// -// for (ClusterLoadAssignment.Policy.DropOverload dropOverloadProto -// : assignment.getPolicy().getDropOverloadsList()) { -// dropOverloads.add(parseDropOverload(dropOverloadProto)); -// } -// return new EdsUpdate(assignment.getClusterName(), localityLbEndpointsMap, dropOverloads); -// } -// -// private static Locality parseLocality(io.envoyproxy.envoy.config.core.v3.Locality proto) { -// return Locality.create(proto.getRegion(), proto.getZone(), proto.getSubZone()); -// } -// -// private static DropOverload parseDropOverload( -// ClusterLoadAssignment.Policy.DropOverload proto) { -// return DropOverload.create(proto.getCategory(), getRatePerMillion(proto.getDropPercentage())); -// } -// -// private static int getRatePerMillion(FractionalPercent percent) { -// int numerator = percent.getNumerator(); -// FractionalPercent.DenominatorType type = percent.getDenominator(); -// switch (type) { -// case TEN_THOUSAND: -// numerator *= 100; -// break; -// case HUNDRED: -// numerator *= 10_000; -// break; -// case MILLION: -// break; -// case UNRECOGNIZED: -// default: -// throw new IllegalArgumentException("Unknown denominator type of " + percent); -// } -// -// if (numerator > 1_000_000 || numerator < 0) { -// numerator = 1_000_000; -// } -// return numerator; -// } -// -// -// @VisibleForTesting -// @Nullable -// static StructOrError parseLocalityLbEndpoints( -// io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints proto) { -// // Filter out localities without or with 0 weight. -// if (!proto.hasLoadBalancingWeight() || proto.getLoadBalancingWeight().getValue() < 1) { -// return null; -// } -// if (proto.getPriority() < 0) { -// return StructOrError.fromError("negative priority"); -// } -// List endpoints = new ArrayList<>(proto.getLbEndpointsCount()); -// for (io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint endpoint : proto.getLbEndpointsList()) { -// // The endpoint field of each lb_endpoints must be set. -// // Inside of it: the address field must be set. -// if (!endpoint.hasEndpoint() || !endpoint.getEndpoint().hasAddress()) { -// return StructOrError.fromError("LbEndpoint with no endpoint/address"); -// } -// io.envoyproxy.envoy.config.core.v3.SocketAddress socketAddress = -// endpoint.getEndpoint().getAddress().getSocketAddress(); -// InetSocketAddress addr = -// new InetSocketAddress(socketAddress.getAddress(), socketAddress.getPortValue()); -// boolean isHealthy = -// endpoint.getHealthStatus() == io.envoyproxy.envoy.config.core.v3.HealthStatus.HEALTHY -// || endpoint.getHealthStatus() -// == io.envoyproxy.envoy.config.core.v3.HealthStatus.UNKNOWN; -// endpoints.add(Endpoints.LbEndpoint.create( -// new EquivalentAddressGroup(ImmutableList.of(addr)), -// endpoint.getLoadBalancingWeight().getValue(), isHealthy)); -// } -// return StructOrError.fromStruct(Endpoints.LocalityLbEndpoints.create( -// endpoints, proto.getLoadBalancingWeight().getValue(), proto.getPriority())); -// } -// -// static final class EdsUpdate implements ResourceUpdate { -// final String clusterName; -// final Map localityLbEndpointsMap; -// final List dropPolicies; -// -// EdsUpdate(String clusterName, Map localityLbEndpoints, -// List dropPolicies) { -// this.clusterName = checkNotNull(clusterName, "clusterName"); -// this.localityLbEndpointsMap = Collections.unmodifiableMap( -// new LinkedHashMap<>(checkNotNull(localityLbEndpoints, "localityLbEndpoints"))); -// this.dropPolicies = Collections.unmodifiableList( -// new ArrayList<>(checkNotNull(dropPolicies, "dropPolicies"))); -// } -// -// @Override -// public boolean equals(Object o) { -// if (this == o) { -// return true; -// } -// if (o == null || getClass() != o.getClass()) { -// return false; -// } -// EdsUpdate that = (EdsUpdate) o; -// return Objects.equals(clusterName, that.clusterName) -// && Objects.equals(localityLbEndpointsMap, that.localityLbEndpointsMap) -// && Objects.equals(dropPolicies, that.dropPolicies); -// } -// -// @Override -// public int hashCode() { -// return Objects.hash(clusterName, localityLbEndpointsMap, dropPolicies); -// } -// -// @Override -// public String toString() { -// return -// MoreObjects -// .toStringHelper(this) -// .add("clusterName", clusterName) -// .add("localityLbEndpointsMap", localityLbEndpointsMap) -// .add("dropPolicies", dropPolicies) -// .toString(); -// } -// } -//} +/* + * Copyright 2022 The 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 org.apache.dubbo.xds.resource.grpc; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.protobuf.Message; +import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment; +import io.envoyproxy.envoy.type.v3.FractionalPercent; +import io.grpc.EquivalentAddressGroup; + +import org.apache.dubbo.xds.resource.grpc.Endpoints.DropOverload; +import org.apache.dubbo.xds.resource.grpc.Endpoints.LocalityLbEndpoints; +import org.apache.dubbo.xds.resource.grpc.XdsClient.ResourceUpdate; +import org.apache.dubbo.xds.resource.grpc.XdsClientImpl.ResourceInvalidException; +import org.apache.dubbo.xds.resource.grpc.XdsEndpointResource.EdsUpdate; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import javax.annotation.Nullable; + +class XdsEndpointResource extends XdsResourceType { + static final String ADS_TYPE_URL_EDS = + "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment"; + + private static final XdsEndpointResource instance = new XdsEndpointResource(); + + public static XdsEndpointResource getInstance() { + return instance; + } + + @Override + @Nullable + String extractResourceName(Message unpackedResource) { + if (!(unpackedResource instanceof ClusterLoadAssignment)) { + return null; + } + return ((ClusterLoadAssignment) unpackedResource).getClusterName(); + } + + @Override + String typeName() { + return "EDS"; + } + + @Override + String typeUrl() { + return ADS_TYPE_URL_EDS; + } + + @Override + boolean isFullStateOfTheWorld() { + return false; + } + + @Override + Class unpackedClassName() { + return ClusterLoadAssignment.class; + } + + @Override + EdsUpdate doParse(Args args, Message unpackedMessage) + throws ResourceInvalidException { + if (!(unpackedMessage instanceof ClusterLoadAssignment)) { + throw new ResourceInvalidException("Invalid message type: " + unpackedMessage.getClass()); + } + return processClusterLoadAssignment((ClusterLoadAssignment) unpackedMessage); + } + + private static EdsUpdate processClusterLoadAssignment(ClusterLoadAssignment assignment) + throws ResourceInvalidException { + Map> priorities = new HashMap<>(); + Map localityLbEndpointsMap = new LinkedHashMap<>(); + List dropOverloads = new ArrayList<>(); + int maxPriority = -1; + for (io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints localityLbEndpointsProto + : assignment.getEndpointsList()) { + StructOrError structOrError = + parseLocalityLbEndpoints(localityLbEndpointsProto); + if (structOrError == null) { + continue; + } + if (structOrError.getErrorDetail() != null) { + throw new ResourceInvalidException(structOrError.getErrorDetail()); + } + + LocalityLbEndpoints localityLbEndpoints = structOrError.getStruct(); + int priority = localityLbEndpoints.priority(); + maxPriority = Math.max(maxPriority, priority); + // Note endpoints with health status other than HEALTHY and UNKNOWN are still + // handed over to watching parties. It is watching parties' responsibility to + // filter out unhealthy endpoints. See EnvoyProtoData.LbEndpoint#isHealthy(). + Locality locality = parseLocality(localityLbEndpointsProto.getLocality()); + localityLbEndpointsMap.put(locality, localityLbEndpoints); + if (!priorities.containsKey(priority)) { + priorities.put(priority, new HashSet<>()); + } + if (!priorities.get(priority).add(locality)) { + throw new ResourceInvalidException("ClusterLoadAssignment has duplicate locality:" + + locality + " for priority:" + priority); + } + } + if (priorities.size() != maxPriority + 1) { + throw new ResourceInvalidException("ClusterLoadAssignment has sparse priorities"); + } + + for (ClusterLoadAssignment.Policy.DropOverload dropOverloadProto + : assignment.getPolicy().getDropOverloadsList()) { + dropOverloads.add(parseDropOverload(dropOverloadProto)); + } + return new EdsUpdate(assignment.getClusterName(), localityLbEndpointsMap, dropOverloads); + } + + private static Locality parseLocality(io.envoyproxy.envoy.config.core.v3.Locality proto) { + return Locality.create(proto.getRegion(), proto.getZone(), proto.getSubZone()); + } + + private static DropOverload parseDropOverload( + io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment.Policy.DropOverload proto) { + return DropOverload.create(proto.getCategory(), getRatePerMillion(proto.getDropPercentage())); + } + + private static int getRatePerMillion(FractionalPercent percent) { + int numerator = percent.getNumerator(); + FractionalPercent.DenominatorType type = percent.getDenominator(); + switch (type) { + case TEN_THOUSAND: + numerator *= 100; + break; + case HUNDRED: + numerator *= 10_000; + break; + case MILLION: + break; + case UNRECOGNIZED: + default: + throw new IllegalArgumentException("Unknown denominator type of " + percent); + } + + if (numerator > 1_000_000 || numerator < 0) { + numerator = 1_000_000; + } + return numerator; + } + + + @VisibleForTesting + @Nullable + static StructOrError parseLocalityLbEndpoints( + io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints proto) { + // Filter out localities without or with 0 weight. + if (!proto.hasLoadBalancingWeight() || proto.getLoadBalancingWeight().getValue() < 1) { + return null; + } + if (proto.getPriority() < 0) { + return StructOrError.fromError("negative priority"); + } + List endpoints = new ArrayList<>(proto.getLbEndpointsCount()); + for (io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint endpoint : proto.getLbEndpointsList()) { + // The endpoint field of each lb_endpoints must be set. + // Inside of it: the address field must be set. + if (!endpoint.hasEndpoint() || !endpoint.getEndpoint().hasAddress()) { + return StructOrError.fromError("LbEndpoint with no endpoint/address"); + } + io.envoyproxy.envoy.config.core.v3.SocketAddress socketAddress = + endpoint.getEndpoint().getAddress().getSocketAddress(); + InetSocketAddress addr = + new InetSocketAddress(socketAddress.getAddress(), socketAddress.getPortValue()); + boolean isHealthy = + endpoint.getHealthStatus() == io.envoyproxy.envoy.config.core.v3.HealthStatus.HEALTHY + || endpoint.getHealthStatus() + == io.envoyproxy.envoy.config.core.v3.HealthStatus.UNKNOWN; + endpoints.add(Endpoints.LbEndpoint.create( + new EquivalentAddressGroup(ImmutableList.of(addr)), + endpoint.getLoadBalancingWeight().getValue(), isHealthy)); + } + return StructOrError.fromStruct(Endpoints.LocalityLbEndpoints.create( + endpoints, proto.getLoadBalancingWeight().getValue(), proto.getPriority())); + } + + static final class EdsUpdate implements ResourceUpdate { + final String clusterName; + final Map localityLbEndpointsMap; + final List dropPolicies; + + EdsUpdate(String clusterName, Map localityLbEndpoints, + List dropPolicies) { + this.clusterName = checkNotNull(clusterName, "clusterName"); + this.localityLbEndpointsMap = Collections.unmodifiableMap( + new LinkedHashMap<>(checkNotNull(localityLbEndpoints, "localityLbEndpoints"))); + this.dropPolicies = Collections.unmodifiableList( + new ArrayList<>(checkNotNull(dropPolicies, "dropPolicies"))); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EdsUpdate that = (EdsUpdate) o; + return Objects.equals(clusterName, that.clusterName) + && Objects.equals(localityLbEndpointsMap, that.localityLbEndpointsMap) + && Objects.equals(dropPolicies, that.dropPolicies); + } + + @Override + public int hashCode() { + return Objects.hash(clusterName, localityLbEndpointsMap, dropPolicies); + } + + @Override + public String toString() { + return + MoreObjects + .toStringHelper(this) + .add("clusterName", clusterName) + .add("localityLbEndpointsMap", localityLbEndpointsMap) + .add("dropPolicies", dropPolicies) + .toString(); + } + } +}