From 1f7bbd8de16c860b7ac18c26135dc98cd1c50acb Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Mon, 23 Jan 2023 13:59:55 -0500 Subject: [PATCH 01/25] refactor round robin LB --- .../io/grpc/util/RoundRobinLoadBalancer.java | 74 +++---------- .../grpc/util/RoundRobinLoadBalancerImpl.java | 100 ++++++++++++++++++ .../SecretRoundRobinLoadBalancerProvider.java | 2 +- ...AutoConfiguredLoadBalancerFactoryTest.java | 2 +- .../OutlierDetectionLoadBalancerTest.java | 2 +- .../grpc/util/RoundRobinLoadBalancerTest.java | 10 +- 6 files changed, 122 insertions(+), 68 deletions(-) create mode 100644 core/src/main/java/io/grpc/util/RoundRobinLoadBalancerImpl.java diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java index b715f756144..c6d2fa4f199 100644 --- a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java @@ -43,14 +43,16 @@ import java.util.Map; import java.util.Random; import java.util.Set; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import javax.annotation.Nonnull; /** - * A {@link LoadBalancer} that provides round-robin load-balancing over the {@link - * EquivalentAddressGroup}s from the {@link NameResolver}. + * A {@link LoadBalancer} that provides load-balancing over the {@link + * EquivalentAddressGroup}s from the {@link NameResolver}. It provides default implementation of + * accepting {@link io.grpc.LoadBalancer.ResolvedAddresses} updates, process aggregated load + * balancing state. The round-robin picker algorithm is implemented by method + * {@link #createReadyPicker}. */ -final class RoundRobinLoadBalancer extends LoadBalancer { +abstract class RoundRobinLoadBalancer extends LoadBalancer { @VisibleForTesting static final Attributes.Key> STATE_INFO = Attributes.Key.create("state-info"); @@ -68,6 +70,11 @@ final class RoundRobinLoadBalancer extends LoadBalancer { this.random = new Random(); } + abstract Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args); + + abstract RoundRobinPicker createReadyPicker(List activeSubchannelList, + int startIndex); + @Override public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { if (resolvedAddresses.getAddresses().isEmpty()) { @@ -105,7 +112,7 @@ public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { new Ref<>(ConnectivityStateInfo.forNonError(IDLE))); final Subchannel subchannel = checkNotNull( - helper.createSubchannel(CreateSubchannelArgs.newBuilder() + createSubchannel(helper, CreateSubchannelArgs.newBuilder() .setAddresses(originalAddressGroup) .setAttributes(subchannelAttrs.build()) .build()), @@ -210,7 +217,7 @@ private void updateBalancingState() { // initialize the Picker to a random start index to ensure that a high frequency of Picker // churn does not skew subchannel selection. int startIndex = random.nextInt(activeList.size()); - updateBalancingState(READY, new ReadyPicker(activeList, startIndex)); + updateBalancingState(READY, createReadyPicker(activeList, startIndex)); } } @@ -275,63 +282,10 @@ private static Set setsDifference(Set a, Set b) { } // Only subclasses are ReadyPicker or EmptyPicker - private abstract static class RoundRobinPicker extends SubchannelPicker { + abstract static class RoundRobinPicker extends SubchannelPicker { abstract boolean isEquivalentTo(RoundRobinPicker picker); } - @VisibleForTesting - static final class ReadyPicker extends RoundRobinPicker { - private static final AtomicIntegerFieldUpdater indexUpdater = - AtomicIntegerFieldUpdater.newUpdater(ReadyPicker.class, "index"); - - private final List list; // non-empty - @SuppressWarnings("unused") - private volatile int index; - - ReadyPicker(List list, int startIndex) { - Preconditions.checkArgument(!list.isEmpty(), "empty list"); - this.list = list; - this.index = startIndex - 1; - } - - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - return PickResult.withSubchannel(nextSubchannel()); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(ReadyPicker.class).add("list", list).toString(); - } - - private Subchannel nextSubchannel() { - int size = list.size(); - int i = indexUpdater.incrementAndGet(this); - if (i >= size) { - int oldi = i; - i %= size; - indexUpdater.compareAndSet(this, oldi, i); - } - return list.get(i); - } - - @VisibleForTesting - List getList() { - return list; - } - - @Override - boolean isEquivalentTo(RoundRobinPicker picker) { - if (!(picker instanceof ReadyPicker)) { - return false; - } - ReadyPicker other = (ReadyPicker) picker; - // the lists cannot contain duplicate subchannels - return other == this - || (list.size() == other.list.size() && new HashSet<>(list).containsAll(other.list)); - } - } - @VisibleForTesting static final class EmptyPicker extends RoundRobinPicker { diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancerImpl.java b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancerImpl.java new file mode 100644 index 00000000000..d65ac5b4e42 --- /dev/null +++ b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancerImpl.java @@ -0,0 +1,100 @@ +/* + * Copyright 2016 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 io.grpc.util; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; +import io.grpc.EquivalentAddressGroup; +import io.grpc.NameResolver; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +/** + * A {@link RoundRobinLoadBalancer} that provides round-robin load-balancing over the {@link + * EquivalentAddressGroup}s from the {@link NameResolver}. + */ +final class RoundRobinLoadBalancerImpl extends RoundRobinLoadBalancer { + + RoundRobinLoadBalancerImpl(Helper helper) { + super(helper); + } + + @Override + Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args) { + return helper.createSubchannel(args); + } + + @Override + RoundRobinPicker createReadyPicker(List activeSubchannelList, int startIndex) { + return new ReadyPicker(activeSubchannelList, startIndex); + } + + @VisibleForTesting + static final class ReadyPicker extends RoundRobinPicker { + private static final AtomicIntegerFieldUpdater indexUpdater = + AtomicIntegerFieldUpdater.newUpdater(ReadyPicker.class, "index"); + + private final List list; // non-empty + @SuppressWarnings("unused") + private volatile int index; + + ReadyPicker(List list, int startIndex) { + Preconditions.checkArgument(!list.isEmpty(), "empty list"); + this.list = list; + this.index = startIndex - 1; + } + + @Override + public PickResult pickSubchannel(PickSubchannelArgs args) { + return PickResult.withSubchannel(nextSubchannel()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(ReadyPicker.class).add("list", list).toString(); + } + + private Subchannel nextSubchannel() { + int size = list.size(); + int i = indexUpdater.incrementAndGet(this); + if (i >= size) { + int oldi = i; + i %= size; + indexUpdater.compareAndSet(this, oldi, i); + } + return list.get(i); + } + + @VisibleForTesting + List getList() { + return list; + } + + @Override + boolean isEquivalentTo(RoundRobinPicker picker) { + if (!(picker instanceof ReadyPicker)) { + return false; + } + ReadyPicker other = (ReadyPicker) picker; + // the lists cannot contain duplicate subchannels + return other == this + || (list.size() == other.list.size() && new HashSet<>(list).containsAll(other.list)); + } + } +} diff --git a/core/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java b/core/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java index 7843832cd5a..9cb794649e9 100644 --- a/core/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java +++ b/core/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java @@ -52,7 +52,7 @@ public String getPolicyName() { @Override public LoadBalancer newLoadBalancer(LoadBalancer.Helper helper) { - return new RoundRobinLoadBalancer(helper); + return new RoundRobinLoadBalancerImpl(helper); } @Override diff --git a/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java b/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java index ad886c31142..72e373a04fd 100644 --- a/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java +++ b/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java @@ -405,7 +405,7 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { .build()); assertThat(addressesAccepted).isTrue(); assertThat(lb.getDelegate().getClass().getName()) - .isEqualTo("io.grpc.util.RoundRobinLoadBalancer"); + .isEqualTo("io.grpc.util.RoundRobinLoadBalancerImpl"); } @Test diff --git a/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java b/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java index ccf3d40cdb6..778dd60ec2c 100644 --- a/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java +++ b/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java @@ -123,7 +123,7 @@ public LoadBalancer newLoadBalancer(Helper helper) { "round_robin") { @Override public LoadBalancer newLoadBalancer(Helper helper) { - return new RoundRobinLoadBalancer(helper); + return new RoundRobinLoadBalancerImpl(helper); } }; diff --git a/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java b/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java index d4c07e3d50e..293b65e61e1 100644 --- a/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java +++ b/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java @@ -22,7 +22,7 @@ import static io.grpc.ConnectivityState.READY; import static io.grpc.ConnectivityState.SHUTDOWN; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; -import static io.grpc.util.RoundRobinLoadBalancer.STATE_INFO; +import static io.grpc.util.RoundRobinLoadBalancerImpl.STATE_INFO; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -56,8 +56,8 @@ import io.grpc.LoadBalancer.SubchannelStateListener; import io.grpc.Status; import io.grpc.util.RoundRobinLoadBalancer.EmptyPicker; -import io.grpc.util.RoundRobinLoadBalancer.ReadyPicker; import io.grpc.util.RoundRobinLoadBalancer.Ref; +import io.grpc.util.RoundRobinLoadBalancerImpl.ReadyPicker; import java.net.SocketAddress; import java.util.ArrayList; import java.util.Arrays; @@ -78,12 +78,12 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -/** Unit test for {@link RoundRobinLoadBalancer}. */ +/** Unit test for {@link RoundRobinLoadBalancerImpl}. */ @RunWith(JUnit4.class) public class RoundRobinLoadBalancerTest { private static final Attributes.Key MAJOR_KEY = Attributes.Key.create("major-key"); - private RoundRobinLoadBalancer loadBalancer; + private RoundRobinLoadBalancerImpl loadBalancer; private final List servers = Lists.newArrayList(); private final Map, Subchannel> subchannels = Maps.newLinkedHashMap(); private final Map subchannelStateListeners = @@ -136,7 +136,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { } }); - loadBalancer = new RoundRobinLoadBalancer(mockHelper); + loadBalancer = new RoundRobinLoadBalancerImpl(mockHelper); } @After From 73cf208e3486834b214aaf1278a9ccb63ebd9f81 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Wed, 25 Jan 2023 10:45:49 -0800 Subject: [PATCH 02/25] rename abstract* --- .../util/AbstractRoundRobinLoadBalancer.java | 326 ++++++++++++++++++ .../io/grpc/util/RoundRobinLoadBalancer.java | 314 +++-------------- .../grpc/util/RoundRobinLoadBalancerImpl.java | 100 ------ .../SecretRoundRobinLoadBalancerProvider.java | 2 +- ...AutoConfiguredLoadBalancerFactoryTest.java | 2 +- .../OutlierDetectionLoadBalancerTest.java | 2 +- .../grpc/util/RoundRobinLoadBalancerTest.java | 14 +- 7 files changed, 380 insertions(+), 380 deletions(-) create mode 100644 core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java delete mode 100644 core/src/main/java/io/grpc/util/RoundRobinLoadBalancerImpl.java diff --git a/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java new file mode 100644 index 00000000000..90318bb49c6 --- /dev/null +++ b/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java @@ -0,0 +1,326 @@ +/* + * Copyright 2016 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 io.grpc.util; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.ConnectivityState.CONNECTING; +import static io.grpc.ConnectivityState.IDLE; +import static io.grpc.ConnectivityState.READY; +import static io.grpc.ConnectivityState.SHUTDOWN; +import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import io.grpc.Attributes; +import io.grpc.ConnectivityState; +import io.grpc.ConnectivityStateInfo; +import io.grpc.EquivalentAddressGroup; +import io.grpc.LoadBalancer; +import io.grpc.NameResolver; +import io.grpc.Status; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import javax.annotation.Nonnull; + +/** + * A {@link LoadBalancer} that provides load-balancing over the {@link + * EquivalentAddressGroup}s from the {@link NameResolver}. It provides default implementation of + * accepting {@link io.grpc.LoadBalancer.ResolvedAddresses} updates, process aggregated load + * balancing state. The round-robin picker algorithm is implemented by method + * {@link #createReadyPicker}. + */ +abstract class AbstractRoundRobinLoadBalancer extends LoadBalancer { + @VisibleForTesting + static final Attributes.Key> STATE_INFO = + Attributes.Key.create("state-info"); + + private final Helper helper; + private final Map subchannels = + new HashMap<>(); + private final Random random; + + private ConnectivityState currentState; + private RoundRobinPicker currentPicker = new EmptyPicker(EMPTY_OK); + + AbstractRoundRobinLoadBalancer(Helper helper) { + this.helper = checkNotNull(helper, "helper"); + this.random = new Random(); + } + + abstract Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args); + + abstract RoundRobinPicker createReadyPicker(List activeSubchannelList, + int startIndex); + + @Override + public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { + if (resolvedAddresses.getAddresses().isEmpty()) { + handleNameResolutionError(Status.UNAVAILABLE.withDescription( + "NameResolver returned no usable address. addrs=" + resolvedAddresses.getAddresses() + + ", attrs=" + resolvedAddresses.getAttributes())); + return false; + } + + List servers = resolvedAddresses.getAddresses(); + Set currentAddrs = subchannels.keySet(); + Map latestAddrs = stripAttrs(servers); + Set removedAddrs = setsDifference(currentAddrs, latestAddrs.keySet()); + + for (Map.Entry latestEntry : + latestAddrs.entrySet()) { + EquivalentAddressGroup strippedAddressGroup = latestEntry.getKey(); + EquivalentAddressGroup originalAddressGroup = latestEntry.getValue(); + Subchannel existingSubchannel = subchannels.get(strippedAddressGroup); + if (existingSubchannel != null) { + // EAG's Attributes may have changed. + existingSubchannel.updateAddresses(Collections.singletonList(originalAddressGroup)); + continue; + } + // Create new subchannels for new addresses. + + // NB(lukaszx0): we don't merge `attributes` with `subchannelAttr` because subchannel + // doesn't need them. They're describing the resolved server list but we're not taking + // any action based on this information. + Attributes.Builder subchannelAttrs = Attributes.newBuilder() + // NB(lukaszx0): because attributes are immutable we can't set new value for the key + // after creation but since we can mutate the values we leverage that and set + // AtomicReference which will allow mutating state info for given channel. + .set(STATE_INFO, + new Ref<>(ConnectivityStateInfo.forNonError(IDLE))); + + final Subchannel subchannel = checkNotNull( + createSubchannel(helper, CreateSubchannelArgs.newBuilder() + .setAddresses(originalAddressGroup) + .setAttributes(subchannelAttrs.build()) + .build()), + "subchannel"); + subchannel.start(new SubchannelStateListener() { + @Override + public void onSubchannelState(ConnectivityStateInfo state) { + processSubchannelState(subchannel, state); + } + }); + subchannels.put(strippedAddressGroup, subchannel); + subchannel.requestConnection(); + } + + ArrayList removedSubchannels = new ArrayList<>(); + for (EquivalentAddressGroup addressGroup : removedAddrs) { + removedSubchannels.add(subchannels.remove(addressGroup)); + } + + // Update the picker before shutting down the subchannels, to reduce the chance of the race + // between picking a subchannel and shutting it down. + updateBalancingState(); + + // Shutdown removed subchannels + for (Subchannel removedSubchannel : removedSubchannels) { + shutdownSubchannel(removedSubchannel); + } + + return true; + } + + @Override + public void handleNameResolutionError(Status error) { + if (currentState != READY) { + updateBalancingState(TRANSIENT_FAILURE, new EmptyPicker(error)); + } + } + + private void processSubchannelState(Subchannel subchannel, ConnectivityStateInfo stateInfo) { + if (subchannels.get(stripAttrs(subchannel.getAddresses())) != subchannel) { + return; + } + if (stateInfo.getState() == TRANSIENT_FAILURE || stateInfo.getState() == IDLE) { + helper.refreshNameResolution(); + } + if (stateInfo.getState() == IDLE) { + subchannel.requestConnection(); + } + Ref subchannelStateRef = getSubchannelStateInfoRef(subchannel); + if (subchannelStateRef.value.getState().equals(TRANSIENT_FAILURE)) { + if (stateInfo.getState().equals(CONNECTING) || stateInfo.getState().equals(IDLE)) { + return; + } + } + subchannelStateRef.value = stateInfo; + updateBalancingState(); + } + + private void shutdownSubchannel(Subchannel subchannel) { + subchannel.shutdown(); + getSubchannelStateInfoRef(subchannel).value = + ConnectivityStateInfo.forNonError(SHUTDOWN); + } + + @Override + public void shutdown() { + for (Subchannel subchannel : getSubchannels()) { + shutdownSubchannel(subchannel); + } + subchannels.clear(); + } + + private static final Status EMPTY_OK = Status.OK.withDescription("no subchannels ready"); + + /** + * Updates picker with the list of active subchannels (state == READY). + */ + @SuppressWarnings("ReferenceEquality") + private void updateBalancingState() { + List activeList = filterNonFailingSubchannels(getSubchannels()); + if (activeList.isEmpty()) { + // No READY subchannels, determine aggregate state and error status + boolean isConnecting = false; + Status aggStatus = EMPTY_OK; + for (Subchannel subchannel : getSubchannels()) { + ConnectivityStateInfo stateInfo = getSubchannelStateInfoRef(subchannel).value; + // This subchannel IDLE is not because of channel IDLE_TIMEOUT, + // in which case LB is already shutdown. + // RRLB will request connection immediately on subchannel IDLE. + if (stateInfo.getState() == CONNECTING || stateInfo.getState() == IDLE) { + isConnecting = true; + } + if (aggStatus == EMPTY_OK || !aggStatus.isOk()) { + aggStatus = stateInfo.getStatus(); + } + } + updateBalancingState(isConnecting ? CONNECTING : TRANSIENT_FAILURE, + // If all subchannels are TRANSIENT_FAILURE, return the Status associated with + // an arbitrary subchannel, otherwise return OK. + new EmptyPicker(aggStatus)); + } else { + // initialize the Picker to a random start index to ensure that a high frequency of Picker + // churn does not skew subchannel selection. + int startIndex = random.nextInt(activeList.size()); + updateBalancingState(READY, createReadyPicker(activeList, startIndex)); + } + } + + private void updateBalancingState(ConnectivityState state, RoundRobinPicker picker) { + if (state != currentState || !picker.isEquivalentTo(currentPicker)) { + helper.updateBalancingState(state, picker); + currentState = state; + currentPicker = picker; + } + } + + /** + * Filters out non-ready subchannels. + */ + private static List filterNonFailingSubchannels( + Collection subchannels) { + List readySubchannels = new ArrayList<>(subchannels.size()); + for (Subchannel subchannel : subchannels) { + if (isReady(subchannel)) { + readySubchannels.add(subchannel); + } + } + return readySubchannels; + } + + /** + * Converts list of {@link EquivalentAddressGroup} to {@link EquivalentAddressGroup} set and + * remove all attributes. The values are the original EAGs. + */ + private static Map stripAttrs( + List groupList) { + Map addrs = new HashMap<>(groupList.size() * 2); + for (EquivalentAddressGroup group : groupList) { + addrs.put(stripAttrs(group), group); + } + return addrs; + } + + private static EquivalentAddressGroup stripAttrs(EquivalentAddressGroup eag) { + return new EquivalentAddressGroup(eag.getAddresses()); + } + + @VisibleForTesting + Collection getSubchannels() { + return subchannels.values(); + } + + private static Ref getSubchannelStateInfoRef( + Subchannel subchannel) { + return checkNotNull(subchannel.getAttributes().get(STATE_INFO), "STATE_INFO"); + } + + // package-private to avoid synthetic access + static boolean isReady(Subchannel subchannel) { + return getSubchannelStateInfoRef(subchannel).value.getState() == READY; + } + + private static Set setsDifference(Set a, Set b) { + Set aCopy = new HashSet<>(a); + aCopy.removeAll(b); + return aCopy; + } + + // Only subclasses are ReadyPicker or EmptyPicker + abstract static class RoundRobinPicker extends SubchannelPicker { + abstract boolean isEquivalentTo(RoundRobinPicker picker); + } + + @VisibleForTesting + static final class EmptyPicker extends RoundRobinPicker { + + private final Status status; + + EmptyPicker(@Nonnull Status status) { + this.status = Preconditions.checkNotNull(status, "status"); + } + + @Override + public PickResult pickSubchannel(PickSubchannelArgs args) { + return status.isOk() ? PickResult.withNoResult() : PickResult.withError(status); + } + + @Override + boolean isEquivalentTo(RoundRobinPicker picker) { + return picker instanceof EmptyPicker && (Objects.equal(status, ((EmptyPicker) picker).status) + || (status.isOk() && ((EmptyPicker) picker).status.isOk())); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(EmptyPicker.class).add("status", status).toString(); + } + } + + /** + * A lighter weight Reference than AtomicReference. + */ + @VisibleForTesting + static final class Ref { + T value; + + Ref(T value) { + this.value = value; + } + } +} diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java index c6d2fa4f199..8f0fbd1b2ec 100644 --- a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java @@ -16,311 +16,85 @@ package io.grpc.util; -import static com.google.common.base.Preconditions.checkNotNull; -import static io.grpc.ConnectivityState.CONNECTING; -import static io.grpc.ConnectivityState.IDLE; -import static io.grpc.ConnectivityState.READY; -import static io.grpc.ConnectivityState.SHUTDOWN; -import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; - import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; -import com.google.common.base.Objects; import com.google.common.base.Preconditions; -import io.grpc.Attributes; -import io.grpc.ConnectivityState; -import io.grpc.ConnectivityStateInfo; import io.grpc.EquivalentAddressGroup; -import io.grpc.LoadBalancer; import io.grpc.NameResolver; -import io.grpc.Status; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Set; -import javax.annotation.Nonnull; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; /** - * A {@link LoadBalancer} that provides load-balancing over the {@link - * EquivalentAddressGroup}s from the {@link NameResolver}. It provides default implementation of - * accepting {@link io.grpc.LoadBalancer.ResolvedAddresses} updates, process aggregated load - * balancing state. The round-robin picker algorithm is implemented by method - * {@link #createReadyPicker}. + * A {@link AbstractRoundRobinLoadBalancer} that provides round-robin load-balancing over the {@link + * EquivalentAddressGroup}s from the {@link NameResolver}. */ -abstract class RoundRobinLoadBalancer extends LoadBalancer { - @VisibleForTesting - static final Attributes.Key> STATE_INFO = - Attributes.Key.create("state-info"); - - private final Helper helper; - private final Map subchannels = - new HashMap<>(); - private final Random random; - - private ConnectivityState currentState; - private RoundRobinPicker currentPicker = new EmptyPicker(EMPTY_OK); +final class RoundRobinLoadBalancer extends AbstractRoundRobinLoadBalancer { RoundRobinLoadBalancer(Helper helper) { - this.helper = checkNotNull(helper, "helper"); - this.random = new Random(); - } - - abstract Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args); - - abstract RoundRobinPicker createReadyPicker(List activeSubchannelList, - int startIndex); - - @Override - public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { - if (resolvedAddresses.getAddresses().isEmpty()) { - handleNameResolutionError(Status.UNAVAILABLE.withDescription( - "NameResolver returned no usable address. addrs=" + resolvedAddresses.getAddresses() - + ", attrs=" + resolvedAddresses.getAttributes())); - return false; - } - - List servers = resolvedAddresses.getAddresses(); - Set currentAddrs = subchannels.keySet(); - Map latestAddrs = stripAttrs(servers); - Set removedAddrs = setsDifference(currentAddrs, latestAddrs.keySet()); - - for (Map.Entry latestEntry : - latestAddrs.entrySet()) { - EquivalentAddressGroup strippedAddressGroup = latestEntry.getKey(); - EquivalentAddressGroup originalAddressGroup = latestEntry.getValue(); - Subchannel existingSubchannel = subchannels.get(strippedAddressGroup); - if (existingSubchannel != null) { - // EAG's Attributes may have changed. - existingSubchannel.updateAddresses(Collections.singletonList(originalAddressGroup)); - continue; - } - // Create new subchannels for new addresses. - - // NB(lukaszx0): we don't merge `attributes` with `subchannelAttr` because subchannel - // doesn't need them. They're describing the resolved server list but we're not taking - // any action based on this information. - Attributes.Builder subchannelAttrs = Attributes.newBuilder() - // NB(lukaszx0): because attributes are immutable we can't set new value for the key - // after creation but since we can mutate the values we leverage that and set - // AtomicReference which will allow mutating state info for given channel. - .set(STATE_INFO, - new Ref<>(ConnectivityStateInfo.forNonError(IDLE))); - - final Subchannel subchannel = checkNotNull( - createSubchannel(helper, CreateSubchannelArgs.newBuilder() - .setAddresses(originalAddressGroup) - .setAttributes(subchannelAttrs.build()) - .build()), - "subchannel"); - subchannel.start(new SubchannelStateListener() { - @Override - public void onSubchannelState(ConnectivityStateInfo state) { - processSubchannelState(subchannel, state); - } - }); - subchannels.put(strippedAddressGroup, subchannel); - subchannel.requestConnection(); - } - - ArrayList removedSubchannels = new ArrayList<>(); - for (EquivalentAddressGroup addressGroup : removedAddrs) { - removedSubchannels.add(subchannels.remove(addressGroup)); - } - - // Update the picker before shutting down the subchannels, to reduce the chance of the race - // between picking a subchannel and shutting it down. - updateBalancingState(); - - // Shutdown removed subchannels - for (Subchannel removedSubchannel : removedSubchannels) { - shutdownSubchannel(removedSubchannel); - } - - return true; + super(helper); } @Override - public void handleNameResolutionError(Status error) { - if (currentState != READY) { - updateBalancingState(TRANSIENT_FAILURE, new EmptyPicker(error)); - } - } - - private void processSubchannelState(Subchannel subchannel, ConnectivityStateInfo stateInfo) { - if (subchannels.get(stripAttrs(subchannel.getAddresses())) != subchannel) { - return; - } - if (stateInfo.getState() == TRANSIENT_FAILURE || stateInfo.getState() == IDLE) { - helper.refreshNameResolution(); - } - if (stateInfo.getState() == IDLE) { - subchannel.requestConnection(); - } - Ref subchannelStateRef = getSubchannelStateInfoRef(subchannel); - if (subchannelStateRef.value.getState().equals(TRANSIENT_FAILURE)) { - if (stateInfo.getState().equals(CONNECTING) || stateInfo.getState().equals(IDLE)) { - return; - } - } - subchannelStateRef.value = stateInfo; - updateBalancingState(); - } - - private void shutdownSubchannel(Subchannel subchannel) { - subchannel.shutdown(); - getSubchannelStateInfoRef(subchannel).value = - ConnectivityStateInfo.forNonError(SHUTDOWN); + Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args) { + return helper.createSubchannel(args); } @Override - public void shutdown() { - for (Subchannel subchannel : getSubchannels()) { - shutdownSubchannel(subchannel); - } - subchannels.clear(); - } - - private static final Status EMPTY_OK = Status.OK.withDescription("no subchannels ready"); - - /** - * Updates picker with the list of active subchannels (state == READY). - */ - @SuppressWarnings("ReferenceEquality") - private void updateBalancingState() { - List activeList = filterNonFailingSubchannels(getSubchannels()); - if (activeList.isEmpty()) { - // No READY subchannels, determine aggregate state and error status - boolean isConnecting = false; - Status aggStatus = EMPTY_OK; - for (Subchannel subchannel : getSubchannels()) { - ConnectivityStateInfo stateInfo = getSubchannelStateInfoRef(subchannel).value; - // This subchannel IDLE is not because of channel IDLE_TIMEOUT, - // in which case LB is already shutdown. - // RRLB will request connection immediately on subchannel IDLE. - if (stateInfo.getState() == CONNECTING || stateInfo.getState() == IDLE) { - isConnecting = true; - } - if (aggStatus == EMPTY_OK || !aggStatus.isOk()) { - aggStatus = stateInfo.getStatus(); - } - } - updateBalancingState(isConnecting ? CONNECTING : TRANSIENT_FAILURE, - // If all subchannels are TRANSIENT_FAILURE, return the Status associated with - // an arbitrary subchannel, otherwise return OK. - new EmptyPicker(aggStatus)); - } else { - // initialize the Picker to a random start index to ensure that a high frequency of Picker - // churn does not skew subchannel selection. - int startIndex = random.nextInt(activeList.size()); - updateBalancingState(READY, createReadyPicker(activeList, startIndex)); - } - } - - private void updateBalancingState(ConnectivityState state, RoundRobinPicker picker) { - if (state != currentState || !picker.isEquivalentTo(currentPicker)) { - helper.updateBalancingState(state, picker); - currentState = state; - currentPicker = picker; - } - } - - /** - * Filters out non-ready subchannels. - */ - private static List filterNonFailingSubchannels( - Collection subchannels) { - List readySubchannels = new ArrayList<>(subchannels.size()); - for (Subchannel subchannel : subchannels) { - if (isReady(subchannel)) { - readySubchannels.add(subchannel); - } - } - return readySubchannels; - } - - /** - * Converts list of {@link EquivalentAddressGroup} to {@link EquivalentAddressGroup} set and - * remove all attributes. The values are the original EAGs. - */ - private static Map stripAttrs( - List groupList) { - Map addrs = new HashMap<>(groupList.size() * 2); - for (EquivalentAddressGroup group : groupList) { - addrs.put(stripAttrs(group), group); - } - return addrs; - } - - private static EquivalentAddressGroup stripAttrs(EquivalentAddressGroup eag) { - return new EquivalentAddressGroup(eag.getAddresses()); + RoundRobinPicker createReadyPicker(List activeSubchannelList, int startIndex) { + return new ReadyPicker(activeSubchannelList, startIndex); } @VisibleForTesting - Collection getSubchannels() { - return subchannels.values(); - } + static final class ReadyPicker extends RoundRobinPicker { + private static final AtomicIntegerFieldUpdater indexUpdater = + AtomicIntegerFieldUpdater.newUpdater(ReadyPicker.class, "index"); - private static Ref getSubchannelStateInfoRef( - Subchannel subchannel) { - return checkNotNull(subchannel.getAttributes().get(STATE_INFO), "STATE_INFO"); - } - - // package-private to avoid synthetic access - static boolean isReady(Subchannel subchannel) { - return getSubchannelStateInfoRef(subchannel).value.getState() == READY; - } + private final List list; // non-empty + @SuppressWarnings("unused") + private volatile int index; - private static Set setsDifference(Set a, Set b) { - Set aCopy = new HashSet<>(a); - aCopy.removeAll(b); - return aCopy; - } - - // Only subclasses are ReadyPicker or EmptyPicker - abstract static class RoundRobinPicker extends SubchannelPicker { - abstract boolean isEquivalentTo(RoundRobinPicker picker); - } - - @VisibleForTesting - static final class EmptyPicker extends RoundRobinPicker { - - private final Status status; - - EmptyPicker(@Nonnull Status status) { - this.status = Preconditions.checkNotNull(status, "status"); + ReadyPicker(List list, int startIndex) { + Preconditions.checkArgument(!list.isEmpty(), "empty list"); + this.list = list; + this.index = startIndex - 1; } @Override public PickResult pickSubchannel(PickSubchannelArgs args) { - return status.isOk() ? PickResult.withNoResult() : PickResult.withError(status); + return PickResult.withSubchannel(nextSubchannel()); } @Override - boolean isEquivalentTo(RoundRobinPicker picker) { - return picker instanceof EmptyPicker && (Objects.equal(status, ((EmptyPicker) picker).status) - || (status.isOk() && ((EmptyPicker) picker).status.isOk())); + public String toString() { + return MoreObjects.toStringHelper(ReadyPicker.class).add("list", list).toString(); } - @Override - public String toString() { - return MoreObjects.toStringHelper(EmptyPicker.class).add("status", status).toString(); + private Subchannel nextSubchannel() { + int size = list.size(); + int i = indexUpdater.incrementAndGet(this); + if (i >= size) { + int oldi = i; + i %= size; + indexUpdater.compareAndSet(this, oldi, i); + } + return list.get(i); } - } - /** - * A lighter weight Reference than AtomicReference. - */ - @VisibleForTesting - static final class Ref { - T value; + @VisibleForTesting + List getList() { + return list; + } - Ref(T value) { - this.value = value; + @Override + boolean isEquivalentTo(RoundRobinPicker picker) { + if (!(picker instanceof ReadyPicker)) { + return false; + } + ReadyPicker other = (ReadyPicker) picker; + // the lists cannot contain duplicate subchannels + return other == this + || (list.size() == other.list.size() && new HashSet<>(list).containsAll(other.list)); } } } diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancerImpl.java b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancerImpl.java deleted file mode 100644 index d65ac5b4e42..00000000000 --- a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancerImpl.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2016 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 io.grpc.util; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.MoreObjects; -import com.google.common.base.Preconditions; -import io.grpc.EquivalentAddressGroup; -import io.grpc.NameResolver; -import java.util.HashSet; -import java.util.List; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; - -/** - * A {@link RoundRobinLoadBalancer} that provides round-robin load-balancing over the {@link - * EquivalentAddressGroup}s from the {@link NameResolver}. - */ -final class RoundRobinLoadBalancerImpl extends RoundRobinLoadBalancer { - - RoundRobinLoadBalancerImpl(Helper helper) { - super(helper); - } - - @Override - Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args) { - return helper.createSubchannel(args); - } - - @Override - RoundRobinPicker createReadyPicker(List activeSubchannelList, int startIndex) { - return new ReadyPicker(activeSubchannelList, startIndex); - } - - @VisibleForTesting - static final class ReadyPicker extends RoundRobinPicker { - private static final AtomicIntegerFieldUpdater indexUpdater = - AtomicIntegerFieldUpdater.newUpdater(ReadyPicker.class, "index"); - - private final List list; // non-empty - @SuppressWarnings("unused") - private volatile int index; - - ReadyPicker(List list, int startIndex) { - Preconditions.checkArgument(!list.isEmpty(), "empty list"); - this.list = list; - this.index = startIndex - 1; - } - - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - return PickResult.withSubchannel(nextSubchannel()); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(ReadyPicker.class).add("list", list).toString(); - } - - private Subchannel nextSubchannel() { - int size = list.size(); - int i = indexUpdater.incrementAndGet(this); - if (i >= size) { - int oldi = i; - i %= size; - indexUpdater.compareAndSet(this, oldi, i); - } - return list.get(i); - } - - @VisibleForTesting - List getList() { - return list; - } - - @Override - boolean isEquivalentTo(RoundRobinPicker picker) { - if (!(picker instanceof ReadyPicker)) { - return false; - } - ReadyPicker other = (ReadyPicker) picker; - // the lists cannot contain duplicate subchannels - return other == this - || (list.size() == other.list.size() && new HashSet<>(list).containsAll(other.list)); - } - } -} diff --git a/core/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java b/core/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java index 9cb794649e9..7843832cd5a 100644 --- a/core/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java +++ b/core/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java @@ -52,7 +52,7 @@ public String getPolicyName() { @Override public LoadBalancer newLoadBalancer(LoadBalancer.Helper helper) { - return new RoundRobinLoadBalancerImpl(helper); + return new RoundRobinLoadBalancer(helper); } @Override diff --git a/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java b/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java index 72e373a04fd..ad886c31142 100644 --- a/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java +++ b/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java @@ -405,7 +405,7 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { .build()); assertThat(addressesAccepted).isTrue(); assertThat(lb.getDelegate().getClass().getName()) - .isEqualTo("io.grpc.util.RoundRobinLoadBalancerImpl"); + .isEqualTo("io.grpc.util.RoundRobinLoadBalancer"); } @Test diff --git a/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java b/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java index 778dd60ec2c..ccf3d40cdb6 100644 --- a/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java +++ b/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java @@ -123,7 +123,7 @@ public LoadBalancer newLoadBalancer(Helper helper) { "round_robin") { @Override public LoadBalancer newLoadBalancer(Helper helper) { - return new RoundRobinLoadBalancerImpl(helper); + return new RoundRobinLoadBalancer(helper); } }; diff --git a/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java b/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java index 293b65e61e1..dbef5f32b57 100644 --- a/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java +++ b/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java @@ -22,7 +22,7 @@ import static io.grpc.ConnectivityState.READY; import static io.grpc.ConnectivityState.SHUTDOWN; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; -import static io.grpc.util.RoundRobinLoadBalancerImpl.STATE_INFO; +import static io.grpc.util.RoundRobinLoadBalancer.STATE_INFO; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -55,9 +55,9 @@ import io.grpc.LoadBalancer.SubchannelPicker; import io.grpc.LoadBalancer.SubchannelStateListener; import io.grpc.Status; -import io.grpc.util.RoundRobinLoadBalancer.EmptyPicker; -import io.grpc.util.RoundRobinLoadBalancer.Ref; -import io.grpc.util.RoundRobinLoadBalancerImpl.ReadyPicker; +import io.grpc.util.AbstractRoundRobinLoadBalancer.EmptyPicker; +import io.grpc.util.AbstractRoundRobinLoadBalancer.Ref; +import io.grpc.util.RoundRobinLoadBalancer.ReadyPicker; import java.net.SocketAddress; import java.util.ArrayList; import java.util.Arrays; @@ -78,12 +78,12 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -/** Unit test for {@link RoundRobinLoadBalancerImpl}. */ +/** Unit test for {@link RoundRobinLoadBalancer}. */ @RunWith(JUnit4.class) public class RoundRobinLoadBalancerTest { private static final Attributes.Key MAJOR_KEY = Attributes.Key.create("major-key"); - private RoundRobinLoadBalancerImpl loadBalancer; + private RoundRobinLoadBalancer loadBalancer; private final List servers = Lists.newArrayList(); private final Map, Subchannel> subchannels = Maps.newLinkedHashMap(); private final Map subchannelStateListeners = @@ -136,7 +136,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { } }); - loadBalancer = new RoundRobinLoadBalancerImpl(mockHelper); + loadBalancer = new RoundRobinLoadBalancer(mockHelper); } @After From 12e79282c69af76de94164cc01945b852b72eccb Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Mon, 23 Jan 2023 13:59:55 -0500 Subject: [PATCH 03/25] refactor round robin LB --- .../io/grpc/util/RoundRobinLoadBalancer.java | 74 +++---------- .../grpc/util/RoundRobinLoadBalancerImpl.java | 100 ++++++++++++++++++ .../SecretRoundRobinLoadBalancerProvider.java | 2 +- ...AutoConfiguredLoadBalancerFactoryTest.java | 2 +- .../OutlierDetectionLoadBalancerTest.java | 2 +- .../grpc/util/RoundRobinLoadBalancerTest.java | 10 +- 6 files changed, 122 insertions(+), 68 deletions(-) create mode 100644 core/src/main/java/io/grpc/util/RoundRobinLoadBalancerImpl.java diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java index b715f756144..c6d2fa4f199 100644 --- a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java @@ -43,14 +43,16 @@ import java.util.Map; import java.util.Random; import java.util.Set; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import javax.annotation.Nonnull; /** - * A {@link LoadBalancer} that provides round-robin load-balancing over the {@link - * EquivalentAddressGroup}s from the {@link NameResolver}. + * A {@link LoadBalancer} that provides load-balancing over the {@link + * EquivalentAddressGroup}s from the {@link NameResolver}. It provides default implementation of + * accepting {@link io.grpc.LoadBalancer.ResolvedAddresses} updates, process aggregated load + * balancing state. The round-robin picker algorithm is implemented by method + * {@link #createReadyPicker}. */ -final class RoundRobinLoadBalancer extends LoadBalancer { +abstract class RoundRobinLoadBalancer extends LoadBalancer { @VisibleForTesting static final Attributes.Key> STATE_INFO = Attributes.Key.create("state-info"); @@ -68,6 +70,11 @@ final class RoundRobinLoadBalancer extends LoadBalancer { this.random = new Random(); } + abstract Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args); + + abstract RoundRobinPicker createReadyPicker(List activeSubchannelList, + int startIndex); + @Override public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { if (resolvedAddresses.getAddresses().isEmpty()) { @@ -105,7 +112,7 @@ public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { new Ref<>(ConnectivityStateInfo.forNonError(IDLE))); final Subchannel subchannel = checkNotNull( - helper.createSubchannel(CreateSubchannelArgs.newBuilder() + createSubchannel(helper, CreateSubchannelArgs.newBuilder() .setAddresses(originalAddressGroup) .setAttributes(subchannelAttrs.build()) .build()), @@ -210,7 +217,7 @@ private void updateBalancingState() { // initialize the Picker to a random start index to ensure that a high frequency of Picker // churn does not skew subchannel selection. int startIndex = random.nextInt(activeList.size()); - updateBalancingState(READY, new ReadyPicker(activeList, startIndex)); + updateBalancingState(READY, createReadyPicker(activeList, startIndex)); } } @@ -275,63 +282,10 @@ private static Set setsDifference(Set a, Set b) { } // Only subclasses are ReadyPicker or EmptyPicker - private abstract static class RoundRobinPicker extends SubchannelPicker { + abstract static class RoundRobinPicker extends SubchannelPicker { abstract boolean isEquivalentTo(RoundRobinPicker picker); } - @VisibleForTesting - static final class ReadyPicker extends RoundRobinPicker { - private static final AtomicIntegerFieldUpdater indexUpdater = - AtomicIntegerFieldUpdater.newUpdater(ReadyPicker.class, "index"); - - private final List list; // non-empty - @SuppressWarnings("unused") - private volatile int index; - - ReadyPicker(List list, int startIndex) { - Preconditions.checkArgument(!list.isEmpty(), "empty list"); - this.list = list; - this.index = startIndex - 1; - } - - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - return PickResult.withSubchannel(nextSubchannel()); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(ReadyPicker.class).add("list", list).toString(); - } - - private Subchannel nextSubchannel() { - int size = list.size(); - int i = indexUpdater.incrementAndGet(this); - if (i >= size) { - int oldi = i; - i %= size; - indexUpdater.compareAndSet(this, oldi, i); - } - return list.get(i); - } - - @VisibleForTesting - List getList() { - return list; - } - - @Override - boolean isEquivalentTo(RoundRobinPicker picker) { - if (!(picker instanceof ReadyPicker)) { - return false; - } - ReadyPicker other = (ReadyPicker) picker; - // the lists cannot contain duplicate subchannels - return other == this - || (list.size() == other.list.size() && new HashSet<>(list).containsAll(other.list)); - } - } - @VisibleForTesting static final class EmptyPicker extends RoundRobinPicker { diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancerImpl.java b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancerImpl.java new file mode 100644 index 00000000000..d65ac5b4e42 --- /dev/null +++ b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancerImpl.java @@ -0,0 +1,100 @@ +/* + * Copyright 2016 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 io.grpc.util; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; +import io.grpc.EquivalentAddressGroup; +import io.grpc.NameResolver; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +/** + * A {@link RoundRobinLoadBalancer} that provides round-robin load-balancing over the {@link + * EquivalentAddressGroup}s from the {@link NameResolver}. + */ +final class RoundRobinLoadBalancerImpl extends RoundRobinLoadBalancer { + + RoundRobinLoadBalancerImpl(Helper helper) { + super(helper); + } + + @Override + Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args) { + return helper.createSubchannel(args); + } + + @Override + RoundRobinPicker createReadyPicker(List activeSubchannelList, int startIndex) { + return new ReadyPicker(activeSubchannelList, startIndex); + } + + @VisibleForTesting + static final class ReadyPicker extends RoundRobinPicker { + private static final AtomicIntegerFieldUpdater indexUpdater = + AtomicIntegerFieldUpdater.newUpdater(ReadyPicker.class, "index"); + + private final List list; // non-empty + @SuppressWarnings("unused") + private volatile int index; + + ReadyPicker(List list, int startIndex) { + Preconditions.checkArgument(!list.isEmpty(), "empty list"); + this.list = list; + this.index = startIndex - 1; + } + + @Override + public PickResult pickSubchannel(PickSubchannelArgs args) { + return PickResult.withSubchannel(nextSubchannel()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(ReadyPicker.class).add("list", list).toString(); + } + + private Subchannel nextSubchannel() { + int size = list.size(); + int i = indexUpdater.incrementAndGet(this); + if (i >= size) { + int oldi = i; + i %= size; + indexUpdater.compareAndSet(this, oldi, i); + } + return list.get(i); + } + + @VisibleForTesting + List getList() { + return list; + } + + @Override + boolean isEquivalentTo(RoundRobinPicker picker) { + if (!(picker instanceof ReadyPicker)) { + return false; + } + ReadyPicker other = (ReadyPicker) picker; + // the lists cannot contain duplicate subchannels + return other == this + || (list.size() == other.list.size() && new HashSet<>(list).containsAll(other.list)); + } + } +} diff --git a/core/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java b/core/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java index 7843832cd5a..9cb794649e9 100644 --- a/core/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java +++ b/core/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java @@ -52,7 +52,7 @@ public String getPolicyName() { @Override public LoadBalancer newLoadBalancer(LoadBalancer.Helper helper) { - return new RoundRobinLoadBalancer(helper); + return new RoundRobinLoadBalancerImpl(helper); } @Override diff --git a/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java b/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java index ad886c31142..72e373a04fd 100644 --- a/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java +++ b/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java @@ -405,7 +405,7 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { .build()); assertThat(addressesAccepted).isTrue(); assertThat(lb.getDelegate().getClass().getName()) - .isEqualTo("io.grpc.util.RoundRobinLoadBalancer"); + .isEqualTo("io.grpc.util.RoundRobinLoadBalancerImpl"); } @Test diff --git a/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java b/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java index ccf3d40cdb6..778dd60ec2c 100644 --- a/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java +++ b/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java @@ -123,7 +123,7 @@ public LoadBalancer newLoadBalancer(Helper helper) { "round_robin") { @Override public LoadBalancer newLoadBalancer(Helper helper) { - return new RoundRobinLoadBalancer(helper); + return new RoundRobinLoadBalancerImpl(helper); } }; diff --git a/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java b/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java index d4c07e3d50e..293b65e61e1 100644 --- a/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java +++ b/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java @@ -22,7 +22,7 @@ import static io.grpc.ConnectivityState.READY; import static io.grpc.ConnectivityState.SHUTDOWN; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; -import static io.grpc.util.RoundRobinLoadBalancer.STATE_INFO; +import static io.grpc.util.RoundRobinLoadBalancerImpl.STATE_INFO; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -56,8 +56,8 @@ import io.grpc.LoadBalancer.SubchannelStateListener; import io.grpc.Status; import io.grpc.util.RoundRobinLoadBalancer.EmptyPicker; -import io.grpc.util.RoundRobinLoadBalancer.ReadyPicker; import io.grpc.util.RoundRobinLoadBalancer.Ref; +import io.grpc.util.RoundRobinLoadBalancerImpl.ReadyPicker; import java.net.SocketAddress; import java.util.ArrayList; import java.util.Arrays; @@ -78,12 +78,12 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -/** Unit test for {@link RoundRobinLoadBalancer}. */ +/** Unit test for {@link RoundRobinLoadBalancerImpl}. */ @RunWith(JUnit4.class) public class RoundRobinLoadBalancerTest { private static final Attributes.Key MAJOR_KEY = Attributes.Key.create("major-key"); - private RoundRobinLoadBalancer loadBalancer; + private RoundRobinLoadBalancerImpl loadBalancer; private final List servers = Lists.newArrayList(); private final Map, Subchannel> subchannels = Maps.newLinkedHashMap(); private final Map subchannelStateListeners = @@ -136,7 +136,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { } }); - loadBalancer = new RoundRobinLoadBalancer(mockHelper); + loadBalancer = new RoundRobinLoadBalancerImpl(mockHelper); } @After From 2727d9bf79788b599931df96bc363de580e824e5 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Wed, 25 Jan 2023 10:45:49 -0800 Subject: [PATCH 04/25] rename abstract* --- .../util/AbstractRoundRobinLoadBalancer.java | 326 ++++++++++++++++++ .../io/grpc/util/RoundRobinLoadBalancer.java | 314 +++-------------- .../grpc/util/RoundRobinLoadBalancerImpl.java | 100 ------ .../SecretRoundRobinLoadBalancerProvider.java | 2 +- ...AutoConfiguredLoadBalancerFactoryTest.java | 2 +- .../OutlierDetectionLoadBalancerTest.java | 2 +- .../grpc/util/RoundRobinLoadBalancerTest.java | 14 +- 7 files changed, 380 insertions(+), 380 deletions(-) create mode 100644 core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java delete mode 100644 core/src/main/java/io/grpc/util/RoundRobinLoadBalancerImpl.java diff --git a/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java new file mode 100644 index 00000000000..90318bb49c6 --- /dev/null +++ b/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java @@ -0,0 +1,326 @@ +/* + * Copyright 2016 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 io.grpc.util; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.ConnectivityState.CONNECTING; +import static io.grpc.ConnectivityState.IDLE; +import static io.grpc.ConnectivityState.READY; +import static io.grpc.ConnectivityState.SHUTDOWN; +import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import io.grpc.Attributes; +import io.grpc.ConnectivityState; +import io.grpc.ConnectivityStateInfo; +import io.grpc.EquivalentAddressGroup; +import io.grpc.LoadBalancer; +import io.grpc.NameResolver; +import io.grpc.Status; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import javax.annotation.Nonnull; + +/** + * A {@link LoadBalancer} that provides load-balancing over the {@link + * EquivalentAddressGroup}s from the {@link NameResolver}. It provides default implementation of + * accepting {@link io.grpc.LoadBalancer.ResolvedAddresses} updates, process aggregated load + * balancing state. The round-robin picker algorithm is implemented by method + * {@link #createReadyPicker}. + */ +abstract class AbstractRoundRobinLoadBalancer extends LoadBalancer { + @VisibleForTesting + static final Attributes.Key> STATE_INFO = + Attributes.Key.create("state-info"); + + private final Helper helper; + private final Map subchannels = + new HashMap<>(); + private final Random random; + + private ConnectivityState currentState; + private RoundRobinPicker currentPicker = new EmptyPicker(EMPTY_OK); + + AbstractRoundRobinLoadBalancer(Helper helper) { + this.helper = checkNotNull(helper, "helper"); + this.random = new Random(); + } + + abstract Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args); + + abstract RoundRobinPicker createReadyPicker(List activeSubchannelList, + int startIndex); + + @Override + public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { + if (resolvedAddresses.getAddresses().isEmpty()) { + handleNameResolutionError(Status.UNAVAILABLE.withDescription( + "NameResolver returned no usable address. addrs=" + resolvedAddresses.getAddresses() + + ", attrs=" + resolvedAddresses.getAttributes())); + return false; + } + + List servers = resolvedAddresses.getAddresses(); + Set currentAddrs = subchannels.keySet(); + Map latestAddrs = stripAttrs(servers); + Set removedAddrs = setsDifference(currentAddrs, latestAddrs.keySet()); + + for (Map.Entry latestEntry : + latestAddrs.entrySet()) { + EquivalentAddressGroup strippedAddressGroup = latestEntry.getKey(); + EquivalentAddressGroup originalAddressGroup = latestEntry.getValue(); + Subchannel existingSubchannel = subchannels.get(strippedAddressGroup); + if (existingSubchannel != null) { + // EAG's Attributes may have changed. + existingSubchannel.updateAddresses(Collections.singletonList(originalAddressGroup)); + continue; + } + // Create new subchannels for new addresses. + + // NB(lukaszx0): we don't merge `attributes` with `subchannelAttr` because subchannel + // doesn't need them. They're describing the resolved server list but we're not taking + // any action based on this information. + Attributes.Builder subchannelAttrs = Attributes.newBuilder() + // NB(lukaszx0): because attributes are immutable we can't set new value for the key + // after creation but since we can mutate the values we leverage that and set + // AtomicReference which will allow mutating state info for given channel. + .set(STATE_INFO, + new Ref<>(ConnectivityStateInfo.forNonError(IDLE))); + + final Subchannel subchannel = checkNotNull( + createSubchannel(helper, CreateSubchannelArgs.newBuilder() + .setAddresses(originalAddressGroup) + .setAttributes(subchannelAttrs.build()) + .build()), + "subchannel"); + subchannel.start(new SubchannelStateListener() { + @Override + public void onSubchannelState(ConnectivityStateInfo state) { + processSubchannelState(subchannel, state); + } + }); + subchannels.put(strippedAddressGroup, subchannel); + subchannel.requestConnection(); + } + + ArrayList removedSubchannels = new ArrayList<>(); + for (EquivalentAddressGroup addressGroup : removedAddrs) { + removedSubchannels.add(subchannels.remove(addressGroup)); + } + + // Update the picker before shutting down the subchannels, to reduce the chance of the race + // between picking a subchannel and shutting it down. + updateBalancingState(); + + // Shutdown removed subchannels + for (Subchannel removedSubchannel : removedSubchannels) { + shutdownSubchannel(removedSubchannel); + } + + return true; + } + + @Override + public void handleNameResolutionError(Status error) { + if (currentState != READY) { + updateBalancingState(TRANSIENT_FAILURE, new EmptyPicker(error)); + } + } + + private void processSubchannelState(Subchannel subchannel, ConnectivityStateInfo stateInfo) { + if (subchannels.get(stripAttrs(subchannel.getAddresses())) != subchannel) { + return; + } + if (stateInfo.getState() == TRANSIENT_FAILURE || stateInfo.getState() == IDLE) { + helper.refreshNameResolution(); + } + if (stateInfo.getState() == IDLE) { + subchannel.requestConnection(); + } + Ref subchannelStateRef = getSubchannelStateInfoRef(subchannel); + if (subchannelStateRef.value.getState().equals(TRANSIENT_FAILURE)) { + if (stateInfo.getState().equals(CONNECTING) || stateInfo.getState().equals(IDLE)) { + return; + } + } + subchannelStateRef.value = stateInfo; + updateBalancingState(); + } + + private void shutdownSubchannel(Subchannel subchannel) { + subchannel.shutdown(); + getSubchannelStateInfoRef(subchannel).value = + ConnectivityStateInfo.forNonError(SHUTDOWN); + } + + @Override + public void shutdown() { + for (Subchannel subchannel : getSubchannels()) { + shutdownSubchannel(subchannel); + } + subchannels.clear(); + } + + private static final Status EMPTY_OK = Status.OK.withDescription("no subchannels ready"); + + /** + * Updates picker with the list of active subchannels (state == READY). + */ + @SuppressWarnings("ReferenceEquality") + private void updateBalancingState() { + List activeList = filterNonFailingSubchannels(getSubchannels()); + if (activeList.isEmpty()) { + // No READY subchannels, determine aggregate state and error status + boolean isConnecting = false; + Status aggStatus = EMPTY_OK; + for (Subchannel subchannel : getSubchannels()) { + ConnectivityStateInfo stateInfo = getSubchannelStateInfoRef(subchannel).value; + // This subchannel IDLE is not because of channel IDLE_TIMEOUT, + // in which case LB is already shutdown. + // RRLB will request connection immediately on subchannel IDLE. + if (stateInfo.getState() == CONNECTING || stateInfo.getState() == IDLE) { + isConnecting = true; + } + if (aggStatus == EMPTY_OK || !aggStatus.isOk()) { + aggStatus = stateInfo.getStatus(); + } + } + updateBalancingState(isConnecting ? CONNECTING : TRANSIENT_FAILURE, + // If all subchannels are TRANSIENT_FAILURE, return the Status associated with + // an arbitrary subchannel, otherwise return OK. + new EmptyPicker(aggStatus)); + } else { + // initialize the Picker to a random start index to ensure that a high frequency of Picker + // churn does not skew subchannel selection. + int startIndex = random.nextInt(activeList.size()); + updateBalancingState(READY, createReadyPicker(activeList, startIndex)); + } + } + + private void updateBalancingState(ConnectivityState state, RoundRobinPicker picker) { + if (state != currentState || !picker.isEquivalentTo(currentPicker)) { + helper.updateBalancingState(state, picker); + currentState = state; + currentPicker = picker; + } + } + + /** + * Filters out non-ready subchannels. + */ + private static List filterNonFailingSubchannels( + Collection subchannels) { + List readySubchannels = new ArrayList<>(subchannels.size()); + for (Subchannel subchannel : subchannels) { + if (isReady(subchannel)) { + readySubchannels.add(subchannel); + } + } + return readySubchannels; + } + + /** + * Converts list of {@link EquivalentAddressGroup} to {@link EquivalentAddressGroup} set and + * remove all attributes. The values are the original EAGs. + */ + private static Map stripAttrs( + List groupList) { + Map addrs = new HashMap<>(groupList.size() * 2); + for (EquivalentAddressGroup group : groupList) { + addrs.put(stripAttrs(group), group); + } + return addrs; + } + + private static EquivalentAddressGroup stripAttrs(EquivalentAddressGroup eag) { + return new EquivalentAddressGroup(eag.getAddresses()); + } + + @VisibleForTesting + Collection getSubchannels() { + return subchannels.values(); + } + + private static Ref getSubchannelStateInfoRef( + Subchannel subchannel) { + return checkNotNull(subchannel.getAttributes().get(STATE_INFO), "STATE_INFO"); + } + + // package-private to avoid synthetic access + static boolean isReady(Subchannel subchannel) { + return getSubchannelStateInfoRef(subchannel).value.getState() == READY; + } + + private static Set setsDifference(Set a, Set b) { + Set aCopy = new HashSet<>(a); + aCopy.removeAll(b); + return aCopy; + } + + // Only subclasses are ReadyPicker or EmptyPicker + abstract static class RoundRobinPicker extends SubchannelPicker { + abstract boolean isEquivalentTo(RoundRobinPicker picker); + } + + @VisibleForTesting + static final class EmptyPicker extends RoundRobinPicker { + + private final Status status; + + EmptyPicker(@Nonnull Status status) { + this.status = Preconditions.checkNotNull(status, "status"); + } + + @Override + public PickResult pickSubchannel(PickSubchannelArgs args) { + return status.isOk() ? PickResult.withNoResult() : PickResult.withError(status); + } + + @Override + boolean isEquivalentTo(RoundRobinPicker picker) { + return picker instanceof EmptyPicker && (Objects.equal(status, ((EmptyPicker) picker).status) + || (status.isOk() && ((EmptyPicker) picker).status.isOk())); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(EmptyPicker.class).add("status", status).toString(); + } + } + + /** + * A lighter weight Reference than AtomicReference. + */ + @VisibleForTesting + static final class Ref { + T value; + + Ref(T value) { + this.value = value; + } + } +} diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java index c6d2fa4f199..8f0fbd1b2ec 100644 --- a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java @@ -16,311 +16,85 @@ package io.grpc.util; -import static com.google.common.base.Preconditions.checkNotNull; -import static io.grpc.ConnectivityState.CONNECTING; -import static io.grpc.ConnectivityState.IDLE; -import static io.grpc.ConnectivityState.READY; -import static io.grpc.ConnectivityState.SHUTDOWN; -import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; - import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; -import com.google.common.base.Objects; import com.google.common.base.Preconditions; -import io.grpc.Attributes; -import io.grpc.ConnectivityState; -import io.grpc.ConnectivityStateInfo; import io.grpc.EquivalentAddressGroup; -import io.grpc.LoadBalancer; import io.grpc.NameResolver; -import io.grpc.Status; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Set; -import javax.annotation.Nonnull; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; /** - * A {@link LoadBalancer} that provides load-balancing over the {@link - * EquivalentAddressGroup}s from the {@link NameResolver}. It provides default implementation of - * accepting {@link io.grpc.LoadBalancer.ResolvedAddresses} updates, process aggregated load - * balancing state. The round-robin picker algorithm is implemented by method - * {@link #createReadyPicker}. + * A {@link AbstractRoundRobinLoadBalancer} that provides round-robin load-balancing over the {@link + * EquivalentAddressGroup}s from the {@link NameResolver}. */ -abstract class RoundRobinLoadBalancer extends LoadBalancer { - @VisibleForTesting - static final Attributes.Key> STATE_INFO = - Attributes.Key.create("state-info"); - - private final Helper helper; - private final Map subchannels = - new HashMap<>(); - private final Random random; - - private ConnectivityState currentState; - private RoundRobinPicker currentPicker = new EmptyPicker(EMPTY_OK); +final class RoundRobinLoadBalancer extends AbstractRoundRobinLoadBalancer { RoundRobinLoadBalancer(Helper helper) { - this.helper = checkNotNull(helper, "helper"); - this.random = new Random(); - } - - abstract Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args); - - abstract RoundRobinPicker createReadyPicker(List activeSubchannelList, - int startIndex); - - @Override - public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { - if (resolvedAddresses.getAddresses().isEmpty()) { - handleNameResolutionError(Status.UNAVAILABLE.withDescription( - "NameResolver returned no usable address. addrs=" + resolvedAddresses.getAddresses() - + ", attrs=" + resolvedAddresses.getAttributes())); - return false; - } - - List servers = resolvedAddresses.getAddresses(); - Set currentAddrs = subchannels.keySet(); - Map latestAddrs = stripAttrs(servers); - Set removedAddrs = setsDifference(currentAddrs, latestAddrs.keySet()); - - for (Map.Entry latestEntry : - latestAddrs.entrySet()) { - EquivalentAddressGroup strippedAddressGroup = latestEntry.getKey(); - EquivalentAddressGroup originalAddressGroup = latestEntry.getValue(); - Subchannel existingSubchannel = subchannels.get(strippedAddressGroup); - if (existingSubchannel != null) { - // EAG's Attributes may have changed. - existingSubchannel.updateAddresses(Collections.singletonList(originalAddressGroup)); - continue; - } - // Create new subchannels for new addresses. - - // NB(lukaszx0): we don't merge `attributes` with `subchannelAttr` because subchannel - // doesn't need them. They're describing the resolved server list but we're not taking - // any action based on this information. - Attributes.Builder subchannelAttrs = Attributes.newBuilder() - // NB(lukaszx0): because attributes are immutable we can't set new value for the key - // after creation but since we can mutate the values we leverage that and set - // AtomicReference which will allow mutating state info for given channel. - .set(STATE_INFO, - new Ref<>(ConnectivityStateInfo.forNonError(IDLE))); - - final Subchannel subchannel = checkNotNull( - createSubchannel(helper, CreateSubchannelArgs.newBuilder() - .setAddresses(originalAddressGroup) - .setAttributes(subchannelAttrs.build()) - .build()), - "subchannel"); - subchannel.start(new SubchannelStateListener() { - @Override - public void onSubchannelState(ConnectivityStateInfo state) { - processSubchannelState(subchannel, state); - } - }); - subchannels.put(strippedAddressGroup, subchannel); - subchannel.requestConnection(); - } - - ArrayList removedSubchannels = new ArrayList<>(); - for (EquivalentAddressGroup addressGroup : removedAddrs) { - removedSubchannels.add(subchannels.remove(addressGroup)); - } - - // Update the picker before shutting down the subchannels, to reduce the chance of the race - // between picking a subchannel and shutting it down. - updateBalancingState(); - - // Shutdown removed subchannels - for (Subchannel removedSubchannel : removedSubchannels) { - shutdownSubchannel(removedSubchannel); - } - - return true; + super(helper); } @Override - public void handleNameResolutionError(Status error) { - if (currentState != READY) { - updateBalancingState(TRANSIENT_FAILURE, new EmptyPicker(error)); - } - } - - private void processSubchannelState(Subchannel subchannel, ConnectivityStateInfo stateInfo) { - if (subchannels.get(stripAttrs(subchannel.getAddresses())) != subchannel) { - return; - } - if (stateInfo.getState() == TRANSIENT_FAILURE || stateInfo.getState() == IDLE) { - helper.refreshNameResolution(); - } - if (stateInfo.getState() == IDLE) { - subchannel.requestConnection(); - } - Ref subchannelStateRef = getSubchannelStateInfoRef(subchannel); - if (subchannelStateRef.value.getState().equals(TRANSIENT_FAILURE)) { - if (stateInfo.getState().equals(CONNECTING) || stateInfo.getState().equals(IDLE)) { - return; - } - } - subchannelStateRef.value = stateInfo; - updateBalancingState(); - } - - private void shutdownSubchannel(Subchannel subchannel) { - subchannel.shutdown(); - getSubchannelStateInfoRef(subchannel).value = - ConnectivityStateInfo.forNonError(SHUTDOWN); + Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args) { + return helper.createSubchannel(args); } @Override - public void shutdown() { - for (Subchannel subchannel : getSubchannels()) { - shutdownSubchannel(subchannel); - } - subchannels.clear(); - } - - private static final Status EMPTY_OK = Status.OK.withDescription("no subchannels ready"); - - /** - * Updates picker with the list of active subchannels (state == READY). - */ - @SuppressWarnings("ReferenceEquality") - private void updateBalancingState() { - List activeList = filterNonFailingSubchannels(getSubchannels()); - if (activeList.isEmpty()) { - // No READY subchannels, determine aggregate state and error status - boolean isConnecting = false; - Status aggStatus = EMPTY_OK; - for (Subchannel subchannel : getSubchannels()) { - ConnectivityStateInfo stateInfo = getSubchannelStateInfoRef(subchannel).value; - // This subchannel IDLE is not because of channel IDLE_TIMEOUT, - // in which case LB is already shutdown. - // RRLB will request connection immediately on subchannel IDLE. - if (stateInfo.getState() == CONNECTING || stateInfo.getState() == IDLE) { - isConnecting = true; - } - if (aggStatus == EMPTY_OK || !aggStatus.isOk()) { - aggStatus = stateInfo.getStatus(); - } - } - updateBalancingState(isConnecting ? CONNECTING : TRANSIENT_FAILURE, - // If all subchannels are TRANSIENT_FAILURE, return the Status associated with - // an arbitrary subchannel, otherwise return OK. - new EmptyPicker(aggStatus)); - } else { - // initialize the Picker to a random start index to ensure that a high frequency of Picker - // churn does not skew subchannel selection. - int startIndex = random.nextInt(activeList.size()); - updateBalancingState(READY, createReadyPicker(activeList, startIndex)); - } - } - - private void updateBalancingState(ConnectivityState state, RoundRobinPicker picker) { - if (state != currentState || !picker.isEquivalentTo(currentPicker)) { - helper.updateBalancingState(state, picker); - currentState = state; - currentPicker = picker; - } - } - - /** - * Filters out non-ready subchannels. - */ - private static List filterNonFailingSubchannels( - Collection subchannels) { - List readySubchannels = new ArrayList<>(subchannels.size()); - for (Subchannel subchannel : subchannels) { - if (isReady(subchannel)) { - readySubchannels.add(subchannel); - } - } - return readySubchannels; - } - - /** - * Converts list of {@link EquivalentAddressGroup} to {@link EquivalentAddressGroup} set and - * remove all attributes. The values are the original EAGs. - */ - private static Map stripAttrs( - List groupList) { - Map addrs = new HashMap<>(groupList.size() * 2); - for (EquivalentAddressGroup group : groupList) { - addrs.put(stripAttrs(group), group); - } - return addrs; - } - - private static EquivalentAddressGroup stripAttrs(EquivalentAddressGroup eag) { - return new EquivalentAddressGroup(eag.getAddresses()); + RoundRobinPicker createReadyPicker(List activeSubchannelList, int startIndex) { + return new ReadyPicker(activeSubchannelList, startIndex); } @VisibleForTesting - Collection getSubchannels() { - return subchannels.values(); - } + static final class ReadyPicker extends RoundRobinPicker { + private static final AtomicIntegerFieldUpdater indexUpdater = + AtomicIntegerFieldUpdater.newUpdater(ReadyPicker.class, "index"); - private static Ref getSubchannelStateInfoRef( - Subchannel subchannel) { - return checkNotNull(subchannel.getAttributes().get(STATE_INFO), "STATE_INFO"); - } - - // package-private to avoid synthetic access - static boolean isReady(Subchannel subchannel) { - return getSubchannelStateInfoRef(subchannel).value.getState() == READY; - } + private final List list; // non-empty + @SuppressWarnings("unused") + private volatile int index; - private static Set setsDifference(Set a, Set b) { - Set aCopy = new HashSet<>(a); - aCopy.removeAll(b); - return aCopy; - } - - // Only subclasses are ReadyPicker or EmptyPicker - abstract static class RoundRobinPicker extends SubchannelPicker { - abstract boolean isEquivalentTo(RoundRobinPicker picker); - } - - @VisibleForTesting - static final class EmptyPicker extends RoundRobinPicker { - - private final Status status; - - EmptyPicker(@Nonnull Status status) { - this.status = Preconditions.checkNotNull(status, "status"); + ReadyPicker(List list, int startIndex) { + Preconditions.checkArgument(!list.isEmpty(), "empty list"); + this.list = list; + this.index = startIndex - 1; } @Override public PickResult pickSubchannel(PickSubchannelArgs args) { - return status.isOk() ? PickResult.withNoResult() : PickResult.withError(status); + return PickResult.withSubchannel(nextSubchannel()); } @Override - boolean isEquivalentTo(RoundRobinPicker picker) { - return picker instanceof EmptyPicker && (Objects.equal(status, ((EmptyPicker) picker).status) - || (status.isOk() && ((EmptyPicker) picker).status.isOk())); + public String toString() { + return MoreObjects.toStringHelper(ReadyPicker.class).add("list", list).toString(); } - @Override - public String toString() { - return MoreObjects.toStringHelper(EmptyPicker.class).add("status", status).toString(); + private Subchannel nextSubchannel() { + int size = list.size(); + int i = indexUpdater.incrementAndGet(this); + if (i >= size) { + int oldi = i; + i %= size; + indexUpdater.compareAndSet(this, oldi, i); + } + return list.get(i); } - } - /** - * A lighter weight Reference than AtomicReference. - */ - @VisibleForTesting - static final class Ref { - T value; + @VisibleForTesting + List getList() { + return list; + } - Ref(T value) { - this.value = value; + @Override + boolean isEquivalentTo(RoundRobinPicker picker) { + if (!(picker instanceof ReadyPicker)) { + return false; + } + ReadyPicker other = (ReadyPicker) picker; + // the lists cannot contain duplicate subchannels + return other == this + || (list.size() == other.list.size() && new HashSet<>(list).containsAll(other.list)); } } } diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancerImpl.java b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancerImpl.java deleted file mode 100644 index d65ac5b4e42..00000000000 --- a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancerImpl.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2016 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 io.grpc.util; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.MoreObjects; -import com.google.common.base.Preconditions; -import io.grpc.EquivalentAddressGroup; -import io.grpc.NameResolver; -import java.util.HashSet; -import java.util.List; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; - -/** - * A {@link RoundRobinLoadBalancer} that provides round-robin load-balancing over the {@link - * EquivalentAddressGroup}s from the {@link NameResolver}. - */ -final class RoundRobinLoadBalancerImpl extends RoundRobinLoadBalancer { - - RoundRobinLoadBalancerImpl(Helper helper) { - super(helper); - } - - @Override - Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args) { - return helper.createSubchannel(args); - } - - @Override - RoundRobinPicker createReadyPicker(List activeSubchannelList, int startIndex) { - return new ReadyPicker(activeSubchannelList, startIndex); - } - - @VisibleForTesting - static final class ReadyPicker extends RoundRobinPicker { - private static final AtomicIntegerFieldUpdater indexUpdater = - AtomicIntegerFieldUpdater.newUpdater(ReadyPicker.class, "index"); - - private final List list; // non-empty - @SuppressWarnings("unused") - private volatile int index; - - ReadyPicker(List list, int startIndex) { - Preconditions.checkArgument(!list.isEmpty(), "empty list"); - this.list = list; - this.index = startIndex - 1; - } - - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - return PickResult.withSubchannel(nextSubchannel()); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(ReadyPicker.class).add("list", list).toString(); - } - - private Subchannel nextSubchannel() { - int size = list.size(); - int i = indexUpdater.incrementAndGet(this); - if (i >= size) { - int oldi = i; - i %= size; - indexUpdater.compareAndSet(this, oldi, i); - } - return list.get(i); - } - - @VisibleForTesting - List getList() { - return list; - } - - @Override - boolean isEquivalentTo(RoundRobinPicker picker) { - if (!(picker instanceof ReadyPicker)) { - return false; - } - ReadyPicker other = (ReadyPicker) picker; - // the lists cannot contain duplicate subchannels - return other == this - || (list.size() == other.list.size() && new HashSet<>(list).containsAll(other.list)); - } - } -} diff --git a/core/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java b/core/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java index 9cb794649e9..7843832cd5a 100644 --- a/core/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java +++ b/core/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java @@ -52,7 +52,7 @@ public String getPolicyName() { @Override public LoadBalancer newLoadBalancer(LoadBalancer.Helper helper) { - return new RoundRobinLoadBalancerImpl(helper); + return new RoundRobinLoadBalancer(helper); } @Override diff --git a/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java b/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java index 72e373a04fd..ad886c31142 100644 --- a/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java +++ b/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java @@ -405,7 +405,7 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { .build()); assertThat(addressesAccepted).isTrue(); assertThat(lb.getDelegate().getClass().getName()) - .isEqualTo("io.grpc.util.RoundRobinLoadBalancerImpl"); + .isEqualTo("io.grpc.util.RoundRobinLoadBalancer"); } @Test diff --git a/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java b/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java index 778dd60ec2c..ccf3d40cdb6 100644 --- a/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java +++ b/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java @@ -123,7 +123,7 @@ public LoadBalancer newLoadBalancer(Helper helper) { "round_robin") { @Override public LoadBalancer newLoadBalancer(Helper helper) { - return new RoundRobinLoadBalancerImpl(helper); + return new RoundRobinLoadBalancer(helper); } }; diff --git a/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java b/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java index 293b65e61e1..dbef5f32b57 100644 --- a/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java +++ b/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java @@ -22,7 +22,7 @@ import static io.grpc.ConnectivityState.READY; import static io.grpc.ConnectivityState.SHUTDOWN; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; -import static io.grpc.util.RoundRobinLoadBalancerImpl.STATE_INFO; +import static io.grpc.util.RoundRobinLoadBalancer.STATE_INFO; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -55,9 +55,9 @@ import io.grpc.LoadBalancer.SubchannelPicker; import io.grpc.LoadBalancer.SubchannelStateListener; import io.grpc.Status; -import io.grpc.util.RoundRobinLoadBalancer.EmptyPicker; -import io.grpc.util.RoundRobinLoadBalancer.Ref; -import io.grpc.util.RoundRobinLoadBalancerImpl.ReadyPicker; +import io.grpc.util.AbstractRoundRobinLoadBalancer.EmptyPicker; +import io.grpc.util.AbstractRoundRobinLoadBalancer.Ref; +import io.grpc.util.RoundRobinLoadBalancer.ReadyPicker; import java.net.SocketAddress; import java.util.ArrayList; import java.util.Arrays; @@ -78,12 +78,12 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -/** Unit test for {@link RoundRobinLoadBalancerImpl}. */ +/** Unit test for {@link RoundRobinLoadBalancer}. */ @RunWith(JUnit4.class) public class RoundRobinLoadBalancerTest { private static final Attributes.Key MAJOR_KEY = Attributes.Key.create("major-key"); - private RoundRobinLoadBalancerImpl loadBalancer; + private RoundRobinLoadBalancer loadBalancer; private final List servers = Lists.newArrayList(); private final Map, Subchannel> subchannels = Maps.newLinkedHashMap(); private final Map subchannelStateListeners = @@ -136,7 +136,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { } }); - loadBalancer = new RoundRobinLoadBalancerImpl(mockHelper); + loadBalancer = new RoundRobinLoadBalancer(mockHelper); } @After From 764c9a9a88895d703ec94976a84f5af5c73f71ed Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Wed, 25 Jan 2023 10:26:10 -0800 Subject: [PATCH 05/25] temp: add weightedroundrobinimpl 1. to merge orca api remove listener 2. to merge metric report api rqs 3. to settle the package --- .../util/AbstractRoundRobinLoadBalancer.java | 22 +- .../io/grpc/util/RoundRobinLoadBalancer.java | 7 +- googleapis/build.gradle | 1 + .../WeightedRoundRobinLoadBalancer.java | 232 ++++++++++++++++++ 4 files changed, 248 insertions(+), 14 deletions(-) create mode 100644 googleapis/src/main/java/io/grpc/googleapis/WeightedRoundRobinLoadBalancer.java diff --git a/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java index 90318bb49c6..1c61fde76f1 100644 --- a/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java @@ -52,27 +52,27 @@ * balancing state. The round-robin picker algorithm is implemented by method * {@link #createReadyPicker}. */ -abstract class AbstractRoundRobinLoadBalancer extends LoadBalancer { +public abstract class AbstractRoundRobinLoadBalancer extends LoadBalancer { @VisibleForTesting static final Attributes.Key> STATE_INFO = Attributes.Key.create("state-info"); - private final Helper helper; - private final Map subchannels = + protected final Helper helper; + protected final Map subchannels = new HashMap<>(); private final Random random; private ConnectivityState currentState; private RoundRobinPicker currentPicker = new EmptyPicker(EMPTY_OK); - AbstractRoundRobinLoadBalancer(Helper helper) { + public AbstractRoundRobinLoadBalancer(Helper helper) { this.helper = checkNotNull(helper, "helper"); this.random = new Random(); } - abstract Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args); + protected abstract Subchannel createSubchannel(CreateSubchannelArgs args); - abstract RoundRobinPicker createReadyPicker(List activeSubchannelList, + protected abstract RoundRobinPicker createReadyPicker(List activeSubchannelList, int startIndex); @Override @@ -112,7 +112,7 @@ public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { new Ref<>(ConnectivityStateInfo.forNonError(IDLE))); final Subchannel subchannel = checkNotNull( - createSubchannel(helper, CreateSubchannelArgs.newBuilder() + createSubchannel(CreateSubchannelArgs.newBuilder() .setAddresses(originalAddressGroup) .setAttributes(subchannelAttrs.build()) .build()), @@ -282,12 +282,12 @@ private static Set setsDifference(Set a, Set b) { } // Only subclasses are ReadyPicker or EmptyPicker - abstract static class RoundRobinPicker extends SubchannelPicker { - abstract boolean isEquivalentTo(RoundRobinPicker picker); + public abstract static class RoundRobinPicker extends SubchannelPicker { + protected abstract boolean isEquivalentTo(RoundRobinPicker picker); } @VisibleForTesting - static final class EmptyPicker extends RoundRobinPicker { + public static final class EmptyPicker extends RoundRobinPicker { private final Status status; @@ -301,7 +301,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { } @Override - boolean isEquivalentTo(RoundRobinPicker picker) { + protected boolean isEquivalentTo(RoundRobinPicker picker) { return picker instanceof EmptyPicker && (Objects.equal(status, ((EmptyPicker) picker).status) || (status.isOk() && ((EmptyPicker) picker).status.isOk())); } diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java index 8f0fbd1b2ec..86eed872679 100644 --- a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java @@ -36,12 +36,13 @@ final class RoundRobinLoadBalancer extends AbstractRoundRobinLoadBalancer { } @Override - Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args) { + protected Subchannel createSubchannel(CreateSubchannelArgs args) { return helper.createSubchannel(args); } @Override - RoundRobinPicker createReadyPicker(List activeSubchannelList, int startIndex) { + protected RoundRobinPicker createReadyPicker(List activeSubchannelList, + int startIndex) { return new ReadyPicker(activeSubchannelList, startIndex); } @@ -87,7 +88,7 @@ List getList() { } @Override - boolean isEquivalentTo(RoundRobinPicker picker) { + protected boolean isEquivalentTo(RoundRobinPicker picker) { if (!(picker instanceof ReadyPicker)) { return false; } diff --git a/googleapis/build.gradle b/googleapis/build.gradle index d829b1d28e9..99799c5649f 100644 --- a/googleapis/build.gradle +++ b/googleapis/build.gradle @@ -12,6 +12,7 @@ dependencies { implementation project(':grpc-alts'), project(':grpc-core'), project(':grpc-xds'), + project(':grpc-services'), libraries.guava testImplementation project(':grpc-core').sourceSets.test.output diff --git a/googleapis/src/main/java/io/grpc/googleapis/WeightedRoundRobinLoadBalancer.java b/googleapis/src/main/java/io/grpc/googleapis/WeightedRoundRobinLoadBalancer.java new file mode 100644 index 00000000000..06da624964e --- /dev/null +++ b/googleapis/src/main/java/io/grpc/googleapis/WeightedRoundRobinLoadBalancer.java @@ -0,0 +1,232 @@ +/* + * Copyright 2016 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 io.grpc.googleapis; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; +import io.grpc.EquivalentAddressGroup; +import io.grpc.NameResolver; +import io.grpc.services.MetricReport; +import io.grpc.util.AbstractRoundRobinLoadBalancer; +import io.grpc.util.ForwardingSubchannel; +import io.grpc.xds.orca.OrcaOobUtil; +import io.grpc.xds.orca.OrcaOobUtil.OrcaOobReportListener; +import io.grpc.xds.orca.OrcaPerRequestUtil.OrcaPerRequestReportListener; +import java.util.HashSet; +import java.util.List; + +/** + * A {@link AbstractRoundRobinLoadBalancer} that provides round-robin load-balancing over the {@link + * EquivalentAddressGroup}s from the {@link NameResolver}. + */ +final class WeightedRoundRobinLoadBalancer extends AbstractRoundRobinLoadBalancer { + private final Helper orcaOobHelper; + private WeightedRoundRobinLoadBalancerConfig config; + + WeightedRoundRobinLoadBalancer(Helper helper) { + super(helper); + this.orcaOobHelper = OrcaOobUtil.newOrcaReportingHelper(helper); + } + + @Override + public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { + config = + (WeightedRoundRobinLoadBalancerConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); + + return false; + } + + @Override + protected Subchannel createSubchannel(CreateSubchannelArgs args) { + // add oob listener + if (config.enableOobLoadReport) { + new WeightedRoundRobinSubchannel(orcaOobHelper.createSubchannel(args)); + } + return helper.createSubchannel(args); + } + + @Override + protected RoundRobinPicker createReadyPicker(List activeSubchannelList, + int startIndex) { + return new WeightedRoundRobinPicker(activeSubchannelList, startIndex); + } + + static final class WeightedRoundRobinSubchannel extends ForwardingSubchannel { + private Subchannel delegate; + OrcaOobReportListener oobListener = new OrcaOobReportListener() { + @Override + public void onLoadReport(MetricReport report) { + updateWeight(report); + } + }; + OrcaPerRequestReportListener perRpcListener = new OrcaPerRequestReportListener() { + @Override + public void onLoadReport(MetricReport report) { + updateWeight(report); + } + }; + long lastUpdated; + long nonEmptySince; + long weight; + + public WeightedRoundRobinSubchannel(Subchannel delegate) { + this.delegate = checkNotNull(delegate, "delegate"); + } + + static void updateWeight(MetricReport report) { + long newWeight = report.getCpuUtilization() == 0 ? 0 : + + } + + @Override + protected Subchannel delegate() { + return delegate; + } + } + + @VisibleForTesting + final class WeightedRoundRobinPicker extends RoundRobinPicker { + + private final List list; // non-empty + @SuppressWarnings("unused") + private volatile int index; + + WeightedRoundRobinPicker(List list, int startIndex) { + Preconditions.checkArgument(!list.isEmpty(), "empty list"); + this.list = list; + this.index = startIndex - 1; + } + + @Override + public PickResult pickSubchannel(PickSubchannelArgs args) { + // Subchannel subchannel = ; // A WeightedSubchannel + // if per-request: + // PickResult.withSubchannel( + // subchannel, + // OrcaPerRequestReportUtil.getInstance() + // .newOrcaClientStreamTracerFactory(subchannel.listener)); + // else + if (config.enableOobLoadReport) { + return null; + } + return PickResult.withSubchannel(nextSubchannel()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(WeightedRoundRobinPicker.class) + .add("list", list).toString(); + } + + private Subchannel nextSubchannel() { + return null; + } + + @VisibleForTesting + List getList() { + return list; + } + + @Override + public boolean isEquivalentTo(RoundRobinPicker picker) { + if (!(picker instanceof WeightedRoundRobinPicker)) { + return false; + } + WeightedRoundRobinPicker other = (WeightedRoundRobinPicker) picker; + // the lists cannot contain duplicate subchannels + return other == this + || (list.size() == other.list.size() && new HashSet<>(list).containsAll(other.list)); + } + } + + static final class WeightedRoundRobinLoadBalancerConfig { + final Long blackoutPeriodNanos; + final Long weightExpirationPeriodNanos; + final Boolean enableOobLoadReport; + final Long oobReportingPeriodNanos; + final Long weightUpdatePeriodNanos; + + + private WeightedRoundRobinLoadBalancerConfig(Long blackoutPeriodNanos, + Long weightExpirationPeriodNanos, + Boolean enableOobLoadReport, + Long oobReportingPeriodNanos, + Long weightUpdatePeriodNanos) { + this.blackoutPeriodNanos = blackoutPeriodNanos; + this.weightExpirationPeriodNanos = weightExpirationPeriodNanos; + this.enableOobLoadReport = enableOobLoadReport; + this.oobReportingPeriodNanos = oobReportingPeriodNanos; + this.weightUpdatePeriodNanos = weightUpdatePeriodNanos; + } + + static class Builder { + Long blackoutPeriodNanos = 10_000_000_000L; // 10s + Long weightExpirationPeriodNanos = 180_000_000_000L; //3min + Boolean enableOobLoadReport = false; + Long oobReportingPeriodNanos = 10_000_000_000L; // 10s + Long weightUpdatePeriodNanos = 1_000_000_000L; // 10s + + Builder setBlackoutPeriodNanos(Long blackoutPeriodNanos) { + checkArgument(blackoutPeriodNanos != null); + this.blackoutPeriodNanos = blackoutPeriodNanos; + return this; + } + + Builder setWeightExpirationPeriodNanos(Long weightExpirationPeriodNanos) { + checkArgument(weightExpirationPeriodNanos != null); + this.weightExpirationPeriodNanos = weightExpirationPeriodNanos; + return this; + } + + Builder setEnableOobLoadReport(Boolean enableOobLoadReport) { + checkArgument(enableOobLoadReport != null); + this.enableOobLoadReport = enableOobLoadReport; + return this; + } + + Builder setOobReportingPeriodNanos(Long oobReportingPeriodNanos) { + checkArgument(oobReportingPeriodNanos != null); + this.oobReportingPeriodNanos = oobReportingPeriodNanos; + return this; + } + + Builder setWeightUpdatePeriodNanos(Long weightUpdatePeriodNanos) { + checkArgument(weightUpdatePeriodNanos != null); + this.weightUpdatePeriodNanos = weightUpdatePeriodNanos; + return this; + } + + WeightedRoundRobinLoadBalancerConfig build() { + return new WeightedRoundRobinLoadBalancerConfig(blackoutPeriodNanos, + weightExpirationPeriodNanos, enableOobLoadReport, oobReportingPeriodNanos, + weightUpdatePeriodNanos); + } + } + + + + + + + } + + +} From afa10fba17dd6be6b5ff0b23db6bd249af1c145b Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Thu, 2 Feb 2023 13:35:32 -0800 Subject: [PATCH 06/25] add weighted round robin picker and scheduler --- .../util/AbstractRoundRobinLoadBalancer.java | 3 + .../io/grpc/util/RoundRobinLoadBalancer.java | 6 +- googleapis/build.gradle | 1 - .../WeightedRoundRobinLoadBalancer.java | 232 -------- .../xds/WeightedRoundRobinLoadBalancer.java | 497 ++++++++++++++++++ ...eightedRoundRobinLoadBalancerProvider.java | 82 +++ 6 files changed, 585 insertions(+), 236 deletions(-) delete mode 100644 googleapis/src/main/java/io/grpc/googleapis/WeightedRoundRobinLoadBalancer.java create mode 100644 xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java create mode 100644 xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java diff --git a/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java index 1c61fde76f1..2436c86353f 100644 --- a/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java @@ -72,6 +72,8 @@ public AbstractRoundRobinLoadBalancer(Helper helper) { protected abstract Subchannel createSubchannel(CreateSubchannelArgs args); + protected void afterSubchannelUpdate() {} + protected abstract RoundRobinPicker createReadyPicker(List activeSubchannelList, int startIndex); @@ -131,6 +133,7 @@ public void onSubchannelState(ConnectivityStateInfo state) { for (EquivalentAddressGroup addressGroup : removedAddrs) { removedSubchannels.add(subchannels.remove(addressGroup)); } + afterSubchannelUpdate(); // Update the picker before shutting down the subchannels, to reduce the chance of the race // between picking a subchannel and shutting it down. diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java index 86eed872679..2a7bbab60d6 100644 --- a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java @@ -29,7 +29,7 @@ * A {@link AbstractRoundRobinLoadBalancer} that provides round-robin load-balancing over the {@link * EquivalentAddressGroup}s from the {@link NameResolver}. */ -final class RoundRobinLoadBalancer extends AbstractRoundRobinLoadBalancer { +public final class RoundRobinLoadBalancer extends AbstractRoundRobinLoadBalancer { RoundRobinLoadBalancer(Helper helper) { super(helper); @@ -47,7 +47,7 @@ protected RoundRobinPicker createReadyPicker(List activeSubchannelLi } @VisibleForTesting - static final class ReadyPicker extends RoundRobinPicker { + public static class ReadyPicker extends RoundRobinPicker { private static final AtomicIntegerFieldUpdater indexUpdater = AtomicIntegerFieldUpdater.newUpdater(ReadyPicker.class, "index"); @@ -55,7 +55,7 @@ static final class ReadyPicker extends RoundRobinPicker { @SuppressWarnings("unused") private volatile int index; - ReadyPicker(List list, int startIndex) { + public ReadyPicker(List list, int startIndex) { Preconditions.checkArgument(!list.isEmpty(), "empty list"); this.list = list; this.index = startIndex - 1; diff --git a/googleapis/build.gradle b/googleapis/build.gradle index 99799c5649f..d829b1d28e9 100644 --- a/googleapis/build.gradle +++ b/googleapis/build.gradle @@ -12,7 +12,6 @@ dependencies { implementation project(':grpc-alts'), project(':grpc-core'), project(':grpc-xds'), - project(':grpc-services'), libraries.guava testImplementation project(':grpc-core').sourceSets.test.output diff --git a/googleapis/src/main/java/io/grpc/googleapis/WeightedRoundRobinLoadBalancer.java b/googleapis/src/main/java/io/grpc/googleapis/WeightedRoundRobinLoadBalancer.java deleted file mode 100644 index 06da624964e..00000000000 --- a/googleapis/src/main/java/io/grpc/googleapis/WeightedRoundRobinLoadBalancer.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright 2016 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 io.grpc.googleapis; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.MoreObjects; -import com.google.common.base.Preconditions; -import io.grpc.EquivalentAddressGroup; -import io.grpc.NameResolver; -import io.grpc.services.MetricReport; -import io.grpc.util.AbstractRoundRobinLoadBalancer; -import io.grpc.util.ForwardingSubchannel; -import io.grpc.xds.orca.OrcaOobUtil; -import io.grpc.xds.orca.OrcaOobUtil.OrcaOobReportListener; -import io.grpc.xds.orca.OrcaPerRequestUtil.OrcaPerRequestReportListener; -import java.util.HashSet; -import java.util.List; - -/** - * A {@link AbstractRoundRobinLoadBalancer} that provides round-robin load-balancing over the {@link - * EquivalentAddressGroup}s from the {@link NameResolver}. - */ -final class WeightedRoundRobinLoadBalancer extends AbstractRoundRobinLoadBalancer { - private final Helper orcaOobHelper; - private WeightedRoundRobinLoadBalancerConfig config; - - WeightedRoundRobinLoadBalancer(Helper helper) { - super(helper); - this.orcaOobHelper = OrcaOobUtil.newOrcaReportingHelper(helper); - } - - @Override - public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { - config = - (WeightedRoundRobinLoadBalancerConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); - - return false; - } - - @Override - protected Subchannel createSubchannel(CreateSubchannelArgs args) { - // add oob listener - if (config.enableOobLoadReport) { - new WeightedRoundRobinSubchannel(orcaOobHelper.createSubchannel(args)); - } - return helper.createSubchannel(args); - } - - @Override - protected RoundRobinPicker createReadyPicker(List activeSubchannelList, - int startIndex) { - return new WeightedRoundRobinPicker(activeSubchannelList, startIndex); - } - - static final class WeightedRoundRobinSubchannel extends ForwardingSubchannel { - private Subchannel delegate; - OrcaOobReportListener oobListener = new OrcaOobReportListener() { - @Override - public void onLoadReport(MetricReport report) { - updateWeight(report); - } - }; - OrcaPerRequestReportListener perRpcListener = new OrcaPerRequestReportListener() { - @Override - public void onLoadReport(MetricReport report) { - updateWeight(report); - } - }; - long lastUpdated; - long nonEmptySince; - long weight; - - public WeightedRoundRobinSubchannel(Subchannel delegate) { - this.delegate = checkNotNull(delegate, "delegate"); - } - - static void updateWeight(MetricReport report) { - long newWeight = report.getCpuUtilization() == 0 ? 0 : - - } - - @Override - protected Subchannel delegate() { - return delegate; - } - } - - @VisibleForTesting - final class WeightedRoundRobinPicker extends RoundRobinPicker { - - private final List list; // non-empty - @SuppressWarnings("unused") - private volatile int index; - - WeightedRoundRobinPicker(List list, int startIndex) { - Preconditions.checkArgument(!list.isEmpty(), "empty list"); - this.list = list; - this.index = startIndex - 1; - } - - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - // Subchannel subchannel = ; // A WeightedSubchannel - // if per-request: - // PickResult.withSubchannel( - // subchannel, - // OrcaPerRequestReportUtil.getInstance() - // .newOrcaClientStreamTracerFactory(subchannel.listener)); - // else - if (config.enableOobLoadReport) { - return null; - } - return PickResult.withSubchannel(nextSubchannel()); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(WeightedRoundRobinPicker.class) - .add("list", list).toString(); - } - - private Subchannel nextSubchannel() { - return null; - } - - @VisibleForTesting - List getList() { - return list; - } - - @Override - public boolean isEquivalentTo(RoundRobinPicker picker) { - if (!(picker instanceof WeightedRoundRobinPicker)) { - return false; - } - WeightedRoundRobinPicker other = (WeightedRoundRobinPicker) picker; - // the lists cannot contain duplicate subchannels - return other == this - || (list.size() == other.list.size() && new HashSet<>(list).containsAll(other.list)); - } - } - - static final class WeightedRoundRobinLoadBalancerConfig { - final Long blackoutPeriodNanos; - final Long weightExpirationPeriodNanos; - final Boolean enableOobLoadReport; - final Long oobReportingPeriodNanos; - final Long weightUpdatePeriodNanos; - - - private WeightedRoundRobinLoadBalancerConfig(Long blackoutPeriodNanos, - Long weightExpirationPeriodNanos, - Boolean enableOobLoadReport, - Long oobReportingPeriodNanos, - Long weightUpdatePeriodNanos) { - this.blackoutPeriodNanos = blackoutPeriodNanos; - this.weightExpirationPeriodNanos = weightExpirationPeriodNanos; - this.enableOobLoadReport = enableOobLoadReport; - this.oobReportingPeriodNanos = oobReportingPeriodNanos; - this.weightUpdatePeriodNanos = weightUpdatePeriodNanos; - } - - static class Builder { - Long blackoutPeriodNanos = 10_000_000_000L; // 10s - Long weightExpirationPeriodNanos = 180_000_000_000L; //3min - Boolean enableOobLoadReport = false; - Long oobReportingPeriodNanos = 10_000_000_000L; // 10s - Long weightUpdatePeriodNanos = 1_000_000_000L; // 10s - - Builder setBlackoutPeriodNanos(Long blackoutPeriodNanos) { - checkArgument(blackoutPeriodNanos != null); - this.blackoutPeriodNanos = blackoutPeriodNanos; - return this; - } - - Builder setWeightExpirationPeriodNanos(Long weightExpirationPeriodNanos) { - checkArgument(weightExpirationPeriodNanos != null); - this.weightExpirationPeriodNanos = weightExpirationPeriodNanos; - return this; - } - - Builder setEnableOobLoadReport(Boolean enableOobLoadReport) { - checkArgument(enableOobLoadReport != null); - this.enableOobLoadReport = enableOobLoadReport; - return this; - } - - Builder setOobReportingPeriodNanos(Long oobReportingPeriodNanos) { - checkArgument(oobReportingPeriodNanos != null); - this.oobReportingPeriodNanos = oobReportingPeriodNanos; - return this; - } - - Builder setWeightUpdatePeriodNanos(Long weightUpdatePeriodNanos) { - checkArgument(weightUpdatePeriodNanos != null); - this.weightUpdatePeriodNanos = weightUpdatePeriodNanos; - return this; - } - - WeightedRoundRobinLoadBalancerConfig build() { - return new WeightedRoundRobinLoadBalancerConfig(blackoutPeriodNanos, - weightExpirationPeriodNanos, enableOobLoadReport, oobReportingPeriodNanos, - weightUpdatePeriodNanos); - } - } - - - - - - - } - - -} diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java new file mode 100644 index 00000000000..5ab3db82f4d --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -0,0 +1,497 @@ +/* + * Copyright 2023 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 io.grpc.xds; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; +import io.grpc.EquivalentAddressGroup; +import io.grpc.NameResolver; +import io.grpc.SynchronizationContext; +import io.grpc.services.MetricReport; +import io.grpc.util.AbstractRoundRobinLoadBalancer; +import io.grpc.util.ForwardingSubchannel; +import io.grpc.util.RoundRobinLoadBalancer; +import io.grpc.xds.orca.OrcaOobUtil; +import io.grpc.xds.orca.OrcaOobUtil.OrcaOobReportListener; +import io.grpc.xds.orca.OrcaPerRequestUtil; +import io.grpc.xds.orca.OrcaPerRequestUtil.OrcaPerRequestReportListener; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * A {@link AbstractRoundRobinLoadBalancer} that provides weighted-round-robin load-balancing over + * the {@link EquivalentAddressGroup}s from the {@link NameResolver}. The subchannel weights are + * determined by backend metrics using ORCA. + */ +final class WeightedRoundRobinLoadBalancer extends AbstractRoundRobinLoadBalancer { + private final Helper orcaOobHelper; + private WeightedRoundRobinLoadBalancerConfig config; + + private final SynchronizationContext syncContext; + + private final ScheduledExecutorService timeService; + + WeightedRoundRobinLoadBalancer(Helper helper) { + super(helper); + this.orcaOobHelper = OrcaOobUtil.newOrcaReportingHelper(helper); + this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext"); + this.timeService = checkNotNull(helper.getScheduledExecutorService(), "timeService"); + } + + @Override + public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { + config = + (WeightedRoundRobinLoadBalancerConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); + return super.acceptResolvedAddresses(resolvedAddresses); + } + + @Override + protected Subchannel createSubchannel(CreateSubchannelArgs args) { + return new WeightedRoundRobinSubchannel(orcaOobHelper.createSubchannel(args)); + } + + @Override + protected void afterSubchannelUpdate() { + //todo: may optimize to use previous enabled oob to skip + for (Subchannel subchannel : subchannels.values()) { + WeightedRoundRobinSubchannel weightedSubchannel = (WeightedRoundRobinSubchannel) subchannel; + if (config.enableOobLoadReport) { + OrcaOobUtil.setListener(weightedSubchannel, weightedSubchannel.oobListener, + OrcaOobUtil.OrcaReportingConfig.newBuilder() + .setReportInterval(config.oobReportingPeriodNanos, TimeUnit.NANOSECONDS).build()); + } else { + OrcaOobUtil.setListener(weightedSubchannel, null, null); + } + } + } + + @Override + protected RoundRobinPicker createReadyPicker(List activeSubchannelList, + int startIndex) { + return new WeightedRoundRobinPicker(activeSubchannelList, startIndex); + } + + final class WeightedRoundRobinSubchannel extends ForwardingSubchannel { + private final Subchannel delegate; + private final OrcaOobReportListener oobListener = this::onLoadReport; + private final OrcaPerRequestReportListener perRpcListener = this::onLoadReport; + volatile long lastUpdated; + volatile long nonEmptySince; + volatile double weight; + + public WeightedRoundRobinSubchannel(Subchannel delegate) { + this.delegate = checkNotNull(delegate, "delegate"); + } + + void onLoadReport(MetricReport report) { + double newWeight = report.getCpuUtilization() == 0 ? 0 : + report.getQps() / report.getCpuUtilization(); + if (newWeight == 0) { + return; + } + if (nonEmptySince == Integer.MAX_VALUE) { + nonEmptySince = System.currentTimeMillis(); + } + lastUpdated = System.currentTimeMillis(); + weight = newWeight; + } + + double getWeight() { + double now = System.currentTimeMillis(); + if (now - lastUpdated >= config.weightExpirationPeriodNanos) { + nonEmptySince = Integer.MAX_VALUE; + return 0; + } else if (now - nonEmptySince < config.blackoutPeriodNanos) { + return 0; + } else { + return weight; + } + } + + @Override + protected Subchannel delegate() { + return delegate; + } + } + + @VisibleForTesting + final class WeightedRoundRobinPicker extends RoundRobinLoadBalancer.ReadyPicker { + private final List list; // non-empty + private final AtomicReference schedulerRef; + SynchronizationContext.ScheduledHandle weightUpdateTimer; + boolean rrMode = false; + + WeightedRoundRobinPicker(List list, int startIndex) { + super(list, startIndex); + Preconditions.checkArgument(!list.isEmpty(), "empty list"); + this.list = list; + this.schedulerRef = new AtomicReference<>(new EdfScheduler()); + new UpdateWeightTask().run(); + } + + @Override + public PickResult pickSubchannel(PickSubchannelArgs args) { + if (rrMode) { + return super.pickSubchannel(args); + } + WeightedRoundRobinSubchannel subchannel = (WeightedRoundRobinSubchannel) nextSubchannel(); + if (config.enableOobLoadReport) { + return PickResult.withSubchannel( + subchannel, + OrcaPerRequestUtil.getInstance().newOrcaClientStreamTracerFactory( + subchannel.perRpcListener)); + } else { + return PickResult.withSubchannel(subchannel); + } + } + + final class UpdateWeightTask implements Runnable { + @Override + public void run() { + if (weightUpdateTimer != null && weightUpdateTimer.isPending()) { + return; + } + EdfScheduler scheduler = new EdfScheduler(schedulerRef.get()); + int channelHasLoadCount = 0; + double avgWeight = 0; + for (int i = 0; i < list.size(); i++) { + WeightedRoundRobinSubchannel subchannel = (WeightedRoundRobinSubchannel) list.get(i); + double newWeight = subchannel.getWeight(); + if (newWeight > 0) { + avgWeight += newWeight; + channelHasLoadCount++; + } + } + rrMode = channelHasLoadCount < 2; + if (rrMode) { + return; + } + for (int i = 0; i < list.size(); i++) { + WeightedRoundRobinSubchannel subchannel = (WeightedRoundRobinSubchannel) list.get(i); + double newWeight = subchannel.getWeight(); + scheduler.addOrUpdate(i, newWeight > 0 ? newWeight : avgWeight); + } + schedulerRef.set(scheduler); + + weightUpdateTimer = syncContext.schedule(new UpdateWeightTask(), + config.weightUpdatePeriodNanos, TimeUnit.NANOSECONDS, timeService); + } + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(WeightedRoundRobinPicker.class) + .add("list", list).toString(); + } + + private Subchannel nextSubchannel() { + return list.get(schedulerRef.get().pick()); + } + + @VisibleForTesting + List getList() { + return list; + } + + @Override + public boolean isEquivalentTo(RoundRobinPicker picker) { + if (!(picker instanceof WeightedRoundRobinPicker)) { + return false; + } + WeightedRoundRobinPicker other = (WeightedRoundRobinPicker) picker; + // the lists cannot contain duplicate subchannels + return other == this + || (list.size() == other.list.size() && new HashSet<>(list).containsAll(other.list)); + } + } + + /** + * A earliest deadline first implementation in which each object is + * chosen deterministically and periodically with frequency proportional to its weight. + * + *

Specifically, each object added to chooser is given a period equal to the multiplicative + * inverse of its weight. The place of each object in its period is tracked, and each call to + * choose returns the child with the least remaining time in its period (1/weight). + * (Ties are broken by the order in which the children were added to the chooser.) + * For example, if items A and B are added + * with weights 0.5 and 0.2, successive chooses return: + * + *

    + *
  • In the first call, the remaining periods are A=2 (1/0.5) and B=5 (1/0.2), so A is + * returned. The period of A (as it was picked), is substracted from periods of all other + * objects. + *
  • Next, the remaining periods are A=2 and B=3, so A is returned. The period of A (2) is + * substracted from all other objects (B=1) and A is re-added with A=2. + *
  • Remaining periods are A=2 and B=1, so B is returned. The period of B (1) is substracted + * from all other objects (A=1) and B is re-added with B=5. + *
  • Remaining periods are A=1 and B=5, so A is returned. The period of A (1) is substracted + * from all other objects (B=4) and A is re-added with A=2. + *
  • Remaining periods are A=2 and B=4, so A is returned. The period of A (2) is substracted + * from all other objects (B=2) and A is re-added with A=2. + *
  • Remaining periods are A=2 and B=2, so A is returned. The period of A (2) is substracted + * from all other objects (B=0) and A is re-added with A=2. + *
  • Remaining periods are A=2 and B=0, so B is returned. The period of B (0) is substracted + * from all other objects (A=2) and B is re-added with B=5. + *
  • etc. + *
+ * + *

In short: the entry with the highest weight is preferred. In case of ties, the object that + * was last returned will be preferred. + * + *

    + *
  • add() - O(lg n) + *
  • remove() - O(lg n) + *
  • pick() - O(lg n) with worst case O(n) + *
+ * + */ + private static final class EdfScheduler { + + final Map objects; + private final PriorityBlockingQueue prioQueue; + + /** + * Upon every pick() the "virtual time" is advanced closer to the period of next items. + * In the WeightedRoundRobinChooser implementation this is done by subtracting the period of the + * picked object from all other items yielding O(n). + * Here we have an explicit "virtualTimeNow", which will be added to the period of all newly + * scheduled objects (virtualTimeNow + period). + */ + private double virtualTimeNow = 0.0; + + /** + * Weights below this value will be logged and upped to this minimum weight. + */ + private static final double MINIMUM_WEIGHT = 0.0001; + + /** + * At what threshold of virtualTimeNow to call periodReset(). + * Simulate 100 picks of zero weights. + */ + private static final double VIRTUAL_TIME_RESET_THRESHOLD = 1.0 / MINIMUM_WEIGHT * 100.0; + + public EdfScheduler() { + this.objects = new HashMap(); + Comparator comparator = (o1, o2) -> { + if (o1.priority == o2.priority) { + return o1.index - o2.index; + } else if (o1.priority < o2.priority) { + return -1; + } else { + return 1; + } + }; + this.prioQueue = new PriorityBlockingQueue<>(10, comparator); + } + + public EdfScheduler(EdfScheduler state) { + this.objects = new HashMap(state.objects); + this.prioQueue = new PriorityBlockingQueue<>(state.prioQueue); + } + + /** + * Adds (or updates) the item in the scheduler. + * + * @param index of the object to be added/updated + * @param weight positive weight for the added/updated object + * @return whether the object was updated (true) or added (false) + */ + public boolean addOrUpdate(int index, double weight) { + checkArgument(weight > 0.0, "Weights need to be positive."); + boolean isUpdate = objects.containsKey(index); + ObjectState state = isUpdate ? objects.get(index) : new ObjectState(); + double newWeight = Math.max(weight, MINIMUM_WEIGHT); + + if (isUpdate) { + recordCompletionRatio(state); + double period = calculatePriorityAndSetWeight(state, newWeight); + updatePriority(state, period); + } else { + state.priority = calculatePriorityAndSetWeight(state, newWeight); + prioQueue.add(state); + objects.put(index, state); + } + state.completionRatio = 0; + return isUpdate; + } + + /** + * Removes the object from the MWRR data structure. + * + * @return whether the object existed + */ + public boolean remove(int index) { + ObjectState holder = objects.remove(index); + if (holder == null) { + return false; + } + prioQueue.remove(holder); + return true; + } + + /** + * Picks the next WRR object. + * + * @return next object from WRR, null if WRR empty. + */ + public int pick() { + ObjectState minObject = prioQueue.peek(); + if (minObject == null) { + return -1; + } + double minPeriod = minObject.priority; + + // Simulate advancing in time by setting the current time to the period of the nearest item + // on the "time horizon". + virtualTimeNow = minPeriod; + // If virtualTimeNow becomes large, we need to reset everything for numerical stability. + if (virtualTimeNow > VIRTUAL_TIME_RESET_THRESHOLD) { + virtualTimeReset(); + } + schedule(minObject); + return minObject.index; + } + + private void updatePriority(ObjectState objectState, double newPriority) { + prioQueue.remove(objectState); + objectState.priority = newPriority; + prioQueue.add(objectState); + } + + /** + * Schedules the given object at its original period + virtualTimeNow. + */ + private void schedule(ObjectState state) { + updatePriority(state, virtualTimeNow + (1.0 / state.weight)); + } + + private void recordCompletionRatio(ObjectState state) { + state.completionRatio = Math.max(0, + 1 + (virtualTimeNow - state.priority) * state.weight); + } + + private double calculatePriorityAndSetWeight(ObjectState state, double newWeight) { + state.weight = newWeight; + return virtualTimeNow + Math.max(0, (1 - state.completionRatio) / state.weight); + } + + /** + * Get number of objects in the scheduler. + */ + public int size() { + return objects.size(); + } + + /** + * Decrease the periods in the prioQueue by virtualTimeNow. Reset virtualTimeNow to zero. + * Since the iterator on is iterating over the heap in the heap order + * and it is a min-heap, we will first be reducing the period of parent nodes in the heap. + * As such the siftUp() operation of setPriority() will be O(1) and thus the whole thing O(n). + */ + private void virtualTimeReset() { + for (ObjectState state : objects.values()) { + updatePriority(state, Math.max(state.priority - virtualTimeNow, 0.0)); + } + virtualTimeNow = 0.0; + } + } + + /** Holds the state of the object. */ + @VisibleForTesting + static class ObjectState { + double weight; + double completionRatio; + double priority; + int index; + } + + static final class WeightedRoundRobinLoadBalancerConfig { + final Long blackoutPeriodNanos; + final Long weightExpirationPeriodNanos; + final Boolean enableOobLoadReport; + final Long oobReportingPeriodNanos; + final Long weightUpdatePeriodNanos; + + + private WeightedRoundRobinLoadBalancerConfig(Long blackoutPeriodNanos, + Long weightExpirationPeriodNanos, + Boolean enableOobLoadReport, + Long oobReportingPeriodNanos, + Long weightUpdatePeriodNanos) { + this.blackoutPeriodNanos = blackoutPeriodNanos; + this.weightExpirationPeriodNanos = weightExpirationPeriodNanos; + this.enableOobLoadReport = enableOobLoadReport; + this.oobReportingPeriodNanos = oobReportingPeriodNanos; + this.weightUpdatePeriodNanos = weightUpdatePeriodNanos; + } + + static class Builder { + Long blackoutPeriodNanos = 10_000_000_000L; // 10s + Long weightExpirationPeriodNanos = 180_000_000_000L; //3min + Boolean enableOobLoadReport = false; + Long oobReportingPeriodNanos = 10_000_000_000L; // 10s + Long weightUpdatePeriodNanos = 1_000_000_000L; // 10s + + Builder setBlackoutPeriodNanos(Long blackoutPeriodNanos) { + checkArgument(blackoutPeriodNanos != null); + this.blackoutPeriodNanos = blackoutPeriodNanos; + return this; + } + + Builder setWeightExpirationPeriodNanos(Long weightExpirationPeriodNanos) { + checkArgument(weightExpirationPeriodNanos != null); + this.weightExpirationPeriodNanos = weightExpirationPeriodNanos; + return this; + } + + Builder setEnableOobLoadReport(Boolean enableOobLoadReport) { + checkArgument(enableOobLoadReport != null); + this.enableOobLoadReport = enableOobLoadReport; + return this; + } + + Builder setOobReportingPeriodNanos(Long oobReportingPeriodNanos) { + checkArgument(oobReportingPeriodNanos != null); + this.oobReportingPeriodNanos = oobReportingPeriodNanos; + return this; + } + + Builder setWeightUpdatePeriodNanos(Long weightUpdatePeriodNanos) { + checkArgument(weightUpdatePeriodNanos != null); + this.weightUpdatePeriodNanos = weightUpdatePeriodNanos; + return this; + } + + WeightedRoundRobinLoadBalancerConfig build() { + return new WeightedRoundRobinLoadBalancerConfig(blackoutPeriodNanos, + weightExpirationPeriodNanos, enableOobLoadReport, oobReportingPeriodNanos, + weightUpdatePeriodNanos); + } + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java new file mode 100644 index 00000000000..d28280d9e7f --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java @@ -0,0 +1,82 @@ +/* + * Copyright 2023 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 io.grpc.xds; + +import io.grpc.LoadBalancer; +import io.grpc.LoadBalancer.Helper; +import io.grpc.LoadBalancerProvider; +import io.grpc.NameResolver.ConfigOrError; +import io.grpc.internal.JsonUtil; +import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinLoadBalancerConfig; +import java.util.Map; + +/** + * Providers a {@link WeightedRoundRobinLoadBalancer}. + * */ +final class WeightedRoundRobinLoadBalancerProvider extends LoadBalancerProvider { + + private static final long MIN_WEIGHT_UPDATE_PERIOD_NANOS = 10_000_000L; // 100ms + + @Override + public LoadBalancer newLoadBalancer(Helper helper) { + return new WeightedRoundRobinLoadBalancer(helper); + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public int getPriority() { + return 5; + } + + @Override + public String getPolicyName() { + return "weighted_round_robin_experimental"; + } + + @Override + public ConfigOrError parseLoadBalancingPolicyConfig(Map rawConfig) { + Long blackoutPeriodNanos = JsonUtil.getStringAsDuration(rawConfig, "blackoutPeriod"); + Long weightExpirationPeriodNanos = + JsonUtil.getStringAsDuration(rawConfig, "weightExpirationPeriod"); + Long oobReportingPeriodNanos = JsonUtil.getStringAsDuration(rawConfig, "oobReportingPeriod"); + Boolean enableOobLoadReport = JsonUtil.getBoolean(rawConfig, "enableOobLoadReport"); + Long weightUpdatePeriodNanos = JsonUtil.getStringAsDuration(rawConfig, "weightUpdatePeriod"); + + WeightedRoundRobinLoadBalancerConfig.Builder configBuilder = + new WeightedRoundRobinLoadBalancerConfig.Builder(); + if (blackoutPeriodNanos != null) { + configBuilder.setBlackoutPeriodNanos(blackoutPeriodNanos); + } + if (weightExpirationPeriodNanos != null) { + configBuilder.setWeightExpirationPeriodNanos(weightExpirationPeriodNanos); + } + if (oobReportingPeriodNanos != null) { + configBuilder.setEnableOobLoadReport(enableOobLoadReport); + } + if (weightUpdatePeriodNanos != null) { + configBuilder.setWeightUpdatePeriodNanos(weightUpdatePeriodNanos); + if (weightUpdatePeriodNanos < MIN_WEIGHT_UPDATE_PERIOD_NANOS) { + configBuilder.setWeightUpdatePeriodNanos(MIN_WEIGHT_UPDATE_PERIOD_NANOS); + } + } + return ConfigOrError.fromConfig(configBuilder.build()); + } +} From d4f785dd8912e04b6ac659a82ebd7fda4c9c9953 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Thu, 2 Feb 2023 17:08:30 -0800 Subject: [PATCH 07/25] comments, and add afterSubchannelUpdate --- .../util/AbstractRoundRobinLoadBalancer.java | 18 ++++++++++++++++-- .../io/grpc/util/RoundRobinLoadBalancer.java | 4 ++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java index 90318bb49c6..2d2e1b917ae 100644 --- a/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java @@ -70,9 +70,21 @@ abstract class AbstractRoundRobinLoadBalancer extends LoadBalancer { this.random = new Random(); } - abstract Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args); + /** + * Create a subchannel for a new {@link EquivalentAddressGroup} when handling new resolved + * addresses. + */ + protected abstract Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args); + + /** + * An action point after accepting the most recent resolved addresses. + */ + protected void afterSubchannelUpdate() {} - abstract RoundRobinPicker createReadyPicker(List activeSubchannelList, + /** + * Returns the picker that selects a subchannel in the list for each incoming RPC. + */ + protected abstract RoundRobinPicker createReadyPicker(List activeSubchannelList, int startIndex); @Override @@ -132,6 +144,8 @@ public void onSubchannelState(ConnectivityStateInfo state) { removedSubchannels.add(subchannels.remove(addressGroup)); } + afterSubchannelUpdate(); + // Update the picker before shutting down the subchannels, to reduce the chance of the race // between picking a subchannel and shutting it down. updateBalancingState(); diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java index 8f0fbd1b2ec..813af1f8a2e 100644 --- a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java @@ -36,12 +36,12 @@ final class RoundRobinLoadBalancer extends AbstractRoundRobinLoadBalancer { } @Override - Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args) { + protected Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args) { return helper.createSubchannel(args); } @Override - RoundRobinPicker createReadyPicker(List activeSubchannelList, int startIndex) { + protected RoundRobinPicker createReadyPicker(List activeSubchannelList, int startIndex) { return new ReadyPicker(activeSubchannelList, startIndex); } From 60af73cd3595235a72a80ae209e9a4d575b30c40 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Thu, 2 Feb 2023 17:20:15 -0800 Subject: [PATCH 08/25] format --- .../main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java | 2 +- core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java index 2d2e1b917ae..6bb8332571f 100644 --- a/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 The gRPC Authors + * Copyright 2023 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. diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java index 813af1f8a2e..098f10d1f1d 100644 --- a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java @@ -41,7 +41,8 @@ protected Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args) } @Override - protected RoundRobinPicker createReadyPicker(List activeSubchannelList, int startIndex) { + protected RoundRobinPicker createReadyPicker(List activeSubchannelList, + int startIndex) { return new ReadyPicker(activeSubchannelList, startIndex); } From 4228f97f87118a991d75dab2a0bf6ae9c9e2dcaa Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Fri, 3 Feb 2023 13:10:35 -0800 Subject: [PATCH 09/25] move abstraction to composition --- .../io/grpc/util/RoundRobinLoadBalancer.java | 53 +++++++++-- ...=> SubchannelListLoadBalancerCommons.java} | 94 ++++++++++--------- .../grpc/util/RoundRobinLoadBalancerTest.java | 6 +- 3 files changed, 97 insertions(+), 56 deletions(-) rename core/src/main/java/io/grpc/util/{AbstractRoundRobinLoadBalancer.java => SubchannelListLoadBalancerCommons.java} (79%) diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java index 098f10d1f1d..5ef18384be5 100644 --- a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java @@ -16,34 +16,67 @@ package io.grpc.util; +import static com.google.common.base.Preconditions.checkNotNull; + import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import io.grpc.EquivalentAddressGroup; +import io.grpc.LoadBalancer; import io.grpc.NameResolver; +import io.grpc.Status; +import io.grpc.util.SubchannelListLoadBalancerCommons.RoundRobinPicker; +import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Random; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; /** - * A {@link AbstractRoundRobinLoadBalancer} that provides round-robin load-balancing over the {@link - * EquivalentAddressGroup}s from the {@link NameResolver}. + * A {@link SubchannelListLoadBalancerCommons} that provides round-robin load-balancing over the + * {@link EquivalentAddressGroup}s from the {@link NameResolver}. */ -final class RoundRobinLoadBalancer extends AbstractRoundRobinLoadBalancer { +final class RoundRobinLoadBalancer extends LoadBalancer { + + private final SubchannelListLoadBalancerCommons roundRobinCommons; + private final Helper helper; + private final Random random; RoundRobinLoadBalancer(Helper helper) { - super(helper); + this.helper = checkNotNull(helper, "helper"); + this.roundRobinCommons = new SubchannelListLoadBalancerCommons(helper, this::createSubchannel, + () -> { }, this::createReadyPicker); + this.random = new Random(); } - @Override - protected Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args) { + private Subchannel createSubchannel(CreateSubchannelArgs args) { return helper.createSubchannel(args); } + private RoundRobinPicker createReadyPicker(List activeSubchannelList) { + // initialize the Picker to a random start index to ensure that a high frequency of Picker + // churn does not skew subchannel selection. + return new ReadyPicker(activeSubchannelList, random.nextInt(activeSubchannelList.size())); + } + + @Override + public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { + return roundRobinCommons.acceptResolvedAddresses(resolvedAddresses); + } + + @Override + public void handleNameResolutionError(Status error) { + roundRobinCommons.handleNameResolutionError(error); + } + @Override - protected RoundRobinPicker createReadyPicker(List activeSubchannelList, - int startIndex) { - return new ReadyPicker(activeSubchannelList, startIndex); + public void shutdown() { + roundRobinCommons.shutdown(); + } + + @VisibleForTesting + Collection getSubchannels() { + return roundRobinCommons.getSubchannels(); } @VisibleForTesting @@ -88,7 +121,7 @@ List getList() { } @Override - boolean isEquivalentTo(RoundRobinPicker picker) { + public boolean isEquivalentTo(RoundRobinPicker picker) { if (!(picker instanceof ReadyPicker)) { return false; } diff --git a/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/SubchannelListLoadBalancerCommons.java similarity index 79% rename from core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java rename to core/src/main/java/io/grpc/util/SubchannelListLoadBalancerCommons.java index 6bb8332571f..fb17c2ffa3e 100644 --- a/core/src/main/java/io/grpc/util/AbstractRoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/SubchannelListLoadBalancerCommons.java @@ -24,6 +24,7 @@ import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.base.Preconditions; @@ -31,7 +32,16 @@ import io.grpc.ConnectivityState; import io.grpc.ConnectivityStateInfo; import io.grpc.EquivalentAddressGroup; +import io.grpc.Internal; import io.grpc.LoadBalancer; +import io.grpc.LoadBalancer.CreateSubchannelArgs; +import io.grpc.LoadBalancer.Helper; +import io.grpc.LoadBalancer.PickResult; +import io.grpc.LoadBalancer.PickSubchannelArgs; +import io.grpc.LoadBalancer.ResolvedAddresses; +import io.grpc.LoadBalancer.Subchannel; +import io.grpc.LoadBalancer.SubchannelPicker; +import io.grpc.LoadBalancer.SubchannelStateListener; import io.grpc.NameResolver; import io.grpc.Status; import java.util.ArrayList; @@ -41,53 +51,53 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Random; import java.util.Set; import javax.annotation.Nonnull; /** - * A {@link LoadBalancer} that provides load-balancing over the {@link + * A utility function that provides common processing for accepting a list of {@link * EquivalentAddressGroup}s from the {@link NameResolver}. It provides default implementation of - * accepting {@link io.grpc.LoadBalancer.ResolvedAddresses} updates, process aggregated load - * balancing state. The round-robin picker algorithm is implemented by method - * {@link #createReadyPicker}. + * accepting {@link io.grpc.LoadBalancer.ResolvedAddresses} updates, process + * aggregated load balancing state. A {@link LoadBalancer} that has sunchannel list will provide + * creating subchannel task and load-balancing strategy over the subchannel list. */ -abstract class AbstractRoundRobinLoadBalancer extends LoadBalancer { +@Internal +public class SubchannelListLoadBalancerCommons { @VisibleForTesting static final Attributes.Key> STATE_INFO = Attributes.Key.create("state-info"); - - private final Helper helper; + private final LoadBalancer.Helper helper; private final Map subchannels = new HashMap<>(); - private final Random random; - private ConnectivityState currentState; private RoundRobinPicker currentPicker = new EmptyPicker(EMPTY_OK); + private final Function createSubchannelTask; + private final Runnable afterUpdateTask; + private final Function, RoundRobinPicker> createReadyPickerTask; - AbstractRoundRobinLoadBalancer(Helper helper) { + /** + * Common new addresses list update handling. + * + * @param createSubchannelTask Create a subchannel for a new {@link EquivalentAddressGroup} + * when handling new resolved addresses. + * @param afterUpdateTask An action point after accepting the most recent resolved addresses. + * @param createReadyPickerTask Returns the picker that selects a subchannel in the list for each + * incoming RPC. + */ + SubchannelListLoadBalancerCommons(Helper helper, + Function createSubchannelTask, + Runnable afterUpdateTask, + Function, RoundRobinPicker> + createReadyPickerTask) { this.helper = checkNotNull(helper, "helper"); - this.random = new Random(); + this.createSubchannelTask = checkNotNull(createSubchannelTask, "createSubchannelTask"); + this.afterUpdateTask = checkNotNull(afterUpdateTask, "afterUpdateTask"); + this.createReadyPickerTask = checkNotNull(createReadyPickerTask, "createReadyPickerTask"); } /** - * Create a subchannel for a new {@link EquivalentAddressGroup} when handling new resolved - * addresses. + * Common new addresses list update handling. */ - protected abstract Subchannel createSubchannel(Helper helper, CreateSubchannelArgs args); - - /** - * An action point after accepting the most recent resolved addresses. - */ - protected void afterSubchannelUpdate() {} - - /** - * Returns the picker that selects a subchannel in the list for each incoming RPC. - */ - protected abstract RoundRobinPicker createReadyPicker(List activeSubchannelList, - int startIndex); - - @Override public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { if (resolvedAddresses.getAddresses().isEmpty()) { handleNameResolutionError(Status.UNAVAILABLE.withDescription( @@ -124,7 +134,7 @@ public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { new Ref<>(ConnectivityStateInfo.forNonError(IDLE))); final Subchannel subchannel = checkNotNull( - createSubchannel(helper, CreateSubchannelArgs.newBuilder() + createSubchannelTask.apply(CreateSubchannelArgs.newBuilder() .setAddresses(originalAddressGroup) .setAttributes(subchannelAttrs.build()) .build()), @@ -144,11 +154,11 @@ public void onSubchannelState(ConnectivityStateInfo state) { removedSubchannels.add(subchannels.remove(addressGroup)); } - afterSubchannelUpdate(); + afterUpdateTask.run(); // Update the picker before shutting down the subchannels, to reduce the chance of the race // between picking a subchannel and shutting it down. - updateBalancingState(); + updateBalancingState(createReadyPickerTask); // Shutdown removed subchannels for (Subchannel removedSubchannel : removedSubchannels) { @@ -158,7 +168,9 @@ public void onSubchannelState(ConnectivityStateInfo state) { return true; } - @Override + /** + * Common error handling from the name resolver. + */ public void handleNameResolutionError(Status error) { if (currentState != READY) { updateBalancingState(TRANSIENT_FAILURE, new EmptyPicker(error)); @@ -182,7 +194,7 @@ private void processSubchannelState(Subchannel subchannel, ConnectivityStateInfo } } subchannelStateRef.value = stateInfo; - updateBalancingState(); + updateBalancingState(createReadyPickerTask); } private void shutdownSubchannel(Subchannel subchannel) { @@ -191,7 +203,6 @@ private void shutdownSubchannel(Subchannel subchannel) { ConnectivityStateInfo.forNonError(SHUTDOWN); } - @Override public void shutdown() { for (Subchannel subchannel : getSubchannels()) { shutdownSubchannel(subchannel); @@ -205,7 +216,8 @@ public void shutdown() { * Updates picker with the list of active subchannels (state == READY). */ @SuppressWarnings("ReferenceEquality") - private void updateBalancingState() { + private void updateBalancingState(Function, RoundRobinPicker> + createReadyPickerTask) { List activeList = filterNonFailingSubchannels(getSubchannels()); if (activeList.isEmpty()) { // No READY subchannels, determine aggregate state and error status @@ -228,10 +240,7 @@ private void updateBalancingState() { // an arbitrary subchannel, otherwise return OK. new EmptyPicker(aggStatus)); } else { - // initialize the Picker to a random start index to ensure that a high frequency of Picker - // churn does not skew subchannel selection. - int startIndex = random.nextInt(activeList.size()); - updateBalancingState(READY, createReadyPicker(activeList, startIndex)); + updateBalancingState(READY, createReadyPickerTask.apply(activeList)); } } @@ -274,7 +283,6 @@ private static EquivalentAddressGroup stripAttrs(EquivalentAddressGroup eag) { return new EquivalentAddressGroup(eag.getAddresses()); } - @VisibleForTesting Collection getSubchannels() { return subchannels.values(); } @@ -296,8 +304,8 @@ private static Set setsDifference(Set a, Set b) { } // Only subclasses are ReadyPicker or EmptyPicker - abstract static class RoundRobinPicker extends SubchannelPicker { - abstract boolean isEquivalentTo(RoundRobinPicker picker); + public abstract static class RoundRobinPicker extends SubchannelPicker { + public abstract boolean isEquivalentTo(RoundRobinPicker picker); } @VisibleForTesting @@ -315,7 +323,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { } @Override - boolean isEquivalentTo(RoundRobinPicker picker) { + public boolean isEquivalentTo(RoundRobinPicker picker) { return picker instanceof EmptyPicker && (Objects.equal(status, ((EmptyPicker) picker).status) || (status.isOk() && ((EmptyPicker) picker).status.isOk())); } diff --git a/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java b/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java index dbef5f32b57..38e552d9071 100644 --- a/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java +++ b/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java @@ -22,7 +22,7 @@ import static io.grpc.ConnectivityState.READY; import static io.grpc.ConnectivityState.SHUTDOWN; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; -import static io.grpc.util.RoundRobinLoadBalancer.STATE_INFO; +import static io.grpc.util.SubchannelListLoadBalancerCommons.STATE_INFO; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -55,9 +55,9 @@ import io.grpc.LoadBalancer.SubchannelPicker; import io.grpc.LoadBalancer.SubchannelStateListener; import io.grpc.Status; -import io.grpc.util.AbstractRoundRobinLoadBalancer.EmptyPicker; -import io.grpc.util.AbstractRoundRobinLoadBalancer.Ref; import io.grpc.util.RoundRobinLoadBalancer.ReadyPicker; +import io.grpc.util.SubchannelListLoadBalancerCommons.EmptyPicker; +import io.grpc.util.SubchannelListLoadBalancerCommons.Ref; import java.net.SocketAddress; import java.util.ArrayList; import java.util.Arrays; From cf3d6400f9d334cc444c7ece97bacd08932c4f38 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Fri, 3 Feb 2023 15:30:27 -0800 Subject: [PATCH 10/25] remove listener --- .../java/io/grpc/util/RoundRobinLoadBalancer.java | 12 ++---------- .../grpc/util/SubchannelListLoadBalancerCommons.java | 7 +------ 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java index 5ef18384be5..cf6e46f44ea 100644 --- a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java @@ -16,8 +16,6 @@ package io.grpc.util; -import static com.google.common.base.Preconditions.checkNotNull; - import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; @@ -39,20 +37,14 @@ final class RoundRobinLoadBalancer extends LoadBalancer { private final SubchannelListLoadBalancerCommons roundRobinCommons; - private final Helper helper; private final Random random; RoundRobinLoadBalancer(Helper helper) { - this.helper = checkNotNull(helper, "helper"); - this.roundRobinCommons = new SubchannelListLoadBalancerCommons(helper, this::createSubchannel, - () -> { }, this::createReadyPicker); + this.roundRobinCommons = new SubchannelListLoadBalancerCommons(helper, () -> { }, + this::createReadyPicker); this.random = new Random(); } - private Subchannel createSubchannel(CreateSubchannelArgs args) { - return helper.createSubchannel(args); - } - private RoundRobinPicker createReadyPicker(List activeSubchannelList) { // initialize the Picker to a random start index to ensure that a high frequency of Picker // churn does not skew subchannel selection. diff --git a/core/src/main/java/io/grpc/util/SubchannelListLoadBalancerCommons.java b/core/src/main/java/io/grpc/util/SubchannelListLoadBalancerCommons.java index fb17c2ffa3e..07111843599 100644 --- a/core/src/main/java/io/grpc/util/SubchannelListLoadBalancerCommons.java +++ b/core/src/main/java/io/grpc/util/SubchannelListLoadBalancerCommons.java @@ -71,26 +71,21 @@ public class SubchannelListLoadBalancerCommons { new HashMap<>(); private ConnectivityState currentState; private RoundRobinPicker currentPicker = new EmptyPicker(EMPTY_OK); - private final Function createSubchannelTask; private final Runnable afterUpdateTask; private final Function, RoundRobinPicker> createReadyPickerTask; /** * Common new addresses list update handling. * - * @param createSubchannelTask Create a subchannel for a new {@link EquivalentAddressGroup} - * when handling new resolved addresses. * @param afterUpdateTask An action point after accepting the most recent resolved addresses. * @param createReadyPickerTask Returns the picker that selects a subchannel in the list for each * incoming RPC. */ SubchannelListLoadBalancerCommons(Helper helper, - Function createSubchannelTask, Runnable afterUpdateTask, Function, RoundRobinPicker> createReadyPickerTask) { this.helper = checkNotNull(helper, "helper"); - this.createSubchannelTask = checkNotNull(createSubchannelTask, "createSubchannelTask"); this.afterUpdateTask = checkNotNull(afterUpdateTask, "afterUpdateTask"); this.createReadyPickerTask = checkNotNull(createReadyPickerTask, "createReadyPickerTask"); } @@ -134,7 +129,7 @@ public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { new Ref<>(ConnectivityStateInfo.forNonError(IDLE))); final Subchannel subchannel = checkNotNull( - createSubchannelTask.apply(CreateSubchannelArgs.newBuilder() + helper.createSubchannel(CreateSubchannelArgs.newBuilder() .setAddresses(originalAddressGroup) .setAttributes(subchannelAttrs.build()) .build()), From 975eeed5857a6e0f075c76d310512e6cf30f615e Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Mon, 6 Feb 2023 10:03:46 -0800 Subject: [PATCH 11/25] use original round robin wl --- .../io/grpc/util/RoundRobinLoadBalancer.java | 311 +++++++++++++-- .../SubchannelListLoadBalancerCommons.java | 354 ------------------ .../grpc/util/RoundRobinLoadBalancerTest.java | 6 +- .../xds/WeightedRoundRobinLoadBalancer.java | 98 ++--- 4 files changed, 320 insertions(+), 449 deletions(-) delete mode 100644 core/src/main/java/io/grpc/util/SubchannelListLoadBalancerCommons.java diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java index 16cb3a6fbee..c148d48d02c 100644 --- a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 The gRPC Authors + * Copyright 2023 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. @@ -16,79 +16,281 @@ package io.grpc.util; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.ConnectivityState.CONNECTING; +import static io.grpc.ConnectivityState.IDLE; +import static io.grpc.ConnectivityState.READY; +import static io.grpc.ConnectivityState.SHUTDOWN; +import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; + import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; import com.google.common.base.Preconditions; +import io.grpc.Attributes; +import io.grpc.ConnectivityState; import io.grpc.ConnectivityStateInfo; import io.grpc.EquivalentAddressGroup; import io.grpc.Internal; import io.grpc.LoadBalancer; import io.grpc.NameResolver; import io.grpc.Status; -import io.grpc.util.SubchannelListLoadBalancerCommons.RoundRobinPicker; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Random; +import java.util.Set; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicReference; +import javax.annotation.Nonnull; /** - * A {@link SubchannelListLoadBalancerCommons} that provides round-robin load-balancing over the - * {@link EquivalentAddressGroup}s from the {@link NameResolver}. + * A utility function that provides common processing for accepting a list of {@link + * EquivalentAddressGroup}s from the {@link NameResolver}. It provides default implementation of + * accepting {@link io.grpc.LoadBalancer.ResolvedAddresses} updates, process + * aggregated load balancing state. A {@link LoadBalancer} that has sunchannel list will provide + * creating subchannel task and load-balancing strategy over the subchannel list. */ @Internal -public final class RoundRobinLoadBalancer extends LoadBalancer { +public class RoundRobinLoadBalancer extends LoadBalancer { + @VisibleForTesting + static final Attributes.Key> STATE_INFO = + Attributes.Key.create("state-info"); + private final LoadBalancer.Helper helper; + protected final Map subchannels = + new HashMap<>(); + private ConnectivityState currentState; - private final SubchannelListLoadBalancerCommons roundRobinCommons; private final Random random; + private RoundRobinPicker currentPicker = new EmptyPicker(EMPTY_OK); - private final AtomicReference readyPickerRef = new AtomicReference<>(); + public RoundRobinLoadBalancer(Helper helper) { + this.helper = checkNotNull(helper, "helper"); + this.random = new Random(); + } - private final SubchannelStateListener readyListener = new SubchannelStateListener() { - @Override - public void onSubchannelState(ConnectivityStateInfo newState) { - readyPickerRef.set(createReadyPicker()); + /** + * Common new addresses list update handling. + */ + @Override + public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { + if (resolvedAddresses.getAddresses().isEmpty()) { + handleNameResolutionError(Status.UNAVAILABLE.withDescription( + "NameResolver returned no usable address. addrs=" + resolvedAddresses.getAddresses() + + ", attrs=" + resolvedAddresses.getAttributes())); + return false; } - }; - RoundRobinLoadBalancer(Helper helper) { - this.roundRobinCommons = new SubchannelListLoadBalancerCommons(helper, () -> { }, - readyPickerRef, readyListener); - this.random = new Random(); - } + List servers = resolvedAddresses.getAddresses(); + Set currentAddrs = subchannels.keySet(); + Map latestAddrs = stripAttrs(servers); + Set removedAddrs = setsDifference(currentAddrs, latestAddrs.keySet()); - private RoundRobinPicker createReadyPicker() { - // initialize the Picker to a random start index to ensure that a high frequency of Picker - // churn does not skew subchannel selection. - List activeList = roundRobinCommons.getActiveSubchannels(); - return new ReadyPicker(activeList, random.nextInt(activeList.size())); } + for (Map.Entry latestEntry : + latestAddrs.entrySet()) { + EquivalentAddressGroup strippedAddressGroup = latestEntry.getKey(); + EquivalentAddressGroup originalAddressGroup = latestEntry.getValue(); + Subchannel existingSubchannel = subchannels.get(strippedAddressGroup); + if (existingSubchannel != null) { + // EAG's Attributes may have changed. + existingSubchannel.updateAddresses(Collections.singletonList(originalAddressGroup)); + continue; + } + // Create new subchannels for new addresses. + // NB(lukaszx0): we don't merge `attributes` with `subchannelAttr` because subchannel + // doesn't need them. They're describing the resolved server list but we're not taking + // any action based on this information. + Attributes.Builder subchannelAttrs = Attributes.newBuilder() + // NB(lukaszx0): because attributes are immutable we can't set new value for the key + // after creation but since we can mutate the values we leverage that and set + // AtomicReference which will allow mutating state info for given channel. + .set(STATE_INFO, + new Ref<>(ConnectivityStateInfo.forNonError(IDLE))); - @Override - public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { - return roundRobinCommons.acceptResolvedAddresses(resolvedAddresses); + final Subchannel subchannel = checkNotNull( + helper.createSubchannel(CreateSubchannelArgs.newBuilder() + .setAddresses(originalAddressGroup) + .setAttributes(subchannelAttrs.build()) + .build()), + "subchannel"); + subchannel.start(new SubchannelStateListener() { + @Override + public void onSubchannelState(ConnectivityStateInfo state) { + processSubchannelState(subchannel, state); + } + }); + subchannels.put(strippedAddressGroup, subchannel); + subchannel.requestConnection(); + } + + ArrayList removedSubchannels = new ArrayList<>(); + for (EquivalentAddressGroup addressGroup : removedAddrs) { + removedSubchannels.add(subchannels.remove(addressGroup)); + } + + // Update the picker before shutting down the subchannels, to reduce the chance of the race + // between picking a subchannel and shutting it down. + updateBalancingState(); + + // Shutdown removed subchannels + for (Subchannel removedSubchannel : removedSubchannels) { + shutdownSubchannel(removedSubchannel); + } + + return true; } + /** + * Common error handling from the name resolver. + */ @Override public void handleNameResolutionError(Status error) { - roundRobinCommons.handleNameResolutionError(error); + if (currentState != READY) { + updateBalancingState(TRANSIENT_FAILURE, new EmptyPicker(error)); + } + } + + private void processSubchannelState(Subchannel subchannel, ConnectivityStateInfo stateInfo) { + if (subchannels.get(stripAttrs(subchannel.getAddresses())) != subchannel) { + return; + } + if (stateInfo.getState() == TRANSIENT_FAILURE || stateInfo.getState() == IDLE) { + helper.refreshNameResolution(); + } + if (stateInfo.getState() == IDLE) { + subchannel.requestConnection(); + } + Ref subchannelStateRef = getSubchannelStateInfoRef(subchannel); + if (subchannelStateRef.value.getState().equals(TRANSIENT_FAILURE)) { + if (stateInfo.getState().equals(CONNECTING) || stateInfo.getState().equals(IDLE)) { + return; + } + } + subchannelStateRef.value = stateInfo; + updateBalancingState(); + } + + private void shutdownSubchannel(Subchannel subchannel) { + subchannel.shutdown(); + getSubchannelStateInfoRef(subchannel).value = + ConnectivityStateInfo.forNonError(SHUTDOWN); } @Override public void shutdown() { - roundRobinCommons.shutdown(); + for (Subchannel subchannel : getSubchannels()) { + shutdownSubchannel(subchannel); + } + subchannels.clear(); } - @VisibleForTesting - Collection getSubchannels() { - return roundRobinCommons.getSubchannels(); + private static final Status EMPTY_OK = Status.OK.withDescription("no subchannels ready"); + + /** + * Updates picker with the list of active subchannels (state == READY). + */ + @SuppressWarnings("ReferenceEquality") + private void updateBalancingState() { + List activeList = filterNonFailingSubchannels(getSubchannels()); + + if (activeList.isEmpty()) { + // No READY subchannels, determine aggregate state and error status + boolean isConnecting = false; + Status aggStatus = EMPTY_OK; + for (Subchannel subchannel : getSubchannels()) { + ConnectivityStateInfo stateInfo = getSubchannelStateInfoRef(subchannel).value; + // This subchannel IDLE is not because of channel IDLE_TIMEOUT, + // in which case LB is already shutdown. + // RRLB will request connection immediately on subchannel IDLE. + if (stateInfo.getState() == CONNECTING || stateInfo.getState() == IDLE) { + isConnecting = true; + } + if (aggStatus == EMPTY_OK || !aggStatus.isOk()) { + aggStatus = stateInfo.getStatus(); + } + } + updateBalancingState(isConnecting ? CONNECTING : TRANSIENT_FAILURE, + // If all subchannels are TRANSIENT_FAILURE, return the Status associated with + // an arbitrary subchannel, otherwise return OK. + new EmptyPicker(aggStatus)); + } else { + // initialize the Picker to a random start index to ensure that a high frequency of Picker + // churn does not skew subchannel selection. + updateBalancingState(READY, createReadyPicker(activeList, random.nextInt(activeList.size()))); + } + } + + private void updateBalancingState(ConnectivityState state, RoundRobinPicker picker) { + if (state != currentState || !picker.isEquivalentTo(currentPicker)) { + helper.updateBalancingState(state, picker); + currentState = state; + currentPicker = picker; + } + } + + public RoundRobinPicker createReadyPicker(List subchannels, int startIndex) { + return new ReadyPicker(subchannels, startIndex); + } + + /** + * Filters out non-ready subchannels. + */ + private static List filterNonFailingSubchannels( + Collection subchannels) { + List readySubchannels = new ArrayList<>(subchannels.size()); + for (Subchannel subchannel : subchannels) { + if (isReady(subchannel)) { + readySubchannels.add(subchannel); + } + } + return readySubchannels; + } + + /** + * Converts list of {@link EquivalentAddressGroup} to {@link EquivalentAddressGroup} set and + * remove all attributes. The values are the original EAGs. + */ + private static Map stripAttrs( + List groupList) { + Map addrs = new HashMap<>(groupList.size() * 2); + for (EquivalentAddressGroup group : groupList) { + addrs.put(stripAttrs(group), group); + } + return addrs; + } + + private static EquivalentAddressGroup stripAttrs(EquivalentAddressGroup eag) { + return new EquivalentAddressGroup(eag.getAddresses()); + } + + public Collection getSubchannels() { + return subchannels.values(); + } + + private static Ref getSubchannelStateInfoRef( + Subchannel subchannel) { + return checkNotNull(subchannel.getAttributes().get(STATE_INFO), "STATE_INFO"); + } + + // package-private to avoid synthetic access + static boolean isReady(Subchannel subchannel) { + return getSubchannelStateInfoRef(subchannel).value.getState() == READY; + } + + private static Set setsDifference(Set a, Set b) { + Set aCopy = new HashSet<>(a); + aCopy.removeAll(b); + return aCopy; } @VisibleForTesting public static class ReadyPicker extends RoundRobinPicker { private static final AtomicIntegerFieldUpdater indexUpdater = - AtomicIntegerFieldUpdater.newUpdater(ReadyPicker.class, "index"); + AtomicIntegerFieldUpdater.newUpdater(ReadyPicker.class, "index"); private final List list; // non-empty @SuppressWarnings("unused") @@ -134,7 +336,50 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { ReadyPicker other = (ReadyPicker) picker; // the lists cannot contain duplicate subchannels return other == this - || (list.size() == other.list.size() && new HashSet<>(list).containsAll(other.list)); + || (list.size() == other.list.size() && new HashSet<>(list).containsAll(other.list)); + } + } + + // Only subclasses are ReadyPicker or EmptyPicker + public abstract static class RoundRobinPicker extends SubchannelPicker { + public abstract boolean isEquivalentTo(RoundRobinPicker picker); + } + + @VisibleForTesting + static final class EmptyPicker extends RoundRobinPicker { + + private final Status status; + + EmptyPicker(@Nonnull Status status) { + this.status = Preconditions.checkNotNull(status, "status"); + } + + @Override + public PickResult pickSubchannel(PickSubchannelArgs args) { + return status.isOk() ? PickResult.withNoResult() : PickResult.withError(status); + } + + @Override + public boolean isEquivalentTo(RoundRobinPicker picker) { + return picker instanceof EmptyPicker && (Objects.equal(status, ((EmptyPicker) picker).status) + || (status.isOk() && ((EmptyPicker) picker).status.isOk())); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(EmptyPicker.class).add("status", status).toString(); + } + } + + /** + * A lighter weight Reference than AtomicReference. + */ + @VisibleForTesting + static final class Ref { + T value; + + Ref(T value) { + this.value = value; } } } diff --git a/core/src/main/java/io/grpc/util/SubchannelListLoadBalancerCommons.java b/core/src/main/java/io/grpc/util/SubchannelListLoadBalancerCommons.java deleted file mode 100644 index 455cc6b59f7..00000000000 --- a/core/src/main/java/io/grpc/util/SubchannelListLoadBalancerCommons.java +++ /dev/null @@ -1,354 +0,0 @@ -/* - * Copyright 2023 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 io.grpc.util; - -import static com.google.common.base.Preconditions.checkNotNull; -import static io.grpc.ConnectivityState.CONNECTING; -import static io.grpc.ConnectivityState.IDLE; -import static io.grpc.ConnectivityState.READY; -import static io.grpc.ConnectivityState.SHUTDOWN; -import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.MoreObjects; -import com.google.common.base.Objects; -import com.google.common.base.Preconditions; -import io.grpc.Attributes; -import io.grpc.ConnectivityState; -import io.grpc.ConnectivityStateInfo; -import io.grpc.EquivalentAddressGroup; -import io.grpc.Internal; -import io.grpc.LoadBalancer; -import io.grpc.LoadBalancer.CreateSubchannelArgs; -import io.grpc.LoadBalancer.Helper; -import io.grpc.LoadBalancer.PickResult; -import io.grpc.LoadBalancer.PickSubchannelArgs; -import io.grpc.LoadBalancer.ResolvedAddresses; -import io.grpc.LoadBalancer.Subchannel; -import io.grpc.LoadBalancer.SubchannelPicker; -import io.grpc.LoadBalancer.SubchannelStateListener; -import io.grpc.NameResolver; -import io.grpc.Status; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; -import javax.annotation.Nonnull; - -/** - * A utility function that provides common processing for accepting a list of {@link - * EquivalentAddressGroup}s from the {@link NameResolver}. It provides default implementation of - * accepting {@link io.grpc.LoadBalancer.ResolvedAddresses} updates, process - * aggregated load balancing state. A {@link LoadBalancer} that has sunchannel list will provide - * creating subchannel task and load-balancing strategy over the subchannel list. - */ -@Internal -public class SubchannelListLoadBalancerCommons { - @VisibleForTesting - static final Attributes.Key> STATE_INFO = - Attributes.Key.create("state-info"); - private final LoadBalancer.Helper helper; - private final Map subchannels = - new HashMap<>(); - private ConnectivityState currentState; - private RoundRobinPicker currentPicker = new EmptyPicker(EMPTY_OK); - private final Runnable afterUpdateTask; - - private List activeSubchannels; - private final AtomicReference readyPickerRef; - - private final SubchannelStateListener readyListener; - - /** - * Common new addresses list update handling. - * - * @param afterUpdateTask An action point after accepting the most recent resolved addresses. - * @param readyPickerRef The picker reference, to get the picker that selects a subchannel in the - * list for each incoming RPC. - */ - public SubchannelListLoadBalancerCommons(Helper helper, - Runnable afterUpdateTask, - AtomicReference readyPickerRef, - SubchannelStateListener readyListener) { - this.helper = checkNotNull(helper, "helper"); - this.afterUpdateTask = checkNotNull(afterUpdateTask, "afterUpdateTask"); - this.readyPickerRef = checkNotNull(readyPickerRef, "readyPickerRef"); - this.readyListener = checkNotNull(readyListener, "readyListener"); - } - - /** - * Common new addresses list update handling. - */ - public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { - if (resolvedAddresses.getAddresses().isEmpty()) { - handleNameResolutionError(Status.UNAVAILABLE.withDescription( - "NameResolver returned no usable address. addrs=" + resolvedAddresses.getAddresses() - + ", attrs=" + resolvedAddresses.getAttributes())); - return false; - } - - List servers = resolvedAddresses.getAddresses(); - Set currentAddrs = subchannels.keySet(); - Map latestAddrs = stripAttrs(servers); - Set removedAddrs = setsDifference(currentAddrs, latestAddrs.keySet()); - - for (Map.Entry latestEntry : - latestAddrs.entrySet()) { - EquivalentAddressGroup strippedAddressGroup = latestEntry.getKey(); - EquivalentAddressGroup originalAddressGroup = latestEntry.getValue(); - Subchannel existingSubchannel = subchannels.get(strippedAddressGroup); - if (existingSubchannel != null) { - // EAG's Attributes may have changed. - existingSubchannel.updateAddresses(Collections.singletonList(originalAddressGroup)); - continue; - } - // Create new subchannels for new addresses. - - // NB(lukaszx0): we don't merge `attributes` with `subchannelAttr` because subchannel - // doesn't need them. They're describing the resolved server list but we're not taking - // any action based on this information. - Attributes.Builder subchannelAttrs = Attributes.newBuilder() - // NB(lukaszx0): because attributes are immutable we can't set new value for the key - // after creation but since we can mutate the values we leverage that and set - // AtomicReference which will allow mutating state info for given channel. - .set(STATE_INFO, - new Ref<>(ConnectivityStateInfo.forNonError(IDLE))); - - final Subchannel subchannel = checkNotNull( - helper.createSubchannel(CreateSubchannelArgs.newBuilder() - .setAddresses(originalAddressGroup) - .setAttributes(subchannelAttrs.build()) - .build()), - "subchannel"); - subchannel.start(new SubchannelStateListener() { - @Override - public void onSubchannelState(ConnectivityStateInfo state) { - processSubchannelState(subchannel, state); - } - }); - subchannels.put(strippedAddressGroup, subchannel); - subchannel.requestConnection(); - } - - ArrayList removedSubchannels = new ArrayList<>(); - for (EquivalentAddressGroup addressGroup : removedAddrs) { - removedSubchannels.add(subchannels.remove(addressGroup)); - } - - afterUpdateTask.run(); - - // Update the picker before shutting down the subchannels, to reduce the chance of the race - // between picking a subchannel and shutting it down. - updateBalancingState(); - - // Shutdown removed subchannels - for (Subchannel removedSubchannel : removedSubchannels) { - shutdownSubchannel(removedSubchannel); - } - - return true; - } - - /** - * Common error handling from the name resolver. - */ - public void handleNameResolutionError(Status error) { - if (currentState != READY) { - updateBalancingState(TRANSIENT_FAILURE, new EmptyPicker(error)); - } - } - - private void processSubchannelState(Subchannel subchannel, ConnectivityStateInfo stateInfo) { - if (subchannels.get(stripAttrs(subchannel.getAddresses())) != subchannel) { - return; - } - if (stateInfo.getState() == TRANSIENT_FAILURE || stateInfo.getState() == IDLE) { - helper.refreshNameResolution(); - } - if (stateInfo.getState() == IDLE) { - subchannel.requestConnection(); - } - Ref subchannelStateRef = getSubchannelStateInfoRef(subchannel); - if (subchannelStateRef.value.getState().equals(TRANSIENT_FAILURE)) { - if (stateInfo.getState().equals(CONNECTING) || stateInfo.getState().equals(IDLE)) { - return; - } - } - subchannelStateRef.value = stateInfo; - updateBalancingState(); - } - - private void shutdownSubchannel(Subchannel subchannel) { - subchannel.shutdown(); - getSubchannelStateInfoRef(subchannel).value = - ConnectivityStateInfo.forNonError(SHUTDOWN); - } - - public void shutdown() { - for (Subchannel subchannel : getSubchannels()) { - shutdownSubchannel(subchannel); - } - subchannels.clear(); - } - - private static final Status EMPTY_OK = Status.OK.withDescription("no subchannels ready"); - - /** - * Updates picker with the list of active subchannels (state == READY). - */ - @SuppressWarnings("ReferenceEquality") - private void updateBalancingState() { - List activeList = filterNonFailingSubchannels(getSubchannels()); - this.activeSubchannels = activeList; - - if (activeList.isEmpty()) { - // No READY subchannels, determine aggregate state and error status - boolean isConnecting = false; - Status aggStatus = EMPTY_OK; - for (Subchannel subchannel : getSubchannels()) { - ConnectivityStateInfo stateInfo = getSubchannelStateInfoRef(subchannel).value; - // This subchannel IDLE is not because of channel IDLE_TIMEOUT, - // in which case LB is already shutdown. - // RRLB will request connection immediately on subchannel IDLE. - if (stateInfo.getState() == CONNECTING || stateInfo.getState() == IDLE) { - isConnecting = true; - } - if (aggStatus == EMPTY_OK || !aggStatus.isOk()) { - aggStatus = stateInfo.getStatus(); - } - } - updateBalancingState(isConnecting ? CONNECTING : TRANSIENT_FAILURE, - // If all subchannels are TRANSIENT_FAILURE, return the Status associated with - // an arbitrary subchannel, otherwise return OK. - new EmptyPicker(aggStatus)); - } else { - readyListener.onSubchannelState(ConnectivityStateInfo.forNonError(READY)); - updateBalancingState(READY, readyPickerRef.get()); - } - } - - private void updateBalancingState(ConnectivityState state, RoundRobinPicker picker) { - if (state != currentState || !picker.isEquivalentTo(currentPicker)) { - helper.updateBalancingState(state, picker); - currentState = state; - currentPicker = picker; - } - } - - /** - * Filters out non-ready subchannels. - */ - private static List filterNonFailingSubchannels( - Collection subchannels) { - List readySubchannels = new ArrayList<>(subchannels.size()); - for (Subchannel subchannel : subchannels) { - if (isReady(subchannel)) { - readySubchannels.add(subchannel); - } - } - return readySubchannels; - } - - /** - * Converts list of {@link EquivalentAddressGroup} to {@link EquivalentAddressGroup} set and - * remove all attributes. The values are the original EAGs. - */ - private static Map stripAttrs( - List groupList) { - Map addrs = new HashMap<>(groupList.size() * 2); - for (EquivalentAddressGroup group : groupList) { - addrs.put(stripAttrs(group), group); - } - return addrs; - } - - private static EquivalentAddressGroup stripAttrs(EquivalentAddressGroup eag) { - return new EquivalentAddressGroup(eag.getAddresses()); - } - - public Collection getSubchannels() { - return subchannels.values(); - } - - public List getActiveSubchannels() { - return activeSubchannels; - } - - private static Ref getSubchannelStateInfoRef( - Subchannel subchannel) { - return checkNotNull(subchannel.getAttributes().get(STATE_INFO), "STATE_INFO"); - } - - // package-private to avoid synthetic access - static boolean isReady(Subchannel subchannel) { - return getSubchannelStateInfoRef(subchannel).value.getState() == READY; - } - - private static Set setsDifference(Set a, Set b) { - Set aCopy = new HashSet<>(a); - aCopy.removeAll(b); - return aCopy; - } - - // Only subclasses are ReadyPicker or EmptyPicker - public abstract static class RoundRobinPicker extends SubchannelPicker { - public abstract boolean isEquivalentTo(RoundRobinPicker picker); - } - - @VisibleForTesting - static final class EmptyPicker extends RoundRobinPicker { - - private final Status status; - - EmptyPicker(@Nonnull Status status) { - this.status = Preconditions.checkNotNull(status, "status"); - } - - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - return status.isOk() ? PickResult.withNoResult() : PickResult.withError(status); - } - - @Override - public boolean isEquivalentTo(RoundRobinPicker picker) { - return picker instanceof EmptyPicker && (Objects.equal(status, ((EmptyPicker) picker).status) - || (status.isOk() && ((EmptyPicker) picker).status.isOk())); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(EmptyPicker.class).add("status", status).toString(); - } - } - - /** - * A lighter weight Reference than AtomicReference. - */ - @VisibleForTesting - static final class Ref { - T value; - - Ref(T value) { - this.value = value; - } - } -} diff --git a/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java b/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java index 38e552d9071..d4c07e3d50e 100644 --- a/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java +++ b/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java @@ -22,7 +22,7 @@ import static io.grpc.ConnectivityState.READY; import static io.grpc.ConnectivityState.SHUTDOWN; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; -import static io.grpc.util.SubchannelListLoadBalancerCommons.STATE_INFO; +import static io.grpc.util.RoundRobinLoadBalancer.STATE_INFO; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -55,9 +55,9 @@ import io.grpc.LoadBalancer.SubchannelPicker; import io.grpc.LoadBalancer.SubchannelStateListener; import io.grpc.Status; +import io.grpc.util.RoundRobinLoadBalancer.EmptyPicker; import io.grpc.util.RoundRobinLoadBalancer.ReadyPicker; -import io.grpc.util.SubchannelListLoadBalancerCommons.EmptyPicker; -import io.grpc.util.SubchannelListLoadBalancerCommons.Ref; +import io.grpc.util.RoundRobinLoadBalancer.Ref; import java.net.SocketAddress; import java.util.ArrayList; import java.util.Arrays; diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 96119bc8c5a..de1eb945220 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -22,19 +22,14 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; -import com.google.common.util.concurrent.AtomicDouble; -import io.grpc.ConnectivityStateInfo; import io.grpc.EquivalentAddressGroup; import io.grpc.LoadBalancer; import io.grpc.NameResolver; -import io.grpc.Status; import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext.ScheduledHandle; import io.grpc.services.MetricReport; import io.grpc.util.ForwardingSubchannel; -import io.grpc.util.RoundRobinLoadBalancer.ReadyPicker; -import io.grpc.util.SubchannelListLoadBalancerCommons; -import io.grpc.util.SubchannelListLoadBalancerCommons.RoundRobinPicker; +import io.grpc.util.RoundRobinLoadBalancer; import io.grpc.xds.orca.OrcaOobUtil; import io.grpc.xds.orca.OrcaOobUtil.OrcaOobReportListener; import io.grpc.xds.orca.OrcaPerRequestUtil; @@ -43,7 +38,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Random; import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -54,44 +48,33 @@ * the {@link EquivalentAddressGroup}s from the {@link NameResolver}. The subchannel weights are * determined by backend metrics using ORCA. */ -final class WeightedRoundRobinLoadBalancer extends LoadBalancer { - private final Helper helper; - private WeightedRoundRobinLoadBalancerConfig config; // is it thread safe? volatile? +final class WeightedRoundRobinLoadBalancer extends RoundRobinLoadBalancer { + private volatile WeightedRoundRobinLoadBalancerConfig config; private final SynchronizationContext syncContext; private final ScheduledExecutorService timeService; - private final SubchannelListLoadBalancerCommons roundRobinCommons; - private final Random random; - ScheduledHandle weightUpdateTimer; - private final AtomicReference readyPickerRef; - private final SubchannelStateListener readyListener = new SubchannelStateListener() { - @Override - public void onSubchannelState(ConnectivityStateInfo newState) { - readyPickerRef.set(createReadyPicker()); - } - }; + private ScheduledHandle weightUpdateTimer; + private WeightedRoundRobinPicker readyPicker; WeightedRoundRobinLoadBalancer(Helper helper) { - this.helper = OrcaOobUtil.newOrcaReportingHelper(helper); - this.readyPickerRef = new AtomicReference<>(); - this.roundRobinCommons = new SubchannelListLoadBalancerCommons(this.helper, - this::afterSubchannelUpdate, readyPickerRef, readyListener); + super(OrcaOobUtil.newOrcaReportingHelper(helper)); this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext"); this.timeService = checkNotNull(helper.getScheduledExecutorService(), "timeService"); - this.random = new Random(); } @Override public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { config = (WeightedRoundRobinLoadBalancerConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); - boolean accepted = roundRobinCommons.acceptResolvedAddresses(resolvedAddresses); + boolean accepted = super.acceptResolvedAddresses(resolvedAddresses); new UpdateWeightTask().run(); + afterSubchannelUpdate(); return accepted; } - private RoundRobinPicker createReadyPicker() { - List activeList = roundRobinCommons.getActiveSubchannels(); - return new WeightedRoundRobinPicker(activeList, random.nextInt(activeList.size())); + @Override + public RoundRobinPicker createReadyPicker(List activeList, int startIndex) { + this.readyPicker = new WeightedRoundRobinPicker(activeList, startIndex); + return readyPicker; } private final class UpdateWeightTask implements Runnable { @@ -100,9 +83,8 @@ public void run() { if (weightUpdateTimer != null && weightUpdateTimer.isPending()) { return; } - WeightedRoundRobinPicker picker = (WeightedRoundRobinPicker) readyPickerRef.get(); - if (picker != null) { - picker.updateWeight(); + if (readyPicker != null) { + readyPicker.updateWeight(); } weightUpdateTimer = syncContext.schedule(new UpdateWeightTask(), config.weightUpdatePeriodNanos, TimeUnit.NANOSECONDS, timeService); @@ -111,29 +93,25 @@ public void run() { private void afterSubchannelUpdate() { //todo: may optimize to use previous enabled oob to skip - for (Subchannel subchannel : roundRobinCommons.getSubchannels()) { + for (Subchannel subchannel : getSubchannels()) { WeightedRoundRobinSubchannel weightedSubchannel = (WeightedRoundRobinSubchannel) subchannel; if (config.enableOobLoadReport) { OrcaOobUtil.setListener(weightedSubchannel, weightedSubchannel.oobListener, OrcaOobUtil.OrcaReportingConfig.newBuilder() - .setReportInterval(config.oobReportingPeriodNanos, TimeUnit.NANOSECONDS).build()); + .setReportInterval(config.oobReportingPeriodNanos, TimeUnit.NANOSECONDS) + .build()); } else { OrcaOobUtil.setListener(weightedSubchannel, null, null); } } } - @Override - public void handleNameResolutionError(Status error) { - roundRobinCommons.handleNameResolutionError(error); - } - @Override public void shutdown() { if (weightUpdateTimer != null) { weightUpdateTimer.cancel(); } - roundRobinCommons.shutdown(); + super.shutdown(); } final class WeightedRoundRobinSubchannel extends ForwardingSubchannel { @@ -183,7 +161,7 @@ protected Subchannel delegate() { final class WeightedRoundRobinPicker extends ReadyPicker { private final List list; private final AtomicReference schedulerRef; - boolean rrMode = false; + volatile boolean rrMode = false; WeightedRoundRobinPicker(List list, int startIndex) { super(list, startIndex); @@ -210,7 +188,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { } } - void updateWeight() { + private void updateWeight() { EdfScheduler scheduler = new EdfScheduler(schedulerRef.get()); int weightedChannelCount = 0; double avgWeight = 0; @@ -308,7 +286,6 @@ private static final class EdfScheduler { * scheduled objects (virtualTimeNow + period). */ private volatile double virtualTimeNow = 0.0; - private AtomicDouble virtualTimeNowAtomic = new AtomicDouble(0.0); /** * Weights below this value will be logged and upped to this minimum weight. @@ -321,6 +298,8 @@ private static final class EdfScheduler { */ private static final double VIRTUAL_TIME_RESET_THRESHOLD = 1.0 / MINIMUM_WEIGHT * 100.0; + private final Object lock = new Object(); + /** * Use the item's deadline as the order in the priority queue. If the deadlines are the same, * use the index. Index should be unique. @@ -343,7 +322,6 @@ private static final class EdfScheduler { */ EdfScheduler(EdfScheduler state) { this.objects = new HashMap(state.objects); - // is objects.prioQueue thread safe? e.g. picker is updating priority this.prioQueue = new PriorityBlockingQueue<>(state.objects.values()); } @@ -375,28 +353,30 @@ public boolean addOrUpdate(int index, double weight) { /** * Picks the next WRR object. + * Concurrent pick() has issue: * * @return next object index from WRR, -1 if empty; */ public int pick() { - ObjectState minObject = prioQueue.peek(); - if (minObject == null) { - return -1; - } - // Simulate advancing in time by setting the current time to the period of the nearest item - // on the "time horizon". - virtualTimeNowAtomic.compareAndSet() - virtualTimeNow = minObject.priority; - // If virtualTimeNow becomes large, we need to reset everything for numerical stability. - if (virtualTimeNow > VIRTUAL_TIME_RESET_THRESHOLD) { - virtualTimeReset(); + synchronized (lock) { + ObjectState minObject = prioQueue.peek(); + if (minObject == null) { + return -1; + } + // Simulate advancing in time by setting the current time to the period of the nearest item + // on the "time horizon". + virtualTimeNow = minObject.priority; + // If virtualTimeNow becomes large, we need to reset everything for numerical stability. + if (virtualTimeNow > VIRTUAL_TIME_RESET_THRESHOLD) { + virtualTimeReset(); + } + schedule(minObject); + return minObject.index; } - schedule(minObject); - return minObject.index; } private void updatePriority(ObjectState objectState, double newPriority) { - prioQueue.remove(objectState); //guarantees can be removed? multiple pick() happens + prioQueue.remove(objectState); objectState.priority = newPriority; prioQueue.add(objectState); } @@ -473,7 +453,7 @@ static class Builder { Long weightExpirationPeriodNanos = 180_000_000_000L; //3min Boolean enableOobLoadReport = false; Long oobReportingPeriodNanos = 10_000_000_000L; // 10s - Long weightUpdatePeriodNanos = 1_000_000_000L; // 10s + Long weightUpdatePeriodNanos = 100_000_000L; // 1s Builder setBlackoutPeriodNanos(Long blackoutPeriodNanos) { checkArgument(blackoutPeriodNanos != null); From 5300199e1c2e2336aa670f173a17e562eded7a39 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Mon, 6 Feb 2023 12:04:15 -0800 Subject: [PATCH 12/25] add subchannel listener --- .../xds/WeightedRoundRobinLoadBalancer.java | 27 ++++++++++++--- ...eightedRoundRobinLoadBalancerProvider.java | 3 +- .../WeightedRoundRobinLoadBalancerTest.java | 33 +++++++++++++++++++ 3 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index de1eb945220..d35affd77a1 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -22,11 +22,14 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; +import io.grpc.ConnectivityState; +import io.grpc.ConnectivityStateInfo; import io.grpc.EquivalentAddressGroup; import io.grpc.LoadBalancer; import io.grpc.NameResolver; import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext.ScheduledHandle; +import io.grpc.internal.TimeProvider; import io.grpc.services.MetricReport; import io.grpc.util.ForwardingSubchannel; import io.grpc.util.RoundRobinLoadBalancer; @@ -55,10 +58,13 @@ final class WeightedRoundRobinLoadBalancer extends RoundRobinLoadBalancer { private ScheduledHandle weightUpdateTimer; private WeightedRoundRobinPicker readyPicker; - WeightedRoundRobinLoadBalancer(Helper helper) { + private TimeProvider timeProvider; + + WeightedRoundRobinLoadBalancer(Helper helper, TimeProvider timeProvider) { super(OrcaOobUtil.newOrcaReportingHelper(helper)); this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext"); this.timeService = checkNotNull(helper.getScheduledExecutorService(), "timeService"); + this.timeProvider = checkNotNull(timeProvider, "timeProvider"); } @Override @@ -133,14 +139,27 @@ void onLoadReport(MetricReport report) { return; } if (nonEmptySince == Integer.MAX_VALUE) { - nonEmptySince = System.currentTimeMillis(); + nonEmptySince = timeProvider.currentTimeNanos(); } - lastUpdated = System.currentTimeMillis(); + lastUpdated = timeProvider.currentTimeNanos(); weight = newWeight; } + @Override + public void start(SubchannelStateListener listener) { + delegate().start(new SubchannelStateListener() { + @Override + public void onSubchannelState(ConnectivityStateInfo newState) { + if (newState.getState().equals(ConnectivityState.READY)) { + nonEmptySince = Integer.MAX_VALUE; + } + listener.onSubchannelState(newState); + } + }); + } + double getWeight() { - double now = System.currentTimeMillis(); + double now = timeProvider.currentTimeNanos(); if (now - lastUpdated >= config.weightExpirationPeriodNanos) { nonEmptySince = Integer.MAX_VALUE; return 0; diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java index d28280d9e7f..d18eed6696c 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java @@ -21,6 +21,7 @@ import io.grpc.LoadBalancerProvider; import io.grpc.NameResolver.ConfigOrError; import io.grpc.internal.JsonUtil; +import io.grpc.internal.TimeProvider; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinLoadBalancerConfig; import java.util.Map; @@ -33,7 +34,7 @@ final class WeightedRoundRobinLoadBalancerProvider extends LoadBalancerProvider @Override public LoadBalancer newLoadBalancer(Helper helper) { - return new WeightedRoundRobinLoadBalancer(helper); + return new WeightedRoundRobinLoadBalancer(helper, TimeProvider.SYSTEM_TIME_PROVIDER); } @Override diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java new file mode 100644 index 00000000000..a7046a1cac9 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -0,0 +1,33 @@ +/* + * Copyright 2023 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 io.grpc.xds; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class WeightedRoundRobinLoadBalancerTest { + + + @Test + public void weightedPicker() { + + + } +} From 2458ac9a6a29bc7c0d81a80416bd917fec196582 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Tue, 7 Feb 2023 11:26:52 -0800 Subject: [PATCH 13/25] add test --- .../xds/WeightedRoundRobinLoadBalancer.java | 171 +++++++----------- .../java/io/grpc/xds/orca/OrcaOobUtil.java | 13 +- .../WeightedRoundRobinLoadBalancerTest.java | 146 ++++++++++++++- 3 files changed, 213 insertions(+), 117 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index d35affd77a1..543f7054085 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -31,17 +31,16 @@ import io.grpc.SynchronizationContext.ScheduledHandle; import io.grpc.internal.TimeProvider; import io.grpc.services.MetricReport; +import io.grpc.util.ForwardingLoadBalancerHelper; import io.grpc.util.ForwardingSubchannel; import io.grpc.util.RoundRobinLoadBalancer; import io.grpc.xds.orca.OrcaOobUtil; import io.grpc.xds.orca.OrcaOobUtil.OrcaOobReportListener; import io.grpc.xds.orca.OrcaPerRequestUtil; import io.grpc.xds.orca.OrcaPerRequestUtil.OrcaPerRequestReportListener; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.concurrent.PriorityBlockingQueue; +import java.util.PriorityQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -58,13 +57,10 @@ final class WeightedRoundRobinLoadBalancer extends RoundRobinLoadBalancer { private ScheduledHandle weightUpdateTimer; private WeightedRoundRobinPicker readyPicker; - private TimeProvider timeProvider; - WeightedRoundRobinLoadBalancer(Helper helper, TimeProvider timeProvider) { - super(OrcaOobUtil.newOrcaReportingHelper(helper)); + super(new WrrHelper(OrcaOobUtil.newOrcaReportingHelper(helper), timeProvider)); this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext"); this.timeService = checkNotNull(helper.getScheduledExecutorService(), "timeService"); - this.timeProvider = checkNotNull(timeProvider, "timeProvider"); } @Override @@ -100,7 +96,8 @@ public void run() { private void afterSubchannelUpdate() { //todo: may optimize to use previous enabled oob to skip for (Subchannel subchannel : getSubchannels()) { - WeightedRoundRobinSubchannel weightedSubchannel = (WeightedRoundRobinSubchannel) subchannel; + WrrSubchannel weightedSubchannel = (WrrSubchannel) subchannel; + weightedSubchannel.setConfig(config); if (config.enableOobLoadReport) { OrcaOobUtil.setListener(weightedSubchannel, weightedSubchannel.oobListener, OrcaOobUtil.OrcaReportingConfig.newBuilder() @@ -120,16 +117,41 @@ public void shutdown() { super.shutdown(); } - final class WeightedRoundRobinSubchannel extends ForwardingSubchannel { + final static class WrrHelper extends ForwardingLoadBalancerHelper { + private final Helper delegate; + TimeProvider timeProvider; + + WrrHelper(Helper helper, TimeProvider timeProvider) { + this.delegate = helper; + this.timeProvider = timeProvider; + } + @Override + protected Helper delegate() { + return delegate; + } + @Override + public Subchannel createSubchannel(CreateSubchannelArgs args) { + return new WrrSubchannel(delegate().createSubchannel(args), timeProvider); + } + } + + final static class WrrSubchannel extends ForwardingSubchannel { private final Subchannel delegate; private final OrcaOobReportListener oobListener = this::onLoadReport; private final OrcaPerRequestReportListener perRpcListener = this::onLoadReport; volatile long lastUpdated; volatile long nonEmptySince; volatile double weight; + private final TimeProvider timeProvider; + private volatile WeightedRoundRobinLoadBalancerConfig config; - public WeightedRoundRobinSubchannel(Subchannel delegate) { + public WrrSubchannel(Subchannel delegate, TimeProvider timeProvider) { this.delegate = checkNotNull(delegate, "delegate"); + this.timeProvider = checkNotNull(timeProvider, "timeProvider"); + } + + void setConfig(WeightedRoundRobinLoadBalancerConfig config) { + this.config = config; } void onLoadReport(MetricReport report) { @@ -159,6 +181,9 @@ public void onSubchannelState(ConnectivityStateInfo newState) { } double getWeight() { + if (config == null) { + return 0; + } double now = timeProvider.currentTimeNanos(); if (now - lastUpdated >= config.weightExpirationPeriodNanos) { nonEmptySince = Integer.MAX_VALUE; @@ -196,7 +221,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { return super.pickSubchannel(args); } int pickIndex = schedulerRef.get().pick(); - WeightedRoundRobinSubchannel subchannel = (WeightedRoundRobinSubchannel) list.get(pickIndex); + WrrSubchannel subchannel = (WrrSubchannel) list.get(pickIndex); if (config.enableOobLoadReport) { return PickResult.withSubchannel( subchannel, @@ -208,11 +233,11 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { } private void updateWeight() { - EdfScheduler scheduler = new EdfScheduler(schedulerRef.get()); + EdfScheduler scheduler = new EdfScheduler(); int weightedChannelCount = 0; double avgWeight = 0; for (Subchannel value : list) { - double newWeight = ((WeightedRoundRobinSubchannel) value).getWeight(); + double newWeight = ((WrrSubchannel) value).getWeight(); if (newWeight > 0) { avgWeight += newWeight; weightedChannelCount++; @@ -223,9 +248,9 @@ private void updateWeight() { return; } for (int i = 0; i < list.size(); i++) { - WeightedRoundRobinSubchannel subchannel = (WeightedRoundRobinSubchannel) list.get(i); + WrrSubchannel subchannel = (WrrSubchannel) list.get(i); double newWeight = subchannel.getWeight(); - scheduler.addOrUpdate(i, newWeight > 0 ? newWeight : avgWeight); + scheduler.add(i, newWeight > 0 ? newWeight : avgWeight); } schedulerRef.set(scheduler); } @@ -294,8 +319,7 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { * */ private static final class EdfScheduler { - private final Map objects; - private final PriorityBlockingQueue prioQueue; + private final PriorityQueue prioQueue; /** * Upon every pick() the "virtual time" is advanced closer to the period of next items. @@ -304,19 +328,13 @@ private static final class EdfScheduler { * Here we have an explicit "virtualTimeNow", which will be added to the period of all newly * scheduled objects (virtualTimeNow + period). */ - private volatile double virtualTimeNow = 0.0; + private double virtualTimeNow = 0.0; /** * Weights below this value will be logged and upped to this minimum weight. */ private static final double MINIMUM_WEIGHT = 0.0001; - /** - * At what threshold of virtualTimeNow to call periodReset(). - * Simulate 100 picks of zero weights. - */ - private static final double VIRTUAL_TIME_RESET_THRESHOLD = 1.0 / MINIMUM_WEIGHT * 100.0; - private final Object lock = new Object(); /** @@ -324,11 +342,10 @@ private static final class EdfScheduler { * use the index. Index should be unique. */ EdfScheduler() { - this.objects = new HashMap(); - this.prioQueue = new PriorityBlockingQueue<>(10, (o1, o2) -> { - if (o1.priority == o2.priority) { + this.prioQueue = new PriorityQueue(10, (o1, o2) -> { + if (o1.deadline == o2.deadline) { return o1.index - o2.index; - } else if (o1.priority < o2.priority) { + } else if (o1.deadline < o2.deadline) { return -1; } else { return 1; @@ -336,38 +353,17 @@ private static final class EdfScheduler { }); } - /** - * Construct an EdfScheduler with a previous state. - */ - EdfScheduler(EdfScheduler state) { - this.objects = new HashMap(state.objects); - this.prioQueue = new PriorityBlockingQueue<>(state.objects.values()); - } - /** * Adds (or updates) the item in the scheduler. This is not thread safe. * * @param index The field {@link ObjectState#index} to be added/updated * @param weight positive weight for the added/updated object - * @return whether the object was updated (true) or added (false) */ - public boolean addOrUpdate(int index, double weight) { + public void add(int index, double weight) { checkArgument(weight > 0.0, "Weights need to be positive."); - boolean isUpdate = objects.containsKey(index); - ObjectState state = isUpdate ? objects.get(index) : new ObjectState(); - double newWeight = Math.max(weight, MINIMUM_WEIGHT); - - if (isUpdate) { - recordCompletionRatio(state); - double period = calculatePriorityAndSetWeight(state, newWeight); - updatePriority(state, period); - } else { - state.priority = calculatePriorityAndSetWeight(state, newWeight); - prioQueue.add(state); - objects.put(index, state); - } - state.completionRatio = 0; - return isUpdate; + ObjectState state = new ObjectState(Math.max(weight, MINIMUM_WEIGHT), index); + state.deadline = virtualTimeNow + 1 / state.weight; + prioQueue.add(state); } /** @@ -378,73 +374,28 @@ public boolean addOrUpdate(int index, double weight) { */ public int pick() { synchronized (lock) { - ObjectState minObject = prioQueue.peek(); - if (minObject == null) { - return -1; - } + ObjectState minObject = prioQueue.remove(); // Simulate advancing in time by setting the current time to the period of the nearest item // on the "time horizon". - virtualTimeNow = minObject.priority; - // If virtualTimeNow becomes large, we need to reset everything for numerical stability. - if (virtualTimeNow > VIRTUAL_TIME_RESET_THRESHOLD) { - virtualTimeReset(); - } - schedule(minObject); + virtualTimeNow = minObject.deadline; + minObject.deadline = virtualTimeNow + (1.0 / minObject.weight); + prioQueue.add(minObject); return minObject.index; } } - - private void updatePriority(ObjectState objectState, double newPriority) { - prioQueue.remove(objectState); - objectState.priority = newPriority; - prioQueue.add(objectState); - } - - /** - * Schedules the given object at its original period + virtualTimeNow. - */ - private void schedule(ObjectState state) { - updatePriority(state, virtualTimeNow + (1.0 / state.weight)); - } - - private void recordCompletionRatio(ObjectState state) { - state.completionRatio = Math.max(0, - 1 + (virtualTimeNow - state.priority) * state.weight); - } - - private double calculatePriorityAndSetWeight(ObjectState state, double newWeight) { - state.weight = newWeight; - return virtualTimeNow + Math.max(0, (1 - state.completionRatio) / state.weight); - } - - /** - * Get number of objects in the scheduler. - */ - public int size() { - return objects.size(); - } - - /** - * Decrease the periods in the prioQueue by virtualTimeNow. Reset virtualTimeNow to zero. - * Since the iterator on is iterating over the heap in the heap order - * and it is a min-heap, we will first be reducing the period of parent nodes in the heap. - * As such the siftUp() operation of setPriority() will be O(1) and thus the whole thing O(n). - */ - private void virtualTimeReset() { - for (ObjectState state : objects.values()) { - updatePriority(state, Math.max(state.priority - virtualTimeNow, 0.0)); - } - virtualTimeNow = 0.0; - } } /** Holds the state of the object. */ @VisibleForTesting static class ObjectState { - double weight; - double completionRatio; - volatile double priority; - int index; + private final double weight; + private final int index; + volatile double deadline; + + ObjectState(double weight, int index) { + this.weight = weight; + this.index = index; + } } static final class WeightedRoundRobinLoadBalancerConfig { diff --git a/xds/src/main/java/io/grpc/xds/orca/OrcaOobUtil.java b/xds/src/main/java/io/grpc/xds/orca/OrcaOobUtil.java index 016c4ba0eb5..766c66b7370 100644 --- a/xds/src/main/java/io/grpc/xds/orca/OrcaOobUtil.java +++ b/xds/src/main/java/io/grpc/xds/orca/OrcaOobUtil.java @@ -305,18 +305,23 @@ public void run() { if (oldListener != null) { configs.remove(oldListener); } + if (listener != null) { + configs.put(listener, config); + } orcaSubchannel.reportListener = listener; - setReportingConfig(listener, config); + setReportingConfig(config); } }); } - private void setReportingConfig(OrcaOobReportListener listener, OrcaReportingConfig config) { + private void setReportingConfig(OrcaReportingConfig config) { boolean reconfigured = false; - configs.put(listener, config); // Real reporting interval is the minimum of intervals requested by all participating // helpers. - if (overallConfig == null) { + if (configs.isEmpty()) { + overallConfig = null; + reconfigured = true; + } else if (overallConfig == null) { overallConfig = config.toBuilder().build(); reconfigured = true; } else { diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index a7046a1cac9..3b421fb89a5 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -14,20 +14,160 @@ * limitations under the License. */ - package io.grpc.xds; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import io.grpc.Attributes; +import io.grpc.ChannelLogger; +import io.grpc.ConnectivityState; +import io.grpc.ConnectivityStateInfo; +import io.grpc.EquivalentAddressGroup; +import io.grpc.LoadBalancer; +import io.grpc.LoadBalancer.CreateSubchannelArgs; +import io.grpc.LoadBalancer.Helper; +import io.grpc.LoadBalancer.Subchannel; +import io.grpc.LoadBalancer.SubchannelStateListener; +import io.grpc.SynchronizationContext; +import io.grpc.internal.FakeClock; +import io.grpc.services.InternalCallMetricRecorder; +import io.grpc.xds.WeightedRoundRobinLoadBalancer.WrrSubchannel; +import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinPicker; +import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinLoadBalancerConfig; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; +import java.net.SocketAddress; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; @RunWith(JUnit4.class) public class WeightedRoundRobinLoadBalancerTest { + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + @Mock + Helper helper; + private final List servers = Lists.newArrayList(); + private final Map, Subchannel> subchannels = Maps.newLinkedHashMap(); + private final Map subchannelStateListeners = + Maps.newLinkedHashMap(); + @Captor + private ArgumentCaptor pickerCaptor; + private WeightedRoundRobinLoadBalancer wrr; + private final FakeClock fakeClock = new FakeClock(); + + private final WeightedRoundRobinLoadBalancerConfig weightedConfig = + new WeightedRoundRobinLoadBalancerConfig.Builder().build(); + + private static final Attributes.Key MAJOR_KEY = Attributes.Key.create("major-key"); + private final Attributes affinity = + Attributes.newBuilder().set(MAJOR_KEY, "I got the keys").build(); - @Test - public void weightedPicker() { + private final SynchronizationContext syncContext = new SynchronizationContext( + new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread t, Throwable e) { + throw new AssertionError(e); + } + }); + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + for (int i = 0; i < 3; i++) { + SocketAddress addr = new FakeSocketAddress("server" + i); + EquivalentAddressGroup eag = new EquivalentAddressGroup(addr); + servers.add(eag); + Subchannel sc = mock(Subchannel.class); + subchannels.put(Arrays.asList(eag), sc); + } + when(helper.getSynchronizationContext()).thenReturn(syncContext); + when(helper.getScheduledExecutorService()).thenReturn( + fakeClock.getScheduledExecutorService()); + when(helper.createSubchannel(any(CreateSubchannelArgs.class))) + .then(new Answer() { + @Override + public Subchannel answer(InvocationOnMock invocation) throws Throwable { + CreateSubchannelArgs args = (CreateSubchannelArgs) invocation.getArguments()[0]; + final Subchannel subchannel = subchannels.get(args.getAddresses()); + when(subchannel.getAllAddresses()).thenReturn(args.getAddresses()); + when(subchannel.getAttributes()).thenReturn(args.getAttributes()); + when(subchannel.getChannelLogger()).thenReturn(mock(ChannelLogger.class)); + doAnswer( + new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + subchannelStateListeners.put( + subchannel, (SubchannelStateListener) invocation.getArguments()[0]); + return null; + } + }).when(subchannel).start(any(SubchannelStateListener.class)); + return subchannel; + } + }); + wrr = new WeightedRoundRobinLoadBalancer(helper, fakeClock.getTimeProvider()); + } + + @Test + public void pickByWeight() { + syncContext.execute(() -> wrr.acceptResolvedAddresses(LoadBalancer.ResolvedAddresses.newBuilder() + .setAddresses(servers).setLoadBalancingPolicyConfig(weightedConfig) + .setAttributes(affinity).build())); + verify(helper, times(3)).createSubchannel( + any(CreateSubchannelArgs.class)); + Iterator it = subchannels.values().iterator(); + Subchannel readySubchannel1 = it.next(); + subchannelStateListeners.get(readySubchannel1).onSubchannelState(ConnectivityStateInfo. + forNonError(ConnectivityState.READY)); + + Subchannel readySubchannel2 = it.next(); + subchannelStateListeners.get(readySubchannel2).onSubchannelState(ConnectivityStateInfo. + forNonError(ConnectivityState.READY)); + verify(helper, times(2)).updateBalancingState(eq(ConnectivityState.READY), pickerCaptor.capture()); + WeightedRoundRobinPicker weightedPicker = pickerCaptor.getAllValues().get(1); + WrrSubchannel weightedSubchannel1 = (WrrSubchannel) weightedPicker.getList().get(0); + WrrSubchannel weightedSubchannel2 = (WrrSubchannel) weightedPicker.getList().get(1); + weightedSubchannel1.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.1, 0.1, 1, new HashMap<>(), new HashMap<>())); + weightedSubchannel2.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.2, 0.1, 1, new HashMap<>(), new HashMap<>())); + fakeClock.forwardTime(11, TimeUnit.SECONDS); + assertThat(weightedPicker.pickSubchannel(mock(LoadBalancer.PickSubchannelArgs.class)) + .getSubchannel()).isEqualTo(weightedSubchannel1); + } + private static class FakeSocketAddress extends SocketAddress { + final String name; + FakeSocketAddress(String name) { + this.name = name; + } + @Override + public String toString() { + return "FakeSocketAddress-" + name; + } } } From 23231ebb4b88aa0b716a5df594f6f39f7e384bdf Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Wed, 8 Feb 2023 12:32:46 -0800 Subject: [PATCH 14/25] add more tests --- .../io/grpc/util/RoundRobinLoadBalancer.java | 3 +- .../xds/WeightedRoundRobinLoadBalancer.java | 37 ++- ...eightedRoundRobinLoadBalancerProvider.java | 7 +- .../WeightedRoundRobinLoadBalancerTest.java | 239 +++++++++++++++++- 4 files changed, 267 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java index da2c3afebbe..3a835584f4a 100644 --- a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java @@ -336,8 +336,7 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { } } - @VisibleForTesting - static final class EmptyPicker extends RoundRobinPicker { + public static final class EmptyPicker extends RoundRobinPicker { private final Status status; diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 09bc14b33f8..f183710aa2f 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -27,6 +27,7 @@ import io.grpc.EquivalentAddressGroup; import io.grpc.LoadBalancer; import io.grpc.NameResolver; +import io.grpc.Status; import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext.ScheduledHandle; import io.grpc.internal.TimeProvider; @@ -66,8 +67,15 @@ final class WeightedRoundRobinLoadBalancer extends RoundRobinLoadBalancer { @Override public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { + if (resolvedAddresses.getLoadBalancingPolicyConfig() == null) { + handleNameResolutionError(Status.UNAVAILABLE.withDescription( + "NameResolver returned no WeightedRoundRobinLoadBalancerConfig. addrs=" + + resolvedAddresses.getAddresses() + + ", attrs=" + resolvedAddresses.getAttributes())); + return false; + } config = - (WeightedRoundRobinLoadBalancerConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); + (WeightedRoundRobinLoadBalancerConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); boolean accepted = super.acceptResolvedAddresses(resolvedAddresses); new UpdateWeightTask().run(); afterSubchannelUpdate(); @@ -138,6 +146,7 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { } } + @VisibleForTesting static final class WrrSubchannel extends ForwardingSubchannel { private final Subchannel delegate; @@ -210,7 +219,7 @@ protected Subchannel delegate() { final class WeightedRoundRobinPicker extends ReadyPicker { private final List list; private final AtomicReference schedulerRef; - volatile boolean rrMode = false; + private volatile boolean rrMode = false; WeightedRoundRobinPicker(List list, int startIndex) { super(list, startIndex); @@ -227,7 +236,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { } int pickIndex = schedulerRef.get().pick(); WrrSubchannel subchannel = (WrrSubchannel) list.get(pickIndex); - if (config.enableOobLoadReport) { + if (!config.enableOobLoadReport) { return PickResult.withSubchannel( subchannel, OrcaPerRequestUtil.getInstance().newOrcaClientStreamTracerFactory( @@ -284,12 +293,12 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { } /** - * An earliest deadline first implementation in which each object is + * The earliest deadline first implementation in which each object is * chosen deterministically and periodically with frequency proportional to its weight. * *

Specifically, each object added to chooser is given a period equal to the multiplicative * inverse of its weight. The place of each object in its period is tracked, and each call to - * choose returns the child with the least remaining time in its period (1/weight). + * choose returns the object with the least remaining time in its period (1/weight). * (Ties are broken by the order in which the children were added to the chooser.) * For example, if items A and B are added * with weights 0.5 and 0.2, successive chooses return: @@ -328,8 +337,6 @@ private static final class EdfScheduler { /** * Upon every pick() the "virtual time" is advanced closer to the period of next items. - * In the WeightedRoundRobinChooser implementation this is done by subtracting the period of the - * picked object from all other items yielding O(n). * Here we have an explicit "virtualTimeNow", which will be added to the period of all newly * scheduled objects (virtualTimeNow + period). */ @@ -364,7 +371,7 @@ private static final class EdfScheduler { * @param index The field {@link ObjectState#index} to be added/updated * @param weight positive weight for the added/updated object */ - public void add(int index, double weight) { + void add(int index, double weight) { checkArgument(weight > 0.0, "Weights need to be positive."); ObjectState state = new ObjectState(Math.max(weight, MINIMUM_WEIGHT), index); state.deadline = virtualTimeNow + 1 / state.weight; @@ -373,11 +380,8 @@ public void add(int index, double weight) { /** * Picks the next WRR object. - * Concurrent pick() has issue: - * - * @return next object index from WRR, -1 if empty; */ - public int pick() { + int pick() { synchronized (lock) { ObjectState minObject = prioQueue.remove(); // Simulate advancing in time by setting the current time to the period of the nearest item @@ -410,6 +414,9 @@ static final class WeightedRoundRobinLoadBalancerConfig { final Long oobReportingPeriodNanos; final Long weightUpdatePeriodNanos; + public static Builder newBuilder() { + return new Builder(); + } private WeightedRoundRobinLoadBalancerConfig(Long blackoutPeriodNanos, Long weightExpirationPeriodNanos, @@ -423,13 +430,17 @@ private WeightedRoundRobinLoadBalancerConfig(Long blackoutPeriodNanos, this.weightUpdatePeriodNanos = weightUpdatePeriodNanos; } - static class Builder { + static final class Builder { Long blackoutPeriodNanos = 10_000_000_000L; // 10s Long weightExpirationPeriodNanos = 180_000_000_000L; //3min Boolean enableOobLoadReport = false; Long oobReportingPeriodNanos = 10_000_000_000L; // 10s Long weightUpdatePeriodNanos = 100_000_000L; // 1s + private Builder() { + + } + Builder setBlackoutPeriodNanos(Long blackoutPeriodNanos) { checkArgument(blackoutPeriodNanos != null); this.blackoutPeriodNanos = blackoutPeriodNanos; diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java index d18eed6696c..880d270b0e6 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java @@ -62,16 +62,19 @@ public ConfigOrError parseLoadBalancingPolicyConfig(Map rawConfig) { Long weightUpdatePeriodNanos = JsonUtil.getStringAsDuration(rawConfig, "weightUpdatePeriod"); WeightedRoundRobinLoadBalancerConfig.Builder configBuilder = - new WeightedRoundRobinLoadBalancerConfig.Builder(); + WeightedRoundRobinLoadBalancerConfig.newBuilder(); if (blackoutPeriodNanos != null) { configBuilder.setBlackoutPeriodNanos(blackoutPeriodNanos); } if (weightExpirationPeriodNanos != null) { configBuilder.setWeightExpirationPeriodNanos(weightExpirationPeriodNanos); } - if (oobReportingPeriodNanos != null) { + if (enableOobLoadReport != null) { configBuilder.setEnableOobLoadReport(enableOobLoadReport); } + if (oobReportingPeriodNanos != null) { + configBuilder.setOobReportingPeriodNanos(oobReportingPeriodNanos); + } if (weightUpdatePeriodNanos != null) { configBuilder.setWeightUpdatePeriodNanos(weightUpdatePeriodNanos); if (weightUpdatePeriodNanos < MIN_WEIGHT_UPDATE_PERIOD_NANOS) { diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index ae0d6e3cce6..4c26a283e63 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -21,31 +21,39 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import com.github.xds.data.orca.v3.OrcaLoadReport; +import com.github.xds.service.orca.v3.OrcaLoadReportRequest; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import io.grpc.Attributes; +import io.grpc.Channel; import io.grpc.ChannelLogger; +import io.grpc.ClientCall; import io.grpc.ConnectivityState; import io.grpc.ConnectivityStateInfo; import io.grpc.EquivalentAddressGroup; import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.CreateSubchannelArgs; import io.grpc.LoadBalancer.Helper; +import io.grpc.LoadBalancer.PickResult; import io.grpc.LoadBalancer.ResolvedAddresses; import io.grpc.LoadBalancer.Subchannel; import io.grpc.LoadBalancer.SubchannelStateListener; import io.grpc.SynchronizationContext; import io.grpc.internal.FakeClock; import io.grpc.services.InternalCallMetricRecorder; +import io.grpc.util.RoundRobinLoadBalancer.EmptyPicker; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinLoadBalancerConfig; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinPicker; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WrrSubchannel; import java.net.SocketAddress; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; @@ -80,21 +88,28 @@ public class WeightedRoundRobinLoadBalancerTest { private ArgumentCaptor pickerCaptor; private final List servers = Lists.newArrayList(); + private final Map, Subchannel> subchannels = Maps.newLinkedHashMap(); + private final Map subchannelStateListeners = Maps.newLinkedHashMap(); + private final List> oobCalls = + new ArrayList<>(); + private WeightedRoundRobinLoadBalancer wrr; + private final FakeClock fakeClock = new FakeClock(); - private final WeightedRoundRobinLoadBalancerConfig weightedConfig = - new WeightedRoundRobinLoadBalancerConfig.Builder().build(); + private WeightedRoundRobinLoadBalancerConfig weightedConfig = + WeightedRoundRobinLoadBalancerConfig.newBuilder().build(); private static final Attributes.Key MAJOR_KEY = Attributes.Key.create("major-key"); private final Attributes affinity = Attributes.newBuilder().set(MAJOR_KEY, "I got the keys").build(); + private static final double EDF_PRECISE = 0.01; private final SynchronizationContext syncContext = new SynchronizationContext( new Thread.UncaughtExceptionHandler() { @Override @@ -110,6 +125,19 @@ public void setup() { EquivalentAddressGroup eag = new EquivalentAddressGroup(addr); servers.add(eag); Subchannel sc = mock(Subchannel.class); + Channel channel = mock(Channel.class); + when(channel.newCall(any(), any())).then( + new Answer>() { + @SuppressWarnings("unchecked") + @Override + public ClientCall answer( + InvocationOnMock invocation) throws Throwable { + ClientCall clientCall = mock(ClientCall.class); + oobCalls.add(clientCall); + return clientCall; + } + }); + when(sc.asChannel()).thenReturn(channel); subchannels.put(Arrays.asList(eag), sc); } when(helper.getSynchronizationContext()).thenReturn(syncContext); @@ -157,6 +185,8 @@ public void wrrLifeCycle() { .forNonError(ConnectivityState.READY)); verify(helper, times(2)).updateBalancingState( eq(ConnectivityState.READY), pickerCaptor.capture()); + assertThat(pickerCaptor.getAllValues().size()).isEqualTo(2); + assertThat(pickerCaptor.getAllValues().get(0).getList().size()).isEqualTo(1); WeightedRoundRobinPicker weightedPicker = pickerCaptor.getAllValues().get(1); WrrSubchannel weightedSubchannel1 = (WrrSubchannel) weightedPicker.getList().get(0); WrrSubchannel weightedSubchannel2 = (WrrSubchannel) weightedPicker.getList().get(1); @@ -176,6 +206,211 @@ public void wrrLifeCycle() { verifyNoMoreInteractions(mockArgs); } + @Test + public void enableOobLoadReportConfig() { + syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() + .setAddresses(servers).setLoadBalancingPolicyConfig(weightedConfig) + .setAttributes(affinity).build())); + verify(helper, times(3)).createSubchannel( + any(CreateSubchannelArgs.class)); + Iterator it = subchannels.values().iterator(); + Subchannel readySubchannel1 = it.next(); + subchannelStateListeners.get(readySubchannel1).onSubchannelState(ConnectivityStateInfo + .forNonError(ConnectivityState.READY)); + Subchannel readySubchannel2 = it.next(); + subchannelStateListeners.get(readySubchannel2).onSubchannelState(ConnectivityStateInfo + .forNonError(ConnectivityState.READY)); + verify(helper, times(2)).updateBalancingState( + eq(ConnectivityState.READY), pickerCaptor.capture()); + WeightedRoundRobinPicker weightedPicker = pickerCaptor.getAllValues().get(1); + WrrSubchannel weightedSubchannel1 = (WrrSubchannel) weightedPicker.getList().get(0); + WrrSubchannel weightedSubchannel2 = (WrrSubchannel) weightedPicker.getList().get(1); + weightedSubchannel1.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.1, 0.1, 1, new HashMap<>(), new HashMap<>())); + weightedSubchannel2.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.9, 0.1, 1, new HashMap<>(), new HashMap<>())); + assertThat(fakeClock.forwardTime(11, TimeUnit.SECONDS)).isEqualTo(1); + PickResult pickResult = weightedPicker.pickSubchannel(mockArgs); + assertThat(pickResult.getSubchannel()).isEqualTo(weightedSubchannel1); + assertThat(pickResult.getStreamTracerFactory()).isNotNull(); + assertThat(oobCalls.isEmpty()).isTrue(); + weightedConfig = WeightedRoundRobinLoadBalancerConfig.newBuilder().setEnableOobLoadReport(true) + .build(); + syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() + .setAddresses(servers).setLoadBalancingPolicyConfig(weightedConfig) + .setAttributes(affinity).build())); + pickResult = weightedPicker.pickSubchannel(mockArgs); + assertThat(pickResult.getSubchannel()).isEqualTo(weightedSubchannel1); + assertThat(pickResult.getStreamTracerFactory()).isNull(); + assertThat(oobCalls.size()).isEqualTo(2); + } + + @Test + public void pickByWeight() { + syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() + .setAddresses(servers).setLoadBalancingPolicyConfig(weightedConfig) + .setAttributes(affinity).build())); + verify(helper, times(3)).createSubchannel( + any(CreateSubchannelArgs.class)); + assertThat(fakeClock.getPendingTasks().size()).isEqualTo(1); + + Iterator it = subchannels.values().iterator(); + Subchannel readySubchannel1 = it.next(); + subchannelStateListeners.get(readySubchannel1).onSubchannelState(ConnectivityStateInfo + .forNonError(ConnectivityState.READY)); + Subchannel readySubchannel2 = it.next(); + subchannelStateListeners.get(readySubchannel2).onSubchannelState(ConnectivityStateInfo + .forNonError(ConnectivityState.READY)); + Subchannel readySubchannel3 = it.next(); + subchannelStateListeners.get(readySubchannel3).onSubchannelState(ConnectivityStateInfo + .forNonError(ConnectivityState.READY)); + verify(helper, times(3)).updateBalancingState( + eq(ConnectivityState.READY), pickerCaptor.capture()); + WeightedRoundRobinPicker weightedPicker = pickerCaptor.getAllValues().get(2); + WrrSubchannel weightedSubchannel1 = (WrrSubchannel) weightedPicker.getList().get(0); + WrrSubchannel weightedSubchannel2 = (WrrSubchannel) weightedPicker.getList().get(1); + WrrSubchannel weightedSubchannel3 = (WrrSubchannel) weightedPicker.getList().get(2); + weightedSubchannel1.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.12, 0.1, 22, new HashMap<>(), new HashMap<>())); + weightedSubchannel2.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.28, 0.1, 40, new HashMap<>(), new HashMap<>())); + weightedSubchannel3.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.86, 0.1, 100, new HashMap<>(), new HashMap<>())); + assertThat(fakeClock.forwardTime(11, TimeUnit.SECONDS)).isEqualTo(1); + Map pickCount = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + Subchannel result = weightedPicker.pickSubchannel(mockArgs).getSubchannel(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + } + assertThat(pickCount.size()).isEqualTo(3); + assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 + - 22 / 0.12 / (22 / 0.12 + 40 / 0.28 + 100 / 0.86))).isAtMost(EDF_PRECISE); + assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 + - 40 / 0.28 / (22 / 0.12 + 40 / 0.28 + 100 / 0.86) )).isAtMost(EDF_PRECISE); + assertThat(Math.abs(pickCount.get(weightedSubchannel3) / 1000.0 + - 100 / 0.86 / ( 22 / 0.12 + 40 / 0.28 + 100 / 0.86) )).isAtMost(EDF_PRECISE); + } + + @Test + public void emptyConfig() { + assertThat(wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() + .setAddresses(servers).setLoadBalancingPolicyConfig(null) + .setAttributes(affinity).build())).isFalse(); + verify(helper, never()).createSubchannel(any(CreateSubchannelArgs.class)); + verify(helper).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); + assertThat(fakeClock.getPendingTasks()).isEmpty(); + + syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() + .setAddresses(servers).setLoadBalancingPolicyConfig(weightedConfig) + .setAttributes(affinity).build())); + verify(helper, times(3)).createSubchannel( + any(CreateSubchannelArgs.class)); + verify(helper).updateBalancingState(eq(ConnectivityState.CONNECTING), pickerCaptor.capture()); + assertThat(pickerCaptor.getValue()).isInstanceOf(EmptyPicker.class); + assertThat(fakeClock.forwardTime(11, TimeUnit.SECONDS)).isEqualTo(1); + } + + @Test + public void blackoutPeriod() { + syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() + .setAddresses(servers).setLoadBalancingPolicyConfig(weightedConfig) + .setAttributes(affinity).build())); + verify(helper, times(3)).createSubchannel( + any(CreateSubchannelArgs.class)); + assertThat(fakeClock.getPendingTasks().size()).isEqualTo(1); + + Iterator it = subchannels.values().iterator(); + Subchannel readySubchannel1 = it.next(); + subchannelStateListeners.get(readySubchannel1).onSubchannelState(ConnectivityStateInfo + .forNonError(ConnectivityState.READY)); + Subchannel readySubchannel2 = it.next(); + subchannelStateListeners.get(readySubchannel2).onSubchannelState(ConnectivityStateInfo + .forNonError(ConnectivityState.READY)); + verify(helper, times(2)).updateBalancingState( + eq(ConnectivityState.READY), pickerCaptor.capture()); + WeightedRoundRobinPicker weightedPicker = pickerCaptor.getAllValues().get(1); + WrrSubchannel weightedSubchannel1 = (WrrSubchannel) weightedPicker.getList().get(0); + WrrSubchannel weightedSubchannel2 = (WrrSubchannel) weightedPicker.getList().get(1); + weightedSubchannel1.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.1, 0.1, 1, new HashMap<>(), new HashMap<>())); + weightedSubchannel2.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.2, 0.1, 1, new HashMap<>(), new HashMap<>())); + assertThat(fakeClock.forwardTime(5, TimeUnit.SECONDS)).isEqualTo(1); + Map pickCount = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + Subchannel result = weightedPicker.pickSubchannel(mockArgs).getSubchannel(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + } + assertThat(pickCount.size()).isEqualTo(2); + // within blackout period, fallback to simple round robin + assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 0.5)).isAtMost(EDF_PRECISE); + assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 0.5)).isAtMost(EDF_PRECISE); + + assertThat(fakeClock.forwardTime(5, TimeUnit.SECONDS)).isEqualTo(1); + pickCount = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + Subchannel result = weightedPicker.pickSubchannel(mockArgs).getSubchannel(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + } + assertThat(pickCount.size()).isEqualTo(2); + // after blackout period + assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 2.0 / 3)) + .isAtMost(EDF_PRECISE); + assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 1.0 / 3)) + .isAtMost(EDF_PRECISE); + } + + @Test + public void weightExpired() { + syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() + .setAddresses(servers).setLoadBalancingPolicyConfig(weightedConfig) + .setAttributes(affinity).build())); + verify(helper, times(3)).createSubchannel( + any(CreateSubchannelArgs.class)); + assertThat(fakeClock.getPendingTasks().size()).isEqualTo(1); + + Iterator it = subchannels.values().iterator(); + Subchannel readySubchannel1 = it.next(); + subchannelStateListeners.get(readySubchannel1).onSubchannelState(ConnectivityStateInfo + .forNonError(ConnectivityState.READY)); + Subchannel readySubchannel2 = it.next(); + subchannelStateListeners.get(readySubchannel2).onSubchannelState(ConnectivityStateInfo + .forNonError(ConnectivityState.READY)); + verify(helper, times(2)).updateBalancingState( + eq(ConnectivityState.READY), pickerCaptor.capture()); + WeightedRoundRobinPicker weightedPicker = pickerCaptor.getAllValues().get(1); + WrrSubchannel weightedSubchannel1 = (WrrSubchannel) weightedPicker.getList().get(0); + WrrSubchannel weightedSubchannel2 = (WrrSubchannel) weightedPicker.getList().get(1); + weightedSubchannel1.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.1, 0.1, 1, new HashMap<>(), new HashMap<>())); + weightedSubchannel2.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.2, 0.1, 1, new HashMap<>(), new HashMap<>())); + assertThat(fakeClock.forwardTime(10, TimeUnit.SECONDS)).isEqualTo(1); + Map pickCount = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + Subchannel result = weightedPicker.pickSubchannel(mockArgs).getSubchannel(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + } + assertThat(pickCount.size()).isEqualTo(2); + assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 2.0 / 3)) + .isAtMost(EDF_PRECISE); + assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 1.0 / 3)) + .isAtMost(EDF_PRECISE); + + // weight expired, fallback to simple round robin + assertThat(fakeClock.forwardTime(300, TimeUnit.SECONDS)).isEqualTo(1); + pickCount = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + Subchannel result = weightedPicker.pickSubchannel(mockArgs).getSubchannel(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + } + assertThat(pickCount.size()).isEqualTo(2); + assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 0.5)) + .isAtMost(EDF_PRECISE); + assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 0.5)) + .isAtMost(EDF_PRECISE); + } + private static class FakeSocketAddress extends SocketAddress { final String name; From 3d514052e78cd45d810caf28e28fb8fb048d2cfc Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Thu, 9 Feb 2023 10:56:17 -0800 Subject: [PATCH 15/25] add provider test --- xds/BUILD.bazel | 2 + .../xds/WeightedRoundRobinLoadBalancer.java | 18 +-- ...eightedRoundRobinLoadBalancerProvider.java | 8 +- .../services/io.grpc.LoadBalancerProvider | 1 + ...tedRoundRobinLoadBalancerProviderTest.java | 114 ++++++++++++++++++ .../WeightedRoundRobinLoadBalancerTest.java | 18 ++- 6 files changed, 147 insertions(+), 14 deletions(-) create mode 100644 xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProviderTest.java diff --git a/xds/BUILD.bazel b/xds/BUILD.bazel index e62b183f9e8..8f9198953a0 100644 --- a/xds/BUILD.bazel +++ b/xds/BUILD.bazel @@ -40,6 +40,8 @@ java_library( "//core:util", "//netty", "//stub", + "//services:metrics", + "//services:metrics_internal", "@com_google_code_findbugs_jsr305//jar", "@com_google_code_gson_gson//jar", "@com_google_errorprone_error_prone_annotations//jar", diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index f183710aa2f..558bf6c3169 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -25,6 +25,7 @@ import io.grpc.ConnectivityState; import io.grpc.ConnectivityStateInfo; import io.grpc.EquivalentAddressGroup; +import io.grpc.Internal; import io.grpc.LoadBalancer; import io.grpc.NameResolver; import io.grpc.Status; @@ -51,14 +52,15 @@ * the {@link EquivalentAddressGroup}s from the {@link NameResolver}. The subchannel weights are * determined by backend metrics using ORCA. */ -final class WeightedRoundRobinLoadBalancer extends RoundRobinLoadBalancer { +@Internal +public final class WeightedRoundRobinLoadBalancer extends RoundRobinLoadBalancer { private volatile WeightedRoundRobinLoadBalancerConfig config; private final SynchronizationContext syncContext; private final ScheduledExecutorService timeService; private ScheduledHandle weightUpdateTimer; private WeightedRoundRobinPicker readyPicker; - WeightedRoundRobinLoadBalancer(Helper helper, TimeProvider timeProvider) { + public WeightedRoundRobinLoadBalancer(Helper helper, TimeProvider timeProvider) { super(new WrrHelper(OrcaOobUtil.newOrcaReportingHelper(helper), checkNotNull(timeProvider, "timeProvider"))); this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext"); @@ -78,12 +80,12 @@ public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { (WeightedRoundRobinLoadBalancerConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); boolean accepted = super.acceptResolvedAddresses(resolvedAddresses); new UpdateWeightTask().run(); - afterSubchannelUpdate(); + afterAcceptAddresses(); return accepted; } @Override - protected RoundRobinPicker createReadyPicker(List activeList, int startIndex) { + public RoundRobinPicker createReadyPicker(List activeList, int startIndex) { this.readyPicker = new WeightedRoundRobinPicker(activeList, startIndex); return readyPicker; } @@ -102,8 +104,7 @@ public void run() { } } - private void afterSubchannelUpdate() { - //todo: may optimize to use previous enabled oob to skip + private void afterAcceptAddresses() { for (Subchannel subchannel : getSubchannels()) { WrrSubchannel weightedSubchannel = (WrrSubchannel) subchannel; weightedSubchannel.setConfig(config); @@ -149,7 +150,6 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { @VisibleForTesting static final class WrrSubchannel extends ForwardingSubchannel { private final Subchannel delegate; - private final TimeProvider timeProvider; private final OrcaOobReportListener oobListener = this::onLoadReport; private final OrcaPerRequestReportListener perRpcListener = this::onLoadReport; @@ -158,7 +158,7 @@ static final class WrrSubchannel extends ForwardingSubchannel { volatile double weight; private volatile WeightedRoundRobinLoadBalancerConfig config; - public WrrSubchannel(Subchannel delegate, TimeProvider timeProvider) { + WrrSubchannel(Subchannel delegate, TimeProvider timeProvider) { this.delegate = checkNotNull(delegate, "delegate"); this.timeProvider = checkNotNull(timeProvider, "timeProvider"); } @@ -435,7 +435,7 @@ static final class Builder { Long weightExpirationPeriodNanos = 180_000_000_000L; //3min Boolean enableOobLoadReport = false; Long oobReportingPeriodNanos = 10_000_000_000L; // 10s - Long weightUpdatePeriodNanos = 100_000_000L; // 1s + Long weightUpdatePeriodNanos = 1_000_000_000L; // 1s private Builder() { diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java index 880d270b0e6..f59f13221a8 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java @@ -16,6 +16,8 @@ package io.grpc.xds; +import com.google.common.annotations.VisibleForTesting; +import io.grpc.Internal; import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancerProvider; @@ -28,9 +30,11 @@ /** * Providers a {@link WeightedRoundRobinLoadBalancer}. * */ -final class WeightedRoundRobinLoadBalancerProvider extends LoadBalancerProvider { +@Internal +public final class WeightedRoundRobinLoadBalancerProvider extends LoadBalancerProvider { - private static final long MIN_WEIGHT_UPDATE_PERIOD_NANOS = 10_000_000L; // 100ms + @VisibleForTesting + static final long MIN_WEIGHT_UPDATE_PERIOD_NANOS = 100_000_000L; // 100ms @Override public LoadBalancer newLoadBalancer(Helper helper) { diff --git a/xds/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider b/xds/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider index 6b6e3a392a9..e1c4d4aa427 100644 --- a/xds/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider +++ b/xds/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider @@ -7,3 +7,4 @@ io.grpc.xds.ClusterImplLoadBalancerProvider io.grpc.xds.LeastRequestLoadBalancerProvider io.grpc.xds.RingHashLoadBalancerProvider io.grpc.xds.WrrLocalityLoadBalancerProvider +io.grpc.xds.WeightedRoundRobinLoadBalancerProvider diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProviderTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProviderTest.java new file mode 100644 index 00000000000..08945a6ce36 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProviderTest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2023 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 io.grpc.xds; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.grpc.InternalServiceProviders; +import io.grpc.LoadBalancer; +import io.grpc.LoadBalancerProvider; +import io.grpc.NameResolver.ConfigOrError; +import io.grpc.SynchronizationContext; +import io.grpc.internal.FakeClock; +import io.grpc.internal.JsonParser; +import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinLoadBalancerConfig; +import java.io.IOException; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link WeightedRoundRobinLoadBalancerProvider}. */ +@RunWith(JUnit4.class) +public class WeightedRoundRobinLoadBalancerProviderTest { + + private final WeightedRoundRobinLoadBalancerProvider provider = + new WeightedRoundRobinLoadBalancerProvider(); + + private final SynchronizationContext syncContext = new SynchronizationContext( + new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread t, Throwable e) { + throw new AssertionError(e); + } + }); + + @Test + public void provided() { + for (LoadBalancerProvider current : InternalServiceProviders.getCandidatesViaServiceLoader( + LoadBalancerProvider.class, getClass().getClassLoader())) { + if (current instanceof WeightedRoundRobinLoadBalancerProvider) { + return; + } + } + fail("WeightedRoundRobinLoadBalancerProvider not registered"); + } + + @Test + public void providesLoadBalancer() { + LoadBalancer.Helper helper = mock(LoadBalancer.Helper.class); + when(helper.getSynchronizationContext()).thenReturn(syncContext); + when(helper.getScheduledExecutorService()).thenReturn( + new FakeClock().getScheduledExecutorService()); + assertThat(provider.newLoadBalancer(helper)) + .isInstanceOf(WeightedRoundRobinLoadBalancer.class); + } + + @Test + public void parseLoadBalancingConfig() throws IOException { + String lbConfig = + "{\"blackoutPeriod\" : \"20s\"," + + " \"weightExpirationPeriod\" : \"300s\"," + + " \"oobReportingPeriod\" : \"100s\"," + + " \"enableOobLoadReport\" : true," + + " \"weightUpdatePeriod\" : \"2s\"" + + " }"; + + ConfigOrError configOrError = provider.parseLoadBalancingPolicyConfig( + parseJsonObject(lbConfig)); + assertThat(configOrError.getConfig()).isNotNull(); + WeightedRoundRobinLoadBalancerConfig config = + (WeightedRoundRobinLoadBalancerConfig) configOrError.getConfig(); + assertThat(config.blackoutPeriodNanos).isEqualTo(20_000_000_000L); + assertThat(config.weightExpirationPeriodNanos).isEqualTo(300_000_000_000L); + assertThat(config.enableOobLoadReport).isEqualTo(true); + assertThat(config.weightUpdatePeriodNanos).isEqualTo(2_000_000_000L); + } + + @Test + public void parseLoadBalancingConfigDefaultValues() throws IOException { + String lbConfig = "{\"weightUpdatePeriod\" : \"0.02s\"}"; + + ConfigOrError configOrError = provider.parseLoadBalancingPolicyConfig( + parseJsonObject(lbConfig)); + assertThat(configOrError.getConfig()).isNotNull(); + WeightedRoundRobinLoadBalancerConfig config = + (WeightedRoundRobinLoadBalancerConfig) configOrError.getConfig(); + assertThat(config.blackoutPeriodNanos).isEqualTo(10_000_000_000L); + assertThat(config.weightExpirationPeriodNanos).isEqualTo(180_000_000_000L); + assertThat(config.enableOobLoadReport).isEqualTo(false); + assertThat(config.weightUpdatePeriodNanos).isEqualTo(100_000_000L); + } + + @SuppressWarnings("unchecked") + private static Map parseJsonObject(String json) throws IOException { + return (Map) JsonParser.parse(json); + } +} diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index 4c26a283e63..237e47fa02a 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -31,6 +31,7 @@ import com.github.xds.service.orca.v3.OrcaLoadReportRequest; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.google.protobuf.Duration; import io.grpc.Attributes; import io.grpc.Channel; import io.grpc.ChannelLogger; @@ -53,12 +54,13 @@ import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinPicker; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WrrSubchannel; import java.net.SocketAddress; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Rule; @@ -94,8 +96,8 @@ public class WeightedRoundRobinLoadBalancerTest { private final Map subchannelStateListeners = Maps.newLinkedHashMap(); - private final List> oobCalls = - new ArrayList<>(); + private final Queue> oobCalls = + new ConcurrentLinkedQueue<>(); private WeightedRoundRobinLoadBalancer wrr; @@ -110,6 +112,7 @@ public class WeightedRoundRobinLoadBalancerTest { Attributes.newBuilder().set(MAJOR_KEY, "I got the keys").build(); private static final double EDF_PRECISE = 0.01; + private final SynchronizationContext syncContext = new SynchronizationContext( new Thread.UncaughtExceptionHandler() { @Override @@ -183,11 +186,15 @@ public void wrrLifeCycle() { Subchannel readySubchannel2 = it.next(); subchannelStateListeners.get(readySubchannel2).onSubchannelState(ConnectivityStateInfo .forNonError(ConnectivityState.READY)); + Subchannel connectingSubchannel = it.next(); + subchannelStateListeners.get(connectingSubchannel).onSubchannelState(ConnectivityStateInfo + .forNonError(ConnectivityState.CONNECTING)); verify(helper, times(2)).updateBalancingState( eq(ConnectivityState.READY), pickerCaptor.capture()); assertThat(pickerCaptor.getAllValues().size()).isEqualTo(2); assertThat(pickerCaptor.getAllValues().get(0).getList().size()).isEqualTo(1); WeightedRoundRobinPicker weightedPicker = pickerCaptor.getAllValues().get(1); + assertThat(weightedPicker.getList().size()).isEqualTo(2); WrrSubchannel weightedSubchannel1 = (WrrSubchannel) weightedPicker.getList().get(0); WrrSubchannel weightedSubchannel2 = (WrrSubchannel) weightedPicker.getList().get(1); weightedSubchannel1.onLoadReport(InternalCallMetricRecorder.createMetricReport( @@ -235,6 +242,7 @@ public void enableOobLoadReportConfig() { assertThat(pickResult.getStreamTracerFactory()).isNotNull(); assertThat(oobCalls.isEmpty()).isTrue(); weightedConfig = WeightedRoundRobinLoadBalancerConfig.newBuilder().setEnableOobLoadReport(true) + .setOobReportingPeriodNanos(20_030_000_000L) .build(); syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() .setAddresses(servers).setLoadBalancingPolicyConfig(weightedConfig) @@ -242,7 +250,11 @@ public void enableOobLoadReportConfig() { pickResult = weightedPicker.pickSubchannel(mockArgs); assertThat(pickResult.getSubchannel()).isEqualTo(weightedSubchannel1); assertThat(pickResult.getStreamTracerFactory()).isNull(); + OrcaLoadReportRequest golden = OrcaLoadReportRequest.newBuilder().setReportInterval( + Duration.newBuilder().setSeconds(20).setNanos(30000000).build()).build(); assertThat(oobCalls.size()).isEqualTo(2); + verify(oobCalls.poll()).sendMessage(eq(golden)); + verify(oobCalls.poll()).sendMessage(eq(golden)); } @Test From e6886c1c3c7d8d90dc9980338192fd68a705e00f Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Thu, 9 Feb 2023 15:09:12 -0800 Subject: [PATCH 16/25] fix current picker --- .../java/io/grpc/util/RoundRobinLoadBalancer.java | 2 +- .../io/grpc/xds/WeightedRoundRobinLoadBalancer.java | 12 +++++------- .../xds/WeightedRoundRobinLoadBalancerProvider.java | 4 ++-- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java index 3a835584f4a..83c7094c883 100644 --- a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java @@ -62,7 +62,7 @@ public class RoundRobinLoadBalancer extends LoadBalancer { new HashMap<>(); private final Random random; private ConnectivityState currentState; - private RoundRobinPicker currentPicker = new EmptyPicker(EMPTY_OK); + protected RoundRobinPicker currentPicker = new EmptyPicker(EMPTY_OK); public RoundRobinLoadBalancer(Helper helper) { this.helper = checkNotNull(helper, "helper"); diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 558bf6c3169..54418d281cd 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -25,7 +25,7 @@ import io.grpc.ConnectivityState; import io.grpc.ConnectivityStateInfo; import io.grpc.EquivalentAddressGroup; -import io.grpc.Internal; +import io.grpc.ExperimentalApi; import io.grpc.LoadBalancer; import io.grpc.NameResolver; import io.grpc.Status; @@ -52,13 +52,12 @@ * the {@link EquivalentAddressGroup}s from the {@link NameResolver}. The subchannel weights are * determined by backend metrics using ORCA. */ -@Internal +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/9885") public final class WeightedRoundRobinLoadBalancer extends RoundRobinLoadBalancer { private volatile WeightedRoundRobinLoadBalancerConfig config; private final SynchronizationContext syncContext; private final ScheduledExecutorService timeService; private ScheduledHandle weightUpdateTimer; - private WeightedRoundRobinPicker readyPicker; public WeightedRoundRobinLoadBalancer(Helper helper, TimeProvider timeProvider) { super(new WrrHelper(OrcaOobUtil.newOrcaReportingHelper(helper), @@ -86,8 +85,7 @@ public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { @Override public RoundRobinPicker createReadyPicker(List activeList, int startIndex) { - this.readyPicker = new WeightedRoundRobinPicker(activeList, startIndex); - return readyPicker; + return new WeightedRoundRobinPicker(activeList, startIndex); } private final class UpdateWeightTask implements Runnable { @@ -96,8 +94,8 @@ public void run() { if (weightUpdateTimer != null && weightUpdateTimer.isPending()) { return; } - if (readyPicker != null) { - readyPicker.updateWeight(); + if (currentPicker != null && currentPicker instanceof WeightedRoundRobinPicker) { + ((WeightedRoundRobinPicker)currentPicker).updateWeight(); } weightUpdateTimer = syncContext.schedule(new UpdateWeightTask(), config.weightUpdatePeriodNanos, TimeUnit.NANOSECONDS, timeService); diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java index f59f13221a8..5166094daf9 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java @@ -17,7 +17,7 @@ package io.grpc.xds; import com.google.common.annotations.VisibleForTesting; -import io.grpc.Internal; +import io.grpc.ExperimentalApi; import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancerProvider; @@ -30,7 +30,7 @@ /** * Providers a {@link WeightedRoundRobinLoadBalancer}. * */ -@Internal +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/9885") public final class WeightedRoundRobinLoadBalancerProvider extends LoadBalancerProvider { @VisibleForTesting From 3da127efb747a761c37db2c79ec8a11cfc41d577 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Fri, 10 Feb 2023 12:01:48 -0800 Subject: [PATCH 17/25] remove virtual time, change comment --- xds/BUILD.bazel | 1 + .../xds/WeightedRoundRobinLoadBalancer.java | 58 ++++++++----------- 2 files changed, 24 insertions(+), 35 deletions(-) diff --git a/xds/BUILD.bazel b/xds/BUILD.bazel index 8f9198953a0..f75b2bbeeaa 100644 --- a/xds/BUILD.bazel +++ b/xds/BUILD.bazel @@ -32,6 +32,7 @@ java_library( ":envoy_service_load_stats_v3_java_grpc", ":envoy_service_status_v3_java_grpc", ":xds_protos_java", + ":orca", "//:auto_value_annotations", "//alts", "//api", diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 54418d281cd..9157e8fd87b 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -151,9 +151,9 @@ static final class WrrSubchannel extends ForwardingSubchannel { private final TimeProvider timeProvider; private final OrcaOobReportListener oobListener = this::onLoadReport; private final OrcaPerRequestReportListener perRpcListener = this::onLoadReport; - volatile long lastUpdated; - volatile long nonEmptySince; - volatile double weight; + private volatile long lastUpdated; + private volatile long nonEmptySince; + private volatile double weight; private volatile WeightedRoundRobinLoadBalancerConfig config; WrrSubchannel(Subchannel delegate, TimeProvider timeProvider) { @@ -294,29 +294,27 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { * The earliest deadline first implementation in which each object is * chosen deterministically and periodically with frequency proportional to its weight. * - *

Specifically, each object added to chooser is given a period equal to the multiplicative - * inverse of its weight. The place of each object in its period is tracked, and each call to - * choose returns the object with the least remaining time in its period (1/weight). + *

Specifically, each object added to chooser is given a deadline equal to the multiplicative + * inverse of its weight. The place of each object in its deadline is tracked, and each call to + * choose returns the object with the least remaining time in its deadline (1/weight). * (Ties are broken by the order in which the children were added to the chooser.) - * For example, if items A and B are added - * with weights 0.5 and 0.2, successive chooses return: + * For example, if items A and B are added with weights 0.5 and 0.2, successive chooses return: * *

    - *
  • In the first call, the remaining periods are A=2 (1/0.5) and B=5 (1/0.2), so A is - * returned. The period of A (as it was picked), is substracted from periods of all other - * objects. - *
  • Next, the remaining periods are A=2 and B=3, so A is returned. The period of A (2) is - * substracted from all other objects (B=1) and A is re-added with A=2. - *
  • Remaining periods are A=2 and B=1, so B is returned. The period of B (1) is substracted - * from all other objects (A=1) and B is re-added with B=5. - *
  • Remaining periods are A=1 and B=5, so A is returned. The period of A (1) is substracted - * from all other objects (B=4) and A is re-added with A=2. - *
  • Remaining periods are A=2 and B=4, so A is returned. The period of A (2) is substracted - * from all other objects (B=2) and A is re-added with A=2. - *
  • Remaining periods are A=2 and B=2, so A is returned. The period of A (2) is substracted - * from all other objects (B=0) and A is re-added with A=2. - *
  • Remaining periods are A=2 and B=0, so B is returned. The period of B (0) is substracted - * from all other objects (A=2) and B is re-added with B=5. + *
  • In the first call, the deadlines are A=2 (1/0.5) and B=5 (1/0.2), so A is returned. + * The deadline of A is updated to 4. + *
  • Next, the remaining deadlines are A=4 and B=5, so A is returned. The deadline of A (2) is + * updated to A=6. + *
  • Remaining deadlines are A=6 and B=5, so B is returned. The deadline of B is updated with + * with B=10. + *
  • Remaining deadlines are A=6 and B=10, so A is returned. The deadline of A is updated with + * A=8. + *
  • Remaining deadlines are A=8 and B=10, so A is returned. The deadline of A is updated with + * A=10. + *
  • Remaining deadlines are A=10 and B=10, so A is returned. The deadline of A is updated + * with A=12. + *
  • Remaining deadlines are A=12 and B=10, so B is returned. The deadline of B is updated + * with B=15. *
  • etc. *
* @@ -333,13 +331,6 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { private static final class EdfScheduler { private final PriorityQueue prioQueue; - /** - * Upon every pick() the "virtual time" is advanced closer to the period of next items. - * Here we have an explicit "virtualTimeNow", which will be added to the period of all newly - * scheduled objects (virtualTimeNow + period). - */ - private double virtualTimeNow = 0.0; - /** * Weights below this value will be logged and upped to this minimum weight. */ @@ -372,7 +363,7 @@ private static final class EdfScheduler { void add(int index, double weight) { checkArgument(weight > 0.0, "Weights need to be positive."); ObjectState state = new ObjectState(Math.max(weight, MINIMUM_WEIGHT), index); - state.deadline = virtualTimeNow + 1 / state.weight; + state.deadline = 1 / state.weight; prioQueue.add(state); } @@ -382,10 +373,7 @@ void add(int index, double weight) { int pick() { synchronized (lock) { ObjectState minObject = prioQueue.remove(); - // Simulate advancing in time by setting the current time to the period of the nearest item - // on the "time horizon". - virtualTimeNow = minObject.deadline; - minObject.deadline = virtualTimeNow + (1.0 / minObject.weight); + minObject.deadline += 1.0 / minObject.weight; prioQueue.add(minObject); return minObject.index; } From cb31730a629394b1a682a630e052f5187d7e0672 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Mon, 13 Feb 2023 10:33:01 -0800 Subject: [PATCH 18/25] fix avg weight --- .../xds/WeightedRoundRobinLoadBalancer.java | 20 ++- .../WeightedRoundRobinLoadBalancerTest.java | 165 ++++++++++++++++-- 2 files changed, 160 insertions(+), 25 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 9157e8fd87b..039d88a9857 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -217,13 +217,13 @@ protected Subchannel delegate() { final class WeightedRoundRobinPicker extends ReadyPicker { private final List list; private final AtomicReference schedulerRef; - private volatile boolean rrMode = false; + private volatile boolean rrMode = true; WeightedRoundRobinPicker(List list, int startIndex) { - super(list, startIndex); + super(checkNotNull(list, "list"), startIndex); Preconditions.checkArgument(!list.isEmpty(), "empty list"); this.list = list; - this.schedulerRef = new AtomicReference<>(new EdfScheduler()); + this.schedulerRef = new AtomicReference<>(new EdfScheduler(list.size())); updateWeight(); } @@ -245,7 +245,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { } private void updateWeight() { - EdfScheduler scheduler = new EdfScheduler(); + EdfScheduler scheduler = new EdfScheduler(list.size()); int weightedChannelCount = 0; double avgWeight = 0; for (Subchannel value : list) { @@ -255,22 +255,24 @@ private void updateWeight() { weightedChannelCount++; } } - rrMode = weightedChannelCount < 2; - if (rrMode) { + if (weightedChannelCount < 2) { + rrMode = true; return; } + avgWeight /= 1.0 * weightedChannelCount; for (int i = 0; i < list.size(); i++) { WrrSubchannel subchannel = (WrrSubchannel) list.get(i); double newWeight = subchannel.getWeight(); scheduler.add(i, newWeight > 0 ? newWeight : avgWeight); } schedulerRef.set(scheduler); + rrMode = false; } @Override public String toString() { return MoreObjects.toStringHelper(WeightedRoundRobinPicker.class) - .add("list", list).toString(); + .add("list", list).add("rrMode", rrMode).toString(); } @VisibleForTesting @@ -342,8 +344,8 @@ private static final class EdfScheduler { * Use the item's deadline as the order in the priority queue. If the deadlines are the same, * use the index. Index should be unique. */ - EdfScheduler() { - this.prioQueue = new PriorityQueue(10, (o1, o2) -> { + EdfScheduler(int initialCapacity) { + this.prioQueue = new PriorityQueue(initialCapacity, (o1, o2) -> { if (o1.deadline == o2.deadline) { return o1.index - o2.index; } else if (o1.deadline < o2.deadline) { diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index 237e47fa02a..6d8417d4163 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -49,6 +49,7 @@ import io.grpc.SynchronizationContext; import io.grpc.internal.FakeClock; import io.grpc.services.InternalCallMetricRecorder; +import io.grpc.services.MetricReport; import io.grpc.util.RoundRobinLoadBalancer.EmptyPicker; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinLoadBalancerConfig; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinPicker; @@ -61,6 +62,7 @@ import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Rule; @@ -239,7 +241,7 @@ public void enableOobLoadReportConfig() { assertThat(fakeClock.forwardTime(11, TimeUnit.SECONDS)).isEqualTo(1); PickResult pickResult = weightedPicker.pickSubchannel(mockArgs); assertThat(pickResult.getSubchannel()).isEqualTo(weightedSubchannel1); - assertThat(pickResult.getStreamTracerFactory()).isNotNull(); + assertThat(pickResult.getStreamTracerFactory()).isNotNull(); // verify per-request listener assertThat(oobCalls.isEmpty()).isTrue(); weightedConfig = WeightedRoundRobinLoadBalancerConfig.newBuilder().setEnableOobLoadReport(true) .setOobReportingPeriodNanos(20_030_000_000L) @@ -257,8 +259,8 @@ public void enableOobLoadReportConfig() { verify(oobCalls.poll()).sendMessage(eq(golden)); } - @Test - public void pickByWeight() { + private void pickByWeight(MetricReport r1, MetricReport r2, MetricReport r3, + double p1, double p2, double p3) { syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() .setAddresses(servers).setLoadBalancingPolicyConfig(weightedConfig) .setAttributes(affinity).build())); @@ -282,25 +284,47 @@ public void pickByWeight() { WrrSubchannel weightedSubchannel1 = (WrrSubchannel) weightedPicker.getList().get(0); WrrSubchannel weightedSubchannel2 = (WrrSubchannel) weightedPicker.getList().get(1); WrrSubchannel weightedSubchannel3 = (WrrSubchannel) weightedPicker.getList().get(2); - weightedSubchannel1.onLoadReport(InternalCallMetricRecorder.createMetricReport( - 0.12, 0.1, 22, new HashMap<>(), new HashMap<>())); - weightedSubchannel2.onLoadReport(InternalCallMetricRecorder.createMetricReport( - 0.28, 0.1, 40, new HashMap<>(), new HashMap<>())); - weightedSubchannel3.onLoadReport(InternalCallMetricRecorder.createMetricReport( - 0.86, 0.1, 100, new HashMap<>(), new HashMap<>())); + weightedSubchannel1.onLoadReport(r1); + weightedSubchannel2.onLoadReport(r2); + weightedSubchannel3.onLoadReport(r3); assertThat(fakeClock.forwardTime(11, TimeUnit.SECONDS)).isEqualTo(1); Map pickCount = new HashMap<>(); - for (int i = 0; i < 1000; i++) { + for (int i = 0; i < 10000; i++) { Subchannel result = weightedPicker.pickSubchannel(mockArgs).getSubchannel(); pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); } assertThat(pickCount.size()).isEqualTo(3); - assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - - 22 / 0.12 / (22 / 0.12 + 40 / 0.28 + 100 / 0.86))).isAtMost(EDF_PRECISE); - assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - - 40 / 0.28 / (22 / 0.12 + 40 / 0.28 + 100 / 0.86) )).isAtMost(EDF_PRECISE); - assertThat(Math.abs(pickCount.get(weightedSubchannel3) / 1000.0 - - 100 / 0.86 / ( 22 / 0.12 + 40 / 0.28 + 100 / 0.86) )).isAtMost(EDF_PRECISE); + assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 10000.0 - p1)).isAtMost(EDF_PRECISE); + assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 10000.0 - p2 )).isAtMost(EDF_PRECISE); + assertThat(Math.abs(pickCount.get(weightedSubchannel3) / 10000.0 - p3 )).isAtMost(EDF_PRECISE); + } + + @Test + public void pickByWeight_LargeWeight() { + pickByWeight(InternalCallMetricRecorder.createMetricReport( + 0.1, 0.1, 2200, new HashMap<>(), new HashMap<>()), + InternalCallMetricRecorder.createMetricReport( + 0.9, 0.1, 2, new HashMap<>(), new HashMap<>()), + InternalCallMetricRecorder.createMetricReport( + 0.86, 0.1, 100, new HashMap<>(), new HashMap<>()), + 2200 / 0.1 / (2200 / 0.1 + 2 / 0.9 + 100 / 0.86), + 27 / 0.9 / (2200 / 0.1 + 2 / 0.9 + 100 / 0.86), + 100 / 0.86 / ( 2200 / 0.1 + 2 / 0.9 + 100 / 0.86) + ); + } + + @Test + public void pickByWeight_normalWeight() { + pickByWeight(InternalCallMetricRecorder.createMetricReport( + 0.12, 0.1, 22, new HashMap<>(), new HashMap<>()), + InternalCallMetricRecorder.createMetricReport( + 0.28, 0.1, 40, new HashMap<>(), new HashMap<>()), + InternalCallMetricRecorder.createMetricReport( + 0.86, 0.1, 100, new HashMap<>(), new HashMap<>()), + 22 / 0.12 / (22 / 0.12 + 40 / 0.28 + 100 / 0.86), + 40 / 0.28 / (22 / 0.12 + 40 / 0.28 + 100 / 0.86), + 100 / 0.86 / ( 22 / 0.12 + 40 / 0.28 + 100 / 0.86) + ); } @Test @@ -423,6 +447,115 @@ public void weightExpired() { .isAtMost(EDF_PRECISE); } + @Test + public void unknownWeightIsAvgWeight() { + syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() + .setAddresses(servers).setLoadBalancingPolicyConfig(weightedConfig) + .setAttributes(affinity).build())); + verify(helper, times(3)).createSubchannel( + any(CreateSubchannelArgs.class)); + assertThat(fakeClock.getPendingTasks().size()).isEqualTo(1); + + Iterator it = subchannels.values().iterator(); + Subchannel readySubchannel1 = it.next(); + subchannelStateListeners.get(readySubchannel1).onSubchannelState(ConnectivityStateInfo + .forNonError(ConnectivityState.READY)); + Subchannel readySubchannel2 = it.next(); + subchannelStateListeners.get(readySubchannel2).onSubchannelState(ConnectivityStateInfo + .forNonError(ConnectivityState.READY)); + Subchannel readySubchannel3 = it.next(); + subchannelStateListeners.get(readySubchannel3).onSubchannelState(ConnectivityStateInfo + .forNonError(ConnectivityState.READY)); + verify(helper, times(3)).updateBalancingState( + eq(ConnectivityState.READY), pickerCaptor.capture()); + WeightedRoundRobinPicker weightedPicker = pickerCaptor.getAllValues().get(2); + WrrSubchannel weightedSubchannel1 = (WrrSubchannel) weightedPicker.getList().get(0); + WrrSubchannel weightedSubchannel2 = (WrrSubchannel) weightedPicker.getList().get(1); + WrrSubchannel weightedSubchannel3 = (WrrSubchannel) weightedPicker.getList().get(2); + weightedSubchannel1.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.1, 0.1, 1, new HashMap<>(), new HashMap<>())); + weightedSubchannel2.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.2, 0.1, 1, new HashMap<>(), new HashMap<>())); + assertThat(fakeClock.forwardTime(10, TimeUnit.SECONDS)).isEqualTo(1); + Map pickCount = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + Subchannel result = weightedPicker.pickSubchannel(mockArgs).getSubchannel(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + } + assertThat(pickCount.size()).isEqualTo(3); + assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 4.0 / 9)) + .isAtMost(EDF_PRECISE); + assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 2.0 / 9)) + .isAtMost(EDF_PRECISE); + // subchannel3's weight is average of subchannel1 and subchannel2 + assertThat(Math.abs(pickCount.get(weightedSubchannel3) / 1000.0 - 3.0 / 9)) + .isAtMost(EDF_PRECISE); + } + + @Test + public void pickFromOtherThread() throws Exception { + syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() + .setAddresses(servers).setLoadBalancingPolicyConfig(weightedConfig) + .setAttributes(affinity).build())); + verify(helper, times(3)).createSubchannel( + any(CreateSubchannelArgs.class)); + assertThat(fakeClock.getPendingTasks().size()).isEqualTo(1); + + Iterator it = subchannels.values().iterator(); + Subchannel readySubchannel1 = it.next(); + subchannelStateListeners.get(readySubchannel1).onSubchannelState(ConnectivityStateInfo + .forNonError(ConnectivityState.READY)); + Subchannel readySubchannel2 = it.next(); + subchannelStateListeners.get(readySubchannel2).onSubchannelState(ConnectivityStateInfo + .forNonError(ConnectivityState.READY)); + verify(helper, times(2)).updateBalancingState( + eq(ConnectivityState.READY), pickerCaptor.capture()); + WeightedRoundRobinPicker weightedPicker = pickerCaptor.getAllValues().get(1); + WrrSubchannel weightedSubchannel1 = (WrrSubchannel) weightedPicker.getList().get(0); + WrrSubchannel weightedSubchannel2 = (WrrSubchannel) weightedPicker.getList().get(1); + weightedSubchannel1.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.1, 0.1, 1, new HashMap<>(), new HashMap<>())); + weightedSubchannel2.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.2, 0.1, 1, new HashMap<>(), new HashMap<>())); + assertThat(weightedPicker.toString()).contains("rrMode=true"); + CyclicBarrier barrier = new CyclicBarrier(2); + new Thread(new Runnable() { + @Override + public void run() { + try { + weightedPicker.pickSubchannel(mockArgs); + barrier.await(); + Map pickCount = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + Subchannel result = weightedPicker.pickSubchannel(mockArgs).getSubchannel(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + } + assertThat(pickCount.size()).isEqualTo(2); + // after blackout period + assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 2.0 / 3)) + .isAtMost(EDF_PRECISE); + assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 1.0 / 3)) + .isAtMost(EDF_PRECISE); + } catch (Exception ex) { + throw new AssertionError(ex); + } + } + }).start(); + assertThat(fakeClock.forwardTime(10, TimeUnit.SECONDS)).isEqualTo(1); + barrier.await(); + Map pickCount = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + Subchannel result = weightedPicker.pickSubchannel(mockArgs).getSubchannel(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + } + assertThat(pickCount.size()).isEqualTo(2); + // after blackout period + assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 2.0 / 3)) + .isAtMost(EDF_PRECISE); + assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 1.0 / 3)) + .isAtMost(EDF_PRECISE); + } + private static class FakeSocketAddress extends SocketAddress { final String name; From 6dfa072ef8f7c096e18978f07a91e3e69f18f5f4 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Thu, 16 Feb 2023 15:28:21 -0800 Subject: [PATCH 19/25] add env variable --- .../xds/WeightedRoundRobinLoadBalancer.java | 5 ++- ...eightedRoundRobinLoadBalancerProvider.java | 4 +- .../java/io/grpc/xds/XdsClusterResource.java | 5 +++ .../java/io/grpc/xds/XdsResourceType.java | 7 ++++ .../io/grpc/xds/XdsClientImplDataTest.java | 37 +++++++++++++++++++ 5 files changed, 55 insertions(+), 3 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 039d88a9857..e489c676549 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -200,7 +200,8 @@ private double getWeight() { if (now - lastUpdated >= config.weightExpirationPeriodNanos) { nonEmptySince = Integer.MAX_VALUE; return 0; - } else if (now - nonEmptySince < config.blackoutPeriodNanos) { + } else if (now - nonEmptySince < config.blackoutPeriodNanos && + config.blackoutPeriodNanos > 0) { return 0; } else { return weight; @@ -387,7 +388,7 @@ int pick() { static class ObjectState { private final double weight; private final int index; - volatile double deadline; + private volatile double deadline; ObjectState(double weight, int index) { this.weight = weight; diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java index 5166094daf9..7fcd30c75f9 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java @@ -36,6 +36,8 @@ public final class WeightedRoundRobinLoadBalancerProvider extends LoadBalancerPr @VisibleForTesting static final long MIN_WEIGHT_UPDATE_PERIOD_NANOS = 100_000_000L; // 100ms + static final String SCHEME = "weighted_round_robin_experimental"; + @Override public LoadBalancer newLoadBalancer(Helper helper) { return new WeightedRoundRobinLoadBalancer(helper, TimeProvider.SYSTEM_TIME_PROVIDER); @@ -53,7 +55,7 @@ public int getPriority() { @Override public String getPolicyName() { - return "weighted_round_robin_experimental"; + return SCHEME; } @Override diff --git a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java index 33f6176474b..812851e1412 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java @@ -137,6 +137,11 @@ static CdsUpdate processCluster(Cluster cluster, // Validate the LB config by trying to parse it with the corresponding LB provider. LbConfig lbConfig = ServiceConfigUtil.unwrapLoadBalancingConfig(lbPolicyConfig); + if (lbConfig.getPolicyName().equals(WeightedRoundRobinLoadBalancerProvider.SCHEME) + && !enableWrr) { + throw new ResourceInvalidException("Cluster " + cluster.getName() + " specified LB policy " + + WeightedRoundRobinLoadBalancerProvider.SCHEME + " is not supported"); + } NameResolver.ConfigOrError configOrError = loadBalancerRegistry.getProvider( lbConfig.getPolicyName()).parseLoadBalancingPolicyConfig( lbConfig.getRawConfigValue()); diff --git a/xds/src/main/java/io/grpc/xds/XdsResourceType.java b/xds/src/main/java/io/grpc/xds/XdsResourceType.java index 1302f5a59e1..b1b2ee04054 100644 --- a/xds/src/main/java/io/grpc/xds/XdsResourceType.java +++ b/xds/src/main/java/io/grpc/xds/XdsResourceType.java @@ -59,6 +59,10 @@ abstract class XdsResourceType { !Strings.isNullOrEmpty(System.getenv("GRPC_EXPERIMENTAL_ENABLE_LEAST_REQUEST")) ? Boolean.parseBoolean(System.getenv("GRPC_EXPERIMENTAL_ENABLE_LEAST_REQUEST")) : Boolean.parseBoolean(System.getProperty("io.grpc.xds.experimentalEnableLeastRequest")); + + @VisibleForTesting + static boolean enableWrr = getFlag("GRPC_EXPERIMENTAL_XDS_WRR_LB", false); + @VisibleForTesting static boolean enableCustomLbConfig = getFlag("GRPC_EXPERIMENTAL_XDS_CUSTOM_LB_CONFIG", true); @VisibleForTesting @@ -71,6 +75,9 @@ abstract class XdsResourceType { static final String TYPE_URL_TYPED_STRUCT = "type.googleapis.com/xds.type.v3.TypedStruct"; + static final String TYPE_URL_CLIENT_SIDE_WRR = "type.googleapis.com/envoy.extensions.load_balancing_policies.client_" + + "side_weighted_round_robin.v3.ClientSideWeightedRoundRobin"; + @Nullable abstract String extractResourceName(Message unpackedResource); diff --git a/xds/src/test/java/io/grpc/xds/XdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/XdsClientImplDataTest.java index 051d890aea4..d3c15d1616c 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientImplDataTest.java @@ -39,6 +39,7 @@ import io.envoyproxy.envoy.config.cluster.v3.Cluster.DiscoveryType; import io.envoyproxy.envoy.config.cluster.v3.Cluster.EdsClusterConfig; import io.envoyproxy.envoy.config.cluster.v3.Cluster.LbPolicy; +import io.envoyproxy.envoy.config.cluster.v3.LoadBalancingPolicy; import io.envoyproxy.envoy.config.core.v3.Address; import io.envoyproxy.envoy.config.core.v3.AggregatedConfigSource; import io.envoyproxy.envoy.config.core.v3.CidrRange; @@ -164,6 +165,8 @@ public class XdsClientImplDataTest { private boolean originalEnableRouteLookup; private boolean originalEnableLeastRequest; + private boolean originalEnableWrr; + @Before public void setUp() { originalEnableRetry = XdsResourceType.enableRetry; @@ -174,6 +177,8 @@ public void setUp() { assertThat(originalEnableRouteLookup).isFalse(); originalEnableLeastRequest = XdsResourceType.enableLeastRequest; assertThat(originalEnableLeastRequest).isFalse(); + originalEnableWrr = XdsResourceType.enableWrr; + assertThat(originalEnableWrr).isFalse(); } @After @@ -182,6 +187,7 @@ public void tearDown() { XdsResourceType.enableRbac = originalEnableRbac; XdsResourceType.enableRouteLookup = originalEnableRouteLookup; XdsResourceType.enableLeastRequest = originalEnableLeastRequest; + XdsResourceType.enableWrr = originalEnableWrr; } @Test @@ -1965,6 +1971,37 @@ public void parseCluster_leastRequestLbPolicy_defaultLbConfig() throws ResourceI assertThat(childConfigs.get(0).getPolicyName()).isEqualTo("least_request_experimental"); } + @Test + public void parseCluster_WrrLbPolicy_defaultLbConfig() throws ResourceInvalidException { + XdsResourceType.enableWrr = true; + Cluster cluster = Cluster.newBuilder() + .setName("cluster-foo.googleapis.com") + .setType(DiscoveryType.EDS) + .setEdsClusterConfig( + EdsClusterConfig.newBuilder() + .setEdsConfig( + ConfigSource.newBuilder() + .setAds(AggregatedConfigSource.getDefaultInstance())) + .setServiceName("service-foo.googleapis.com")) + .setLoadBalancingPolicy(LoadBalancingPolicy.newBuilder().addPolicies( + LoadBalancingPolicy.Policy.newBuilder().mergeTypedExtensionConfig( + TypedExtensionConfig.newBuilder().mergeTypedConfig(Any.pack( + TypedExtensionConfig.newBuilder().setName( + WeightedRoundRobinLoadBalancerProvider.SCHEME).setTypedConfig( + Any.pack()).build()) + ).build()).build()).build()) + .build(); + + CdsUpdate update = XdsClusterResource.processCluster( + cluster, null, LRS_SERVER_INFO, + LoadBalancerRegistry.getDefaultRegistry()); + LbConfig lbConfig = ServiceConfigUtil.unwrapLoadBalancingConfig(update.lbPolicyConfig()); + assertThat(lbConfig.getPolicyName()).isEqualTo("wrr_locality_experimental"); + List childConfigs = ServiceConfigUtil.unwrapLoadBalancingConfigList( + JsonUtil.getListOfObjects(lbConfig.getRawConfigValue(), "childPolicy")); + assertThat(childConfigs.get(0).getPolicyName()).isEqualTo("weighted_round_robin_experimental"); + } + @Test public void parseCluster_transportSocketMatches_exception() throws ResourceInvalidException { Cluster cluster = Cluster.newBuilder() From 0bdce3247591e2fdfe5ea81471931fa5fb23aba5 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Fri, 17 Feb 2023 14:30:36 -0800 Subject: [PATCH 20/25] parse wrr proto --- .../grpc/xds/LoadBalancerConfigFactory.java | 70 +++++++++++++++-- .../xds/WeightedRoundRobinLoadBalancer.java | 4 +- ...eightedRoundRobinLoadBalancerProvider.java | 8 +- .../java/io/grpc/xds/XdsClusterResource.java | 7 +- .../java/io/grpc/xds/XdsResourceType.java | 3 - .../xds/LoadBalancerConfigFactoryTest.java | 77 ++++++++++++++----- ...tedRoundRobinLoadBalancerProviderTest.java | 11 +-- .../io/grpc/xds/XdsClientImplDataTest.java | 49 ++++++++---- 8 files changed, 169 insertions(+), 60 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/LoadBalancerConfigFactory.java b/xds/src/main/java/io/grpc/xds/LoadBalancerConfigFactory.java index ce3e95f03d1..93fbfacbed4 100644 --- a/xds/src/main/java/io/grpc/xds/LoadBalancerConfigFactory.java +++ b/xds/src/main/java/io/grpc/xds/LoadBalancerConfigFactory.java @@ -22,12 +22,14 @@ import com.google.protobuf.Any; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Struct; +import com.google.protobuf.util.Durations; import com.google.protobuf.util.JsonFormat; import io.envoyproxy.envoy.config.cluster.v3.Cluster; import io.envoyproxy.envoy.config.cluster.v3.Cluster.LeastRequestLbConfig; import io.envoyproxy.envoy.config.cluster.v3.Cluster.RingHashLbConfig; import io.envoyproxy.envoy.config.cluster.v3.LoadBalancingPolicy; import io.envoyproxy.envoy.config.cluster.v3.LoadBalancingPolicy.Policy; +import io.envoyproxy.envoy.extensions.load_balancing_policies.client_side_weighted_round_robin.v3.ClientSideWeightedRoundRobin; import io.envoyproxy.envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest; import io.envoyproxy.envoy.extensions.load_balancing_policies.ring_hash.v3.RingHash; import io.envoyproxy.envoy.extensions.load_balancing_policies.round_robin.v3.RoundRobin; @@ -73,6 +75,16 @@ class LoadBalancerConfigFactory { static final String WRR_LOCALITY_FIELD_NAME = "wrr_locality_experimental"; static final String CHILD_POLICY_FIELD = "childPolicy"; + static final String BLACK_OUT_PERIOD = "blackoutPeriod"; + + static final String WEIGHT_EXPIRATION_PERIOD = "weightExpirationPeriod"; + + static final String OOB_REPORTING_PERIOD = "oobReportingPeriod"; + + static final String ENABLE_OOB_LOAD_REPORT = "enableOobLoadReport"; + + static final String WEIGHT_UPDATE_PERIOD = "weightUpdatePeriod"; + /** * Factory method for creating a new {link LoadBalancerConfigConverter} for a given xDS {@link * Cluster}. @@ -80,14 +92,14 @@ class LoadBalancerConfigFactory { * @throws ResourceInvalidException If the {@link Cluster} has an invalid LB configuration. */ static ImmutableMap newConfig(Cluster cluster, boolean enableLeastRequest, - boolean enableCustomLbConfig) + boolean enableCustomLbConfig, boolean enableWrr) throws ResourceInvalidException { // The new load_balancing_policy will always be used if it is set, but for backward // compatibility we will fall back to using the old lb_policy field if the new field is not set. if (cluster.hasLoadBalancingPolicy() && enableCustomLbConfig) { try { return LoadBalancingPolicyConverter.convertToServiceConfig(cluster.getLoadBalancingPolicy(), - 0); + 0, enableWrr); } catch (MaxRecursionReachedException e) { throw new ResourceInvalidException("Maximum LB config recursion depth reached", e); } @@ -111,6 +123,35 @@ class LoadBalancerConfigFactory { return ImmutableMap.of(RING_HASH_FIELD_NAME, configBuilder.buildOrThrow()); } + /** + * Builds a service config JSON object for the weighted_round_robin load balancer config based on + * the given config values. + */ + private static ImmutableMap buildWrrConfig(Long blackoutPeriod, + Long weightExpirationPeriod, + Long oobReportingPeriod, + Boolean enableOobLoadReport, + Long weightUpdatePeriod) { + ImmutableMap.Builder configBuilder = ImmutableMap.builder(); + if (blackoutPeriod != null) { + configBuilder.put(BLACK_OUT_PERIOD, blackoutPeriod.doubleValue()); + } + if (weightExpirationPeriod != null) { + configBuilder.put(WEIGHT_EXPIRATION_PERIOD, weightExpirationPeriod.doubleValue()); + } + if (oobReportingPeriod != null) { + configBuilder.put(OOB_REPORTING_PERIOD, oobReportingPeriod.doubleValue()); + } + if (enableOobLoadReport != null) { + configBuilder.put(ENABLE_OOB_LOAD_REPORT, enableOobLoadReport); + } + if (weightUpdatePeriod != null) { + configBuilder.put(WEIGHT_UPDATE_PERIOD, weightUpdatePeriod.doubleValue()); + } + return ImmutableMap.of(WeightedRoundRobinLoadBalancerProvider.SCHEME, + configBuilder.buildOrThrow()); + } + /** * Builds a service config JSON object for the least_request load balancer config based on the * given config values.. @@ -151,7 +192,7 @@ static class LoadBalancingPolicyConverter { * Converts a {@link LoadBalancingPolicy} object to a service config JSON object. */ private static ImmutableMap convertToServiceConfig( - LoadBalancingPolicy loadBalancingPolicy, int recursionDepth) + LoadBalancingPolicy loadBalancingPolicy, int recursionDepth, boolean enableWrr) throws ResourceInvalidException, MaxRecursionReachedException { if (recursionDepth > MAX_RECURSION) { throw new MaxRecursionReachedException(); @@ -165,11 +206,16 @@ static class LoadBalancingPolicyConverter { serviceConfig = convertRingHashConfig(typedConfig.unpack(RingHash.class)); } else if (typedConfig.is(WrrLocality.class)) { serviceConfig = convertWrrLocalityConfig(typedConfig.unpack(WrrLocality.class), - recursionDepth); + recursionDepth, enableWrr); } else if (typedConfig.is(RoundRobin.class)) { serviceConfig = convertRoundRobinConfig(); } else if (typedConfig.is(LeastRequest.class)) { serviceConfig = convertLeastRequestConfig(typedConfig.unpack(LeastRequest.class)); + } else if (typedConfig.is(ClientSideWeightedRoundRobin.class)) { + if (enableWrr) { + serviceConfig = convertWeightedRoundRobinConfig( + typedConfig.unpack(ClientSideWeightedRoundRobin.class)); + } } else if (typedConfig.is(com.github.xds.type.v3.TypedStruct.class)) { serviceConfig = convertCustomConfig( typedConfig.unpack(com.github.xds.type.v3.TypedStruct.class)); @@ -217,14 +263,26 @@ static class LoadBalancingPolicyConverter { ringHash.hasMaximumRingSize() ? ringHash.getMaximumRingSize().getValue() : null); } + private static ImmutableMap convertWeightedRoundRobinConfig( + ClientSideWeightedRoundRobin wrr) throws ResourceInvalidException { + return buildWrrConfig( + wrr.hasBlackoutPeriod() ? Durations.toNanos(wrr.getBlackoutPeriod()) : null, + wrr.hasWeightExpirationPeriod() + ? Durations.toNanos(wrr.getWeightExpirationPeriod()) : null, + wrr.hasOobReportingPeriod() ? Durations.toNanos(wrr.getOobReportingPeriod()) : null, + wrr.hasEnableOobLoadReport() ? wrr.getEnableOobLoadReport().getValue() : null, + wrr.hasWeightUpdatePeriod() ? Durations.toNanos(wrr.getWeightUpdatePeriod()) : null); + } + /** * Converts a wrr_locality {@link Any} configuration to service config format. */ private static ImmutableMap convertWrrLocalityConfig(WrrLocality wrrLocality, - int recursionDepth) throws ResourceInvalidException, + int recursionDepth, boolean enableWrr) throws ResourceInvalidException, MaxRecursionReachedException { return buildWrrLocalityConfig( - convertToServiceConfig(wrrLocality.getEndpointPickingPolicy(), recursionDepth + 1)); + convertToServiceConfig(wrrLocality.getEndpointPickingPolicy(), + recursionDepth + 1, enableWrr)); } /** diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index e489c676549..2019e3372df 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -200,8 +200,8 @@ private double getWeight() { if (now - lastUpdated >= config.weightExpirationPeriodNanos) { nonEmptySince = Integer.MAX_VALUE; return 0; - } else if (now - nonEmptySince < config.blackoutPeriodNanos && - config.blackoutPeriodNanos > 0) { + } else if (now - nonEmptySince < config.blackoutPeriodNanos + && config.blackoutPeriodNanos > 0) { return 0; } else { return weight; diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java index 7fcd30c75f9..2b7aaabaa77 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java @@ -60,12 +60,12 @@ public String getPolicyName() { @Override public ConfigOrError parseLoadBalancingPolicyConfig(Map rawConfig) { - Long blackoutPeriodNanos = JsonUtil.getStringAsDuration(rawConfig, "blackoutPeriod"); + Long blackoutPeriodNanos = JsonUtil.getNumberAsLong(rawConfig, "blackoutPeriod"); Long weightExpirationPeriodNanos = - JsonUtil.getStringAsDuration(rawConfig, "weightExpirationPeriod"); - Long oobReportingPeriodNanos = JsonUtil.getStringAsDuration(rawConfig, "oobReportingPeriod"); + JsonUtil.getNumberAsLong(rawConfig, "weightExpirationPeriod"); + Long oobReportingPeriodNanos = JsonUtil.getNumberAsLong(rawConfig, "oobReportingPeriod"); Boolean enableOobLoadReport = JsonUtil.getBoolean(rawConfig, "enableOobLoadReport"); - Long weightUpdatePeriodNanos = JsonUtil.getStringAsDuration(rawConfig, "weightUpdatePeriod"); + Long weightUpdatePeriodNanos = JsonUtil.getNumberAsLong(rawConfig, "weightUpdatePeriod"); WeightedRoundRobinLoadBalancerConfig.Builder configBuilder = WeightedRoundRobinLoadBalancerConfig.newBuilder(); diff --git a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java index 812851e1412..1dc59feb8b5 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java @@ -133,15 +133,10 @@ static CdsUpdate processCluster(Cluster cluster, CdsUpdate.Builder updateBuilder = structOrError.getStruct(); ImmutableMap lbPolicyConfig = LoadBalancerConfigFactory.newConfig(cluster, - enableLeastRequest, enableCustomLbConfig); + enableLeastRequest, enableCustomLbConfig, enableWrr); // Validate the LB config by trying to parse it with the corresponding LB provider. LbConfig lbConfig = ServiceConfigUtil.unwrapLoadBalancingConfig(lbPolicyConfig); - if (lbConfig.getPolicyName().equals(WeightedRoundRobinLoadBalancerProvider.SCHEME) - && !enableWrr) { - throw new ResourceInvalidException("Cluster " + cluster.getName() + " specified LB policy " - + WeightedRoundRobinLoadBalancerProvider.SCHEME + " is not supported"); - } NameResolver.ConfigOrError configOrError = loadBalancerRegistry.getProvider( lbConfig.getPolicyName()).parseLoadBalancingPolicyConfig( lbConfig.getRawConfigValue()); diff --git a/xds/src/main/java/io/grpc/xds/XdsResourceType.java b/xds/src/main/java/io/grpc/xds/XdsResourceType.java index b1b2ee04054..4c19ebf776e 100644 --- a/xds/src/main/java/io/grpc/xds/XdsResourceType.java +++ b/xds/src/main/java/io/grpc/xds/XdsResourceType.java @@ -75,9 +75,6 @@ abstract class XdsResourceType { static final String TYPE_URL_TYPED_STRUCT = "type.googleapis.com/xds.type.v3.TypedStruct"; - static final String TYPE_URL_CLIENT_SIDE_WRR = "type.googleapis.com/envoy.extensions.load_balancing_policies.client_" - + "side_weighted_round_robin.v3.ClientSideWeightedRoundRobin"; - @Nullable abstract String extractResourceName(Message unpackedResource); diff --git a/xds/src/test/java/io/grpc/xds/LoadBalancerConfigFactoryTest.java b/xds/src/test/java/io/grpc/xds/LoadBalancerConfigFactoryTest.java index c7217cb45e3..a34710c9d94 100644 --- a/xds/src/test/java/io/grpc/xds/LoadBalancerConfigFactoryTest.java +++ b/xds/src/test/java/io/grpc/xds/LoadBalancerConfigFactoryTest.java @@ -24,6 +24,8 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.protobuf.Any; +import com.google.protobuf.BoolValue; +import com.google.protobuf.Duration; import com.google.protobuf.Struct; import com.google.protobuf.UInt32Value; import com.google.protobuf.UInt64Value; @@ -36,6 +38,7 @@ import io.envoyproxy.envoy.config.cluster.v3.LoadBalancingPolicy; import io.envoyproxy.envoy.config.cluster.v3.LoadBalancingPolicy.Policy; import io.envoyproxy.envoy.config.core.v3.TypedExtensionConfig; +import io.envoyproxy.envoy.extensions.load_balancing_policies.client_side_weighted_round_robin.v3.ClientSideWeightedRoundRobin; import io.envoyproxy.envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest; import io.envoyproxy.envoy.extensions.load_balancing_policies.ring_hash.v3.RingHash; import io.envoyproxy.envoy.extensions.load_balancing_policies.round_robin.v3.RoundRobin; @@ -78,6 +81,17 @@ public class LoadBalancerConfigFactoryTest { LeastRequest.newBuilder().setChoiceCount(UInt32Value.of(LEAST_REQUEST_CHOICE_COUNT)) .build()))).build(); + private static final Policy WRR_POLICY = Policy.newBuilder() + .setTypedExtensionConfig(TypedExtensionConfig.newBuilder() + .setName("backend") + .setTypedConfig( + Any.pack(ClientSideWeightedRoundRobin.newBuilder() + .setBlackoutPeriod(Duration.newBuilder().setSeconds(287).build()) + .setEnableOobLoadReport( + BoolValue.newBuilder().setValue(true).build()) + .build())) + .build()) + .build(); private static final String CUSTOM_POLICY_NAME = "myorg.MyCustomLeastRequestPolicy"; private static final String CUSTOM_POLICY_FIELD_KEY = "choiceCount"; private static final double CUSTOM_POLICY_FIELD_VALUE = 2; @@ -101,6 +115,11 @@ public class LoadBalancerConfigFactoryTest { private static final LbConfig VALID_ROUND_ROBIN_CONFIG = new LbConfig("wrr_locality_experimental", ImmutableMap.of("childPolicy", ImmutableList.of(ImmutableMap.of("round_robin", ImmutableMap.of())))); + + private static final LbConfig VALID_WRR_CONFIG = new LbConfig("wrr_locality_experimental", + ImmutableMap.of("childPolicy", ImmutableList.of( + ImmutableMap.of("weighted_round_robin_experimental", + ImmutableMap.of("blackoutPeriod",287000000000.0, "enableOobLoadReport", true ))))); private static final LbConfig VALID_RING_HASH_CONFIG = new LbConfig("ring_hash_experimental", ImmutableMap.of("minRingSize", (double) RING_HASH_MIN_RING_SIZE, "maxRingSize", (double) RING_HASH_MAX_RING_SIZE)); @@ -123,14 +142,28 @@ public void deregisterCustomProvider() { public void roundRobin() throws ResourceInvalidException { Cluster cluster = newCluster(buildWrrPolicy(ROUND_ROBIN_POLICY)); - assertThat(newLbConfig(cluster, true, true)).isEqualTo(VALID_ROUND_ROBIN_CONFIG); + assertThat(newLbConfig(cluster, true, true, true)).isEqualTo(VALID_ROUND_ROBIN_CONFIG); + } + + @Test + public void weightedRoundRobin() throws ResourceInvalidException { + Cluster cluster = newCluster(buildWrrPolicy(WRR_POLICY)); + + assertThat(newLbConfig(cluster, true, true, true)).isEqualTo(VALID_WRR_CONFIG); + } + + @Test + public void weightedRoundRobin_fallback_roundrobin() throws ResourceInvalidException { + Cluster cluster = newCluster(buildWrrPolicy(WRR_POLICY, ROUND_ROBIN_POLICY)); + + assertThat(newLbConfig(cluster, true, true, false)).isEqualTo(VALID_ROUND_ROBIN_CONFIG); } @Test public void roundRobin_legacy() throws ResourceInvalidException { Cluster cluster = Cluster.newBuilder().setLbPolicy(LbPolicy.ROUND_ROBIN).build(); - assertThat(newLbConfig(cluster, true, true)).isEqualTo(VALID_ROUND_ROBIN_CONFIG); + assertThat(newLbConfig(cluster, true, true, true)).isEqualTo(VALID_ROUND_ROBIN_CONFIG); } @Test @@ -139,7 +172,7 @@ public void ringHash() throws ResourceInvalidException { .setLoadBalancingPolicy(LoadBalancingPolicy.newBuilder().addPolicies(RING_HASH_POLICY)) .build(); - assertThat(newLbConfig(cluster, true, true)).isEqualTo(VALID_RING_HASH_CONFIG); + assertThat(newLbConfig(cluster, true, true, true)).isEqualTo(VALID_RING_HASH_CONFIG); } @Test @@ -149,7 +182,7 @@ public void ringHash_legacy() throws ResourceInvalidException { .setMaximumRingSize(UInt64Value.of(RING_HASH_MAX_RING_SIZE)) .setHashFunction(HashFunction.XX_HASH)).build(); - assertThat(newLbConfig(cluster, true, true)).isEqualTo(VALID_RING_HASH_CONFIG); + assertThat(newLbConfig(cluster, true, true, true)).isEqualTo(VALID_RING_HASH_CONFIG); } @Test @@ -161,7 +194,7 @@ public void ringHash_invalidHash() { .setMaximumRingSize(UInt64Value.of(RING_HASH_MAX_RING_SIZE)) .setHashFunction(RingHash.HashFunction.MURMUR_HASH_2).build()))).build()); - assertResourceInvalidExceptionThrown(cluster, true, true, "Invalid ring hash function"); + assertResourceInvalidExceptionThrown(cluster, true, true, true, "Invalid ring hash function"); } @Test @@ -169,7 +202,7 @@ public void ringHash_invalidHash_legacy() { Cluster cluster = Cluster.newBuilder().setLbPolicy(LbPolicy.RING_HASH).setRingHashLbConfig( RingHashLbConfig.newBuilder().setHashFunction(HashFunction.MURMUR_HASH_2)).build(); - assertResourceInvalidExceptionThrown(cluster, true, true, "invalid ring hash function"); + assertResourceInvalidExceptionThrown(cluster, true, true, true, "invalid ring hash function"); } @Test @@ -178,7 +211,7 @@ public void leastRequest() throws ResourceInvalidException { .setLoadBalancingPolicy(LoadBalancingPolicy.newBuilder().addPolicies(LEAST_REQUEST_POLICY)) .build(); - assertThat(newLbConfig(cluster, true, true)).isEqualTo(VALID_LEAST_REQUEST_CONFIG); + assertThat(newLbConfig(cluster, true, true, true)).isEqualTo(VALID_LEAST_REQUEST_CONFIG); } @Test @@ -190,7 +223,7 @@ public void leastRequest_legacy() throws ResourceInvalidException { LeastRequestLbConfig.newBuilder() .setChoiceCount(UInt32Value.of(LEAST_REQUEST_CHOICE_COUNT))).build(); - LbConfig lbConfig = newLbConfig(cluster, true, true); + LbConfig lbConfig = newLbConfig(cluster, true, true, true); assertThat(lbConfig.getPolicyName()).isEqualTo("wrr_locality_experimental"); List childConfigs = ServiceConfigUtil.unwrapLoadBalancingConfigList( @@ -207,14 +240,15 @@ public void leastRequest_notEnabled() { Cluster cluster = Cluster.newBuilder().setLbPolicy(LbPolicy.LEAST_REQUEST).build(); - assertResourceInvalidExceptionThrown(cluster, false, true, "unsupported lb policy"); + assertResourceInvalidExceptionThrown(cluster, false, true, true, "unsupported lb policy"); } @Test public void customRootLb_providerRegistered() throws ResourceInvalidException { LoadBalancerRegistry.getDefaultRegistry().register(CUSTOM_POLICY_PROVIDER); - assertThat(newLbConfig(newCluster(CUSTOM_POLICY), false, true)).isEqualTo(VALID_CUSTOM_CONFIG); + assertThat(newLbConfig(newCluster(CUSTOM_POLICY), false, true, + true)).isEqualTo(VALID_CUSTOM_CONFIG); } @Test @@ -223,7 +257,7 @@ public void customRootLb_providerNotRegistered() throws ResourceInvalidException .setLoadBalancingPolicy(LoadBalancingPolicy.newBuilder().addPolicies(CUSTOM_POLICY)) .build(); - assertResourceInvalidExceptionThrown(cluster, false, true, "Invalid LoadBalancingPolicy"); + assertResourceInvalidExceptionThrown(cluster, false, true, true,"Invalid LoadBalancingPolicy"); } // When a provider for the endpoint picking custom policy is available, the configuration should @@ -235,7 +269,7 @@ public void customLbInWrr_providerRegistered() throws ResourceInvalidException { Cluster cluster = Cluster.newBuilder().setLoadBalancingPolicy(LoadBalancingPolicy.newBuilder() .addPolicies(buildWrrPolicy(CUSTOM_POLICY, ROUND_ROBIN_POLICY))).build(); - assertThat(newLbConfig(cluster, false, true)).isEqualTo(VALID_CUSTOM_CONFIG_IN_WRR); + assertThat(newLbConfig(cluster, false, true, true)).isEqualTo(VALID_CUSTOM_CONFIG_IN_WRR); } // When a provider for the endpoint picking custom policy is available, the configuration should @@ -247,7 +281,7 @@ public void customLbInWrr_providerRegistered_udpa() throws ResourceInvalidExcept Cluster cluster = Cluster.newBuilder().setLoadBalancingPolicy(LoadBalancingPolicy.newBuilder() .addPolicies(buildWrrPolicy(CUSTOM_POLICY_UDPA, ROUND_ROBIN_POLICY))).build(); - assertThat(newLbConfig(cluster, false, true)).isEqualTo(VALID_CUSTOM_CONFIG_IN_WRR); + assertThat(newLbConfig(cluster, false, true, true)).isEqualTo(VALID_CUSTOM_CONFIG_IN_WRR); } // When a provider for the custom wrr_locality child policy is NOT available, we should fall back @@ -257,7 +291,7 @@ public void customLbInWrr_providerNotRegistered() throws ResourceInvalidExceptio Cluster cluster = Cluster.newBuilder().setLoadBalancingPolicy(LoadBalancingPolicy.newBuilder() .addPolicies(buildWrrPolicy(CUSTOM_POLICY, ROUND_ROBIN_POLICY))).build(); - assertThat(newLbConfig(cluster, false, true)).isEqualTo(VALID_ROUND_ROBIN_CONFIG); + assertThat(newLbConfig(cluster, false, true, true)).isEqualTo(VALID_ROUND_ROBIN_CONFIG); } // When a provider for the custom wrr_locality child policy is NOT available and no alternative @@ -267,7 +301,7 @@ public void customLbInWrr_providerNotRegistered_noFallback() throws ResourceInva Cluster cluster = Cluster.newBuilder().setLoadBalancingPolicy( LoadBalancingPolicy.newBuilder().addPolicies(buildWrrPolicy(CUSTOM_POLICY))).build(); - assertResourceInvalidExceptionThrown(cluster, false, true, "Invalid LoadBalancingPolicy"); + assertResourceInvalidExceptionThrown(cluster, false, true, true, "Invalid LoadBalancingPolicy"); } @Test @@ -278,7 +312,7 @@ public void customConfig_notEnabled() throws ResourceInvalidException { .build(); // Custom LB flag not set, so we use old logic that will default to round_robin. - assertThat(newLbConfig(cluster, true, false)).isEqualTo(VALID_ROUND_ROBIN_CONFIG); + assertThat(newLbConfig(cluster, true, false, true)).isEqualTo(VALID_ROUND_ROBIN_CONFIG); } @Test @@ -305,7 +339,7 @@ public void maxRecursion() { buildWrrPolicy( ROUND_ROBIN_POLICY))))))))))))))))))).build(); - assertResourceInvalidExceptionThrown(cluster, false, true, + assertResourceInvalidExceptionThrown(cluster, false, true, true, "Maximum LB config recursion depth reached"); } @@ -322,16 +356,17 @@ private static Policy buildWrrPolicy(Policy... childPolicy) { } private LbConfig newLbConfig(Cluster cluster, boolean enableLeastRequest, - boolean enableCustomConfig) + boolean enableCustomConfig, boolean enableWrr) throws ResourceInvalidException { return ServiceConfigUtil.unwrapLoadBalancingConfig( - LoadBalancerConfigFactory.newConfig(cluster, enableLeastRequest, enableCustomConfig)); + LoadBalancerConfigFactory.newConfig(cluster, enableLeastRequest, enableCustomConfig, + enableWrr)); } private void assertResourceInvalidExceptionThrown(Cluster cluster, boolean enableLeastRequest, - boolean enableCustomConfig, String expectedMessage) { + boolean enableCustomConfig, boolean enableWrr, String expectedMessage) { try { - newLbConfig(cluster, enableLeastRequest, enableCustomConfig); + newLbConfig(cluster, enableLeastRequest, enableCustomConfig, enableWrr); } catch (ResourceInvalidException e) { assertThat(e).hasMessageThat().contains(expectedMessage); return; diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProviderTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProviderTest.java index 08945a6ce36..7e82a70ac7f 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProviderTest.java @@ -74,11 +74,11 @@ public void providesLoadBalancer() { @Test public void parseLoadBalancingConfig() throws IOException { String lbConfig = - "{\"blackoutPeriod\" : \"20s\"," - + " \"weightExpirationPeriod\" : \"300s\"," - + " \"oobReportingPeriod\" : \"100s\"," + "{\"blackoutPeriod\" : 20000000000," + + " \"weightExpirationPeriod\" : 300000000000," + + " \"oobReportingPeriod\" : 100000000000," + " \"enableOobLoadReport\" : true," - + " \"weightUpdatePeriod\" : \"2s\"" + + " \"weightUpdatePeriod\" : 2000000000" + " }"; ConfigOrError configOrError = provider.parseLoadBalancingPolicyConfig( @@ -88,13 +88,14 @@ public void parseLoadBalancingConfig() throws IOException { (WeightedRoundRobinLoadBalancerConfig) configOrError.getConfig(); assertThat(config.blackoutPeriodNanos).isEqualTo(20_000_000_000L); assertThat(config.weightExpirationPeriodNanos).isEqualTo(300_000_000_000L); + assertThat(config.oobReportingPeriodNanos).isEqualTo(100_000_000_000L); assertThat(config.enableOobLoadReport).isEqualTo(true); assertThat(config.weightUpdatePeriodNanos).isEqualTo(2_000_000_000L); } @Test public void parseLoadBalancingConfigDefaultValues() throws IOException { - String lbConfig = "{\"weightUpdatePeriod\" : \"0.02s\"}"; + String lbConfig = "{\"weightUpdatePeriod\" : 2000000}"; ConfigOrError configOrError = provider.parseLoadBalancingPolicyConfig( parseJsonObject(lbConfig)); diff --git a/xds/src/test/java/io/grpc/xds/XdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/XdsClientImplDataTest.java index 5a4a67ea384..3293e865842 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientImplDataTest.java @@ -27,6 +27,7 @@ import com.google.protobuf.Any; import com.google.protobuf.BoolValue; import com.google.protobuf.ByteString; +import com.google.protobuf.Duration; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; import com.google.protobuf.StringValue; @@ -86,6 +87,8 @@ import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager; import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter; import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.Rds; +import io.envoyproxy.envoy.extensions.load_balancing_policies.client_side_weighted_round_robin.v3.ClientSideWeightedRoundRobin; +import io.envoyproxy.envoy.extensions.load_balancing_policies.wrr_locality.v3.WrrLocality; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateProviderPluginInstance; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext; @@ -1975,24 +1978,44 @@ public void parseCluster_leastRequestLbPolicy_defaultLbConfig() throws ResourceI @Test public void parseCluster_WrrLbPolicy_defaultLbConfig() throws ResourceInvalidException { XdsResourceType.enableWrr = true; + + LoadBalancingPolicy wrrConfig = + LoadBalancingPolicy.newBuilder().addPolicies( + LoadBalancingPolicy.Policy.newBuilder() + .setTypedExtensionConfig(TypedExtensionConfig.newBuilder() + .setName("backend") + .setTypedConfig( + Any.pack(ClientSideWeightedRoundRobin.newBuilder() + .setBlackoutPeriod(Duration.newBuilder().setSeconds(17).build()) + .setEnableOobLoadReport( + BoolValue.newBuilder().setValue(true).build()) + .build())) + .build()) + .build()) + .build(); + Cluster cluster = Cluster.newBuilder() .setName("cluster-foo.googleapis.com") .setType(DiscoveryType.EDS) .setEdsClusterConfig( - EdsClusterConfig.newBuilder() - .setEdsConfig( - ConfigSource.newBuilder() - .setAds(AggregatedConfigSource.getDefaultInstance())) + EdsClusterConfig.newBuilder() + .setEdsConfig( + ConfigSource.newBuilder() + .setAds(AggregatedConfigSource.getDefaultInstance())) .setServiceName("service-foo.googleapis.com")) - .setLoadBalancingPolicy(LoadBalancingPolicy.newBuilder().addPolicies( - LoadBalancingPolicy.Policy.newBuilder().mergeTypedExtensionConfig( - TypedExtensionConfig.newBuilder().mergeTypedConfig(Any.pack( - TypedExtensionConfig.newBuilder().setName( - WeightedRoundRobinLoadBalancerProvider.SCHEME).setTypedConfig( - Any.pack()).build()) - ).build()).build()).build()) - .build(); - + .setLoadBalancingPolicy( + LoadBalancingPolicy.newBuilder().addPolicies( + LoadBalancingPolicy.Policy.newBuilder() + .setTypedExtensionConfig( + TypedExtensionConfig.newBuilder() + .setTypedConfig( + Any.pack(WrrLocality.newBuilder() + .setEndpointPickingPolicy(wrrConfig) + .build())) + .build()) + .build()) + .build()) + .build(); CdsUpdate update = XdsClusterResource.processCluster( cluster, null, LRS_SERVER_INFO, LoadBalancerRegistry.getDefaultRegistry()); From 3979ef84561585193188023f1bc2c0abd29ab3f5 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Wed, 22 Feb 2023 16:41:56 -0800 Subject: [PATCH 21/25] add test scheduler --- xds/BUILD.bazel | 1 + .../xds/WeightedRoundRobinLoadBalancer.java | 3 +- .../WeightedRoundRobinLoadBalancerTest.java | 133 +++++++++++------- 3 files changed, 86 insertions(+), 51 deletions(-) diff --git a/xds/BUILD.bazel b/xds/BUILD.bazel index f75b2bbeeaa..2d7e18daf1d 100644 --- a/xds/BUILD.bazel +++ b/xds/BUILD.bazel @@ -86,6 +86,7 @@ java_proto_library( "@envoy_api//envoy/extensions/filters/http/rbac/v3:pkg", "@envoy_api//envoy/extensions/filters/http/router/v3:pkg", "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg", + "@envoy_api//envoy/extensions/load_balancing_policies/client_side_weighted_round_robin/v3:pkg", "@envoy_api//envoy/extensions/load_balancing_policies/least_request/v3:pkg", "@envoy_api//envoy/extensions/load_balancing_policies/ring_hash/v3:pkg", "@envoy_api//envoy/extensions/load_balancing_policies/round_robin/v3:pkg", diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 2019e3372df..672692c8391 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -331,7 +331,8 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { * * */ - private static final class EdfScheduler { + @VisibleForTesting + static final class EdfScheduler { private final PriorityQueue prioQueue; /** diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index 6d8417d4163..2a9ce65099e 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -51,6 +51,7 @@ import io.grpc.services.InternalCallMetricRecorder; import io.grpc.services.MetricReport; import io.grpc.util.RoundRobinLoadBalancer.EmptyPicker; +import io.grpc.xds.WeightedRoundRobinLoadBalancer.EdfScheduler; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinLoadBalancerConfig; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinPicker; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WrrSubchannel; @@ -61,9 +62,12 @@ import java.util.List; import java.util.Map; import java.util.Queue; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -113,8 +117,6 @@ public class WeightedRoundRobinLoadBalancerTest { private final Attributes affinity = Attributes.newBuilder().set(MAJOR_KEY, "I got the keys").build(); - private static final double EDF_PRECISE = 0.01; - private final SynchronizationContext syncContext = new SynchronizationContext( new Thread.UncaughtExceptionHandler() { @Override @@ -260,7 +262,8 @@ public void enableOobLoadReportConfig() { } private void pickByWeight(MetricReport r1, MetricReport r2, MetricReport r3, - double p1, double p2, double p3) { + double subchannel1PickRatio, double subchannel2PickRatio, + double subchannel3PickRatio) { syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() .setAddresses(servers).setLoadBalancingPolicyConfig(weightedConfig) .setAttributes(affinity).build())); @@ -294,36 +297,39 @@ private void pickByWeight(MetricReport r1, MetricReport r2, MetricReport r3, pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); } assertThat(pickCount.size()).isEqualTo(3); - assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 10000.0 - p1)).isAtMost(EDF_PRECISE); - assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 10000.0 - p2 )).isAtMost(EDF_PRECISE); - assertThat(Math.abs(pickCount.get(weightedSubchannel3) / 10000.0 - p3 )).isAtMost(EDF_PRECISE); + assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 10000.0 - subchannel1PickRatio)) + .isAtMost(0.001); + assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 10000.0 - subchannel2PickRatio )) + .isAtMost(0.001); + assertThat(Math.abs(pickCount.get(weightedSubchannel3) / 10000.0 - subchannel3PickRatio )) + .isAtMost(0.001); } @Test public void pickByWeight_LargeWeight() { - pickByWeight(InternalCallMetricRecorder.createMetricReport( - 0.1, 0.1, 2200, new HashMap<>(), new HashMap<>()), - InternalCallMetricRecorder.createMetricReport( - 0.9, 0.1, 2, new HashMap<>(), new HashMap<>()), - InternalCallMetricRecorder.createMetricReport( - 0.86, 0.1, 100, new HashMap<>(), new HashMap<>()), - 2200 / 0.1 / (2200 / 0.1 + 2 / 0.9 + 100 / 0.86), - 27 / 0.9 / (2200 / 0.1 + 2 / 0.9 + 100 / 0.86), - 100 / 0.86 / ( 2200 / 0.1 + 2 / 0.9 + 100 / 0.86) - ); + MetricReport report1 = InternalCallMetricRecorder.createMetricReport( + 0.1, 0.1, 999, new HashMap<>(), new HashMap<>()); + MetricReport report2 = InternalCallMetricRecorder.createMetricReport( + 0.9, 0.1, 2, new HashMap<>(), new HashMap<>()); + MetricReport report3 = InternalCallMetricRecorder.createMetricReport( + 0.86, 0.1, 100, new HashMap<>(), new HashMap<>()); + double totalWeight = 999 / 0.1 + 2 / 0.9 + 100 / 0.86; + + pickByWeight(report1, report2, report3, 999 / 0.1 / totalWeight, 2 / 0.9 / totalWeight, + 100 / 0.86 / totalWeight); } @Test public void pickByWeight_normalWeight() { - pickByWeight(InternalCallMetricRecorder.createMetricReport( - 0.12, 0.1, 22, new HashMap<>(), new HashMap<>()), - InternalCallMetricRecorder.createMetricReport( - 0.28, 0.1, 40, new HashMap<>(), new HashMap<>()), - InternalCallMetricRecorder.createMetricReport( - 0.86, 0.1, 100, new HashMap<>(), new HashMap<>()), - 22 / 0.12 / (22 / 0.12 + 40 / 0.28 + 100 / 0.86), - 40 / 0.28 / (22 / 0.12 + 40 / 0.28 + 100 / 0.86), - 100 / 0.86 / ( 22 / 0.12 + 40 / 0.28 + 100 / 0.86) + MetricReport report1 = InternalCallMetricRecorder.createMetricReport( + 0.12, 0.1, 22, new HashMap<>(), new HashMap<>()); + MetricReport report2 = InternalCallMetricRecorder.createMetricReport( + 0.28, 0.1, 40, new HashMap<>(), new HashMap<>()); + MetricReport report3 = InternalCallMetricRecorder.createMetricReport( + 0.86, 0.1, 100, new HashMap<>(), new HashMap<>()); + double totalWeight = 22 / 0.12 + 40 / 0.28 + 100 / 0.86; + pickByWeight(report1, report2, report3, 22 / 0.12 / totalWeight, + 40 / 0.28 / totalWeight, 100 / 0.86 / totalWeight ); } @@ -379,8 +385,8 @@ public void blackoutPeriod() { } assertThat(pickCount.size()).isEqualTo(2); // within blackout period, fallback to simple round robin - assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 0.5)).isAtMost(EDF_PRECISE); - assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 0.5)).isAtMost(EDF_PRECISE); + assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 0.5)).isAtMost(0.001); + assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 0.5)).isAtMost(0.001); assertThat(fakeClock.forwardTime(5, TimeUnit.SECONDS)).isEqualTo(1); pickCount = new HashMap<>(); @@ -391,9 +397,9 @@ public void blackoutPeriod() { assertThat(pickCount.size()).isEqualTo(2); // after blackout period assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 2.0 / 3)) - .isAtMost(EDF_PRECISE); + .isAtMost(0.001); assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 1.0 / 3)) - .isAtMost(EDF_PRECISE); + .isAtMost(0.001); } @Test @@ -429,9 +435,9 @@ public void weightExpired() { } assertThat(pickCount.size()).isEqualTo(2); assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 2.0 / 3)) - .isAtMost(EDF_PRECISE); + .isAtMost(0.001); assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 1.0 / 3)) - .isAtMost(EDF_PRECISE); + .isAtMost(0.001); // weight expired, fallback to simple round robin assertThat(fakeClock.forwardTime(300, TimeUnit.SECONDS)).isEqualTo(1); @@ -442,9 +448,9 @@ public void weightExpired() { } assertThat(pickCount.size()).isEqualTo(2); assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 0.5)) - .isAtMost(EDF_PRECISE); + .isAtMost(0.001); assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 0.5)) - .isAtMost(EDF_PRECISE); + .isAtMost(0.001); } @Test @@ -484,12 +490,12 @@ public void unknownWeightIsAvgWeight() { } assertThat(pickCount.size()).isEqualTo(3); assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 4.0 / 9)) - .isAtMost(EDF_PRECISE); + .isAtMost(0.001); assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 2.0 / 9)) - .isAtMost(EDF_PRECISE); + .isAtMost(0.001); // subchannel3's weight is average of subchannel1 and subchannel2 assertThat(Math.abs(pickCount.get(weightedSubchannel3) / 1000.0 - 3.0 / 9)) - .isAtMost(EDF_PRECISE); + .isAtMost(0.001); } @Test @@ -519,23 +525,20 @@ public void pickFromOtherThread() throws Exception { 0.2, 0.1, 1, new HashMap<>(), new HashMap<>())); assertThat(weightedPicker.toString()).contains("rrMode=true"); CyclicBarrier barrier = new CyclicBarrier(2); + Map pickCount = new ConcurrentHashMap<>(); + pickCount.put(weightedSubchannel1, new AtomicInteger(0)); + pickCount.put(weightedSubchannel2, new AtomicInteger(0)); new Thread(new Runnable() { @Override public void run() { try { weightedPicker.pickSubchannel(mockArgs); barrier.await(); - Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { Subchannel result = weightedPicker.pickSubchannel(mockArgs).getSubchannel(); - pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + pickCount.get(result).addAndGet(1); } - assertThat(pickCount.size()).isEqualTo(2); - // after blackout period - assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 2.0 / 3)) - .isAtMost(EDF_PRECISE); - assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 1.0 / 3)) - .isAtMost(EDF_PRECISE); + barrier.await(); } catch (Exception ex) { throw new AssertionError(ex); } @@ -543,17 +546,47 @@ public void run() { }).start(); assertThat(fakeClock.forwardTime(10, TimeUnit.SECONDS)).isEqualTo(1); barrier.await(); - Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { Subchannel result = weightedPicker.pickSubchannel(mockArgs).getSubchannel(); - pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + pickCount.get(result).addAndGet(1); } + barrier.await(); assertThat(pickCount.size()).isEqualTo(2); // after blackout period - assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 2.0 / 3)) - .isAtMost(EDF_PRECISE); - assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 1.0 / 3)) - .isAtMost(EDF_PRECISE); + assertThat(Math.abs(pickCount.get(weightedSubchannel1).get() / 2000.0 - 2.0 / 3)) + .isAtMost(0.001); + assertThat(Math.abs(pickCount.get(weightedSubchannel2).get() / 2000.0 - 1.0 / 3)) + .isAtMost(0.001); + } + + @Test + public void edfScheduler() { + Random random = new Random(); + double totalWeight = 0; + int capacity = random.nextInt(10) + 1; + double[] weights = new double[capacity]; + EdfScheduler scheduler = new EdfScheduler(capacity); + for (int i = 0; i < capacity; i++) { + weights[i] = random.nextDouble(); + scheduler.add(i, weights[i]); + totalWeight += weights[i]; + } + Map pickCount = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + int result = scheduler.pick(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + } + for (int i = 0; i < capacity; i++) { + assertThat(Math.abs(pickCount.get(i) / 1000.0 - weights[i] / totalWeight) ).isAtMost(0.001); + } + } + + @Test + public void edsScheduler_sameWeight() { + EdfScheduler scheduler = new EdfScheduler(2); + scheduler.add(0, 0.5); + scheduler.add(1, 0.5); + assertThat(scheduler.pick()).isEqualTo(0); } private static class FakeSocketAddress extends SocketAddress { From 0704a6930bca83c2e2c53024cb14b01f2f6e2118 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Thu, 23 Feb 2023 14:52:39 -0800 Subject: [PATCH 22/25] fix comments, timer, volatile, etc --- .../io/grpc/util/RoundRobinLoadBalancer.java | 10 +- repositories.bzl | 4 +- .../grpc/xds/LoadBalancerConfigFactory.java | 24 ++--- .../xds/WeightedRoundRobinLoadBalancer.java | 101 +++++++++--------- ...eightedRoundRobinLoadBalancerProvider.java | 12 ++- .../xds/LoadBalancerConfigFactoryTest.java | 2 +- ...tedRoundRobinLoadBalancerProviderTest.java | 11 +- .../WeightedRoundRobinLoadBalancerTest.java | 72 ++++++++++++- .../io/grpc/xds/XdsClientImplDataTest.java | 10 +- 9 files changed, 163 insertions(+), 83 deletions(-) diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java index 83c7094c883..4649302af1c 100644 --- a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java @@ -208,10 +208,7 @@ private void updateBalancingState() { // an arbitrary subchannel, otherwise return OK. new EmptyPicker(aggStatus)); } else { - // initialize the Picker to a random start index to ensure that a high frequency of Picker - // churn does not skew subchannel selection. - int startIndex = random.nextInt(activeList.size()); - updateBalancingState(READY, createReadyPicker(activeList, startIndex)); + updateBalancingState(READY, createReadyPicker(activeList)); } } @@ -223,7 +220,10 @@ private void updateBalancingState(ConnectivityState state, RoundRobinPicker pick } } - protected RoundRobinPicker createReadyPicker(List activeList, int startIndex) { + protected RoundRobinPicker createReadyPicker(List activeList) { + // initialize the Picker to a random start index to ensure that a high frequency of Picker + // churn does not skew subchannel selection. + int startIndex = random.nextInt(activeList.size()); return new ReadyPicker(activeList, startIndex); } diff --git a/repositories.bzl b/repositories.bzl index 6c586a69d26..1c0759223dc 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -138,9 +138,9 @@ def grpc_java_repositories(): http_archive( name = "envoy_api", sha256 = "a0c58442cc2038ccccad9616dd1bab5ff1e65da2bbc0ae41020ef6010119eb0e", - strip_prefix = "data-plane-api-869b00336913138cad96a653458aab650c4e70ea", + strip_prefix = "data-plane-api-b1d2e441133c00bfe8412dfd6e93ea85e66da9bb", urls = [ - "https://github.com/envoyproxy/data-plane-api/archive/869b00336913138cad96a653458aab650c4e70ea.tar.gz", + "https://github.com/envoyproxy/data-plane-api/archive/b1d2e441133c00bfe8412dfd6e93ea85e66da9bb.tar.gz", ], ) diff --git a/xds/src/main/java/io/grpc/xds/LoadBalancerConfigFactory.java b/xds/src/main/java/io/grpc/xds/LoadBalancerConfigFactory.java index 93fbfacbed4..f0c3cb751de 100644 --- a/xds/src/main/java/io/grpc/xds/LoadBalancerConfigFactory.java +++ b/xds/src/main/java/io/grpc/xds/LoadBalancerConfigFactory.java @@ -127,26 +127,26 @@ class LoadBalancerConfigFactory { * Builds a service config JSON object for the weighted_round_robin load balancer config based on * the given config values. */ - private static ImmutableMap buildWrrConfig(Long blackoutPeriod, - Long weightExpirationPeriod, - Long oobReportingPeriod, + private static ImmutableMap buildWrrConfig(String blackoutPeriod, + String weightExpirationPeriod, + String oobReportingPeriod, Boolean enableOobLoadReport, - Long weightUpdatePeriod) { + String weightUpdatePeriod) { ImmutableMap.Builder configBuilder = ImmutableMap.builder(); if (blackoutPeriod != null) { - configBuilder.put(BLACK_OUT_PERIOD, blackoutPeriod.doubleValue()); + configBuilder.put(BLACK_OUT_PERIOD, blackoutPeriod); } if (weightExpirationPeriod != null) { - configBuilder.put(WEIGHT_EXPIRATION_PERIOD, weightExpirationPeriod.doubleValue()); + configBuilder.put(WEIGHT_EXPIRATION_PERIOD, weightExpirationPeriod); } if (oobReportingPeriod != null) { - configBuilder.put(OOB_REPORTING_PERIOD, oobReportingPeriod.doubleValue()); + configBuilder.put(OOB_REPORTING_PERIOD, oobReportingPeriod); } if (enableOobLoadReport != null) { configBuilder.put(ENABLE_OOB_LOAD_REPORT, enableOobLoadReport); } if (weightUpdatePeriod != null) { - configBuilder.put(WEIGHT_UPDATE_PERIOD, weightUpdatePeriod.doubleValue()); + configBuilder.put(WEIGHT_UPDATE_PERIOD, weightUpdatePeriod); } return ImmutableMap.of(WeightedRoundRobinLoadBalancerProvider.SCHEME, configBuilder.buildOrThrow()); @@ -266,12 +266,12 @@ static class LoadBalancingPolicyConverter { private static ImmutableMap convertWeightedRoundRobinConfig( ClientSideWeightedRoundRobin wrr) throws ResourceInvalidException { return buildWrrConfig( - wrr.hasBlackoutPeriod() ? Durations.toNanos(wrr.getBlackoutPeriod()) : null, + wrr.hasBlackoutPeriod() ? Durations.toString(wrr.getBlackoutPeriod()) : null, wrr.hasWeightExpirationPeriod() - ? Durations.toNanos(wrr.getWeightExpirationPeriod()) : null, - wrr.hasOobReportingPeriod() ? Durations.toNanos(wrr.getOobReportingPeriod()) : null, + ? Durations.toString(wrr.getWeightExpirationPeriod()) : null, + wrr.hasOobReportingPeriod() ? Durations.toString(wrr.getOobReportingPeriod()) : null, wrr.hasEnableOobLoadReport() ? wrr.getEnableOobLoadReport().getValue() : null, - wrr.hasWeightUpdatePeriod() ? Durations.toNanos(wrr.getWeightUpdatePeriod()) : null); + wrr.hasWeightUpdatePeriod() ? Durations.toString(wrr.getWeightUpdatePeriod()) : null); } /** diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 672692c8391..7a53d02809e 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -43,6 +43,7 @@ import java.util.HashSet; import java.util.List; import java.util.PriorityQueue; +import java.util.Random; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -53,17 +54,21 @@ * determined by backend metrics using ORCA. */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/9885") -public final class WeightedRoundRobinLoadBalancer extends RoundRobinLoadBalancer { +final class WeightedRoundRobinLoadBalancer extends RoundRobinLoadBalancer { private volatile WeightedRoundRobinLoadBalancerConfig config; private final SynchronizationContext syncContext; private final ScheduledExecutorService timeService; private ScheduledHandle weightUpdateTimer; + private final Runnable updateWeightTask; + private final Random random; public WeightedRoundRobinLoadBalancer(Helper helper, TimeProvider timeProvider) { super(new WrrHelper(OrcaOobUtil.newOrcaReportingHelper(helper), checkNotNull(timeProvider, "timeProvider"))); this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext"); this.timeService = checkNotNull(helper.getScheduledExecutorService(), "timeService"); + this.updateWeightTask = new UpdateWeightTask(); + this.random = new Random(); } @Override @@ -78,27 +83,28 @@ public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { config = (WeightedRoundRobinLoadBalancerConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); boolean accepted = super.acceptResolvedAddresses(resolvedAddresses); - new UpdateWeightTask().run(); + if (weightUpdateTimer != null && weightUpdateTimer.isPending()) { + weightUpdateTimer.cancel(); + } + updateWeightTask.run(); afterAcceptAddresses(); return accepted; } @Override - public RoundRobinPicker createReadyPicker(List activeList, int startIndex) { + public RoundRobinPicker createReadyPicker(List activeList) { + int startIndex = random.nextInt(activeList.size()); return new WeightedRoundRobinPicker(activeList, startIndex); } private final class UpdateWeightTask implements Runnable { @Override public void run() { - if (weightUpdateTimer != null && weightUpdateTimer.isPending()) { - return; - } if (currentPicker != null && currentPicker instanceof WeightedRoundRobinPicker) { ((WeightedRoundRobinPicker)currentPicker).updateWeight(); } - weightUpdateTimer = syncContext.schedule(new UpdateWeightTask(), - config.weightUpdatePeriodNanos, TimeUnit.NANOSECONDS, timeService); + weightUpdateTimer = syncContext.schedule(this, config.weightUpdatePeriodNanos, + TimeUnit.NANOSECONDS, timeService); } } @@ -154,7 +160,7 @@ static final class WrrSubchannel extends ForwardingSubchannel { private volatile long lastUpdated; private volatile long nonEmptySince; private volatile double weight; - private volatile WeightedRoundRobinLoadBalancerConfig config; + private WeightedRoundRobinLoadBalancerConfig config; WrrSubchannel(Subchannel delegate, TimeProvider timeProvider) { this.delegate = checkNotNull(delegate, "delegate"); @@ -196,7 +202,7 @@ private double getWeight() { if (config == null) { return 0; } - double now = timeProvider.currentTimeNanos(); + long now = timeProvider.currentTimeNanos(); if (now - lastUpdated >= config.weightExpirationPeriodNanos) { nonEmptySince = Integer.MAX_VALUE; return 0; @@ -224,7 +230,7 @@ final class WeightedRoundRobinPicker extends ReadyPicker { super(checkNotNull(list, "list"), startIndex); Preconditions.checkArgument(!list.isEmpty(), "empty list"); this.list = list; - this.schedulerRef = new AtomicReference<>(new EdfScheduler(list.size())); + this.schedulerRef = new AtomicReference<>(null); updateWeight(); } @@ -246,7 +252,6 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { } private void updateWeight() { - EdfScheduler scheduler = new EdfScheduler(list.size()); int weightedChannelCount = 0; double avgWeight = 0; for (Subchannel value : list) { @@ -260,6 +265,7 @@ private void updateWeight() { rrMode = true; return; } + EdfScheduler scheduler = new EdfScheduler(list.size()); avgWeight /= 1.0 * weightedChannelCount; for (int i = 0; i < list.size(); i++) { WrrSubchannel subchannel = (WrrSubchannel) list.get(i); @@ -299,8 +305,9 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { * *

Specifically, each object added to chooser is given a deadline equal to the multiplicative * inverse of its weight. The place of each object in its deadline is tracked, and each call to - * choose returns the object with the least remaining time in its deadline (1/weight). - * (Ties are broken by the order in which the children were added to the chooser.) + * choose returns the object with the least remaining time in its deadline. + * (Ties are broken by the order in which the children were added to the chooser.) The deadline + * advances by the multiplicative inverse of the object's weight. * For example, if items A and B are added with weights 0.5 and 0.2, successive chooses return: * *

    @@ -321,13 +328,11 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { *
  • etc. *
* - *

In short: the entry with the highest weight is preferred. In case of ties, the object that - * was last returned will be preferred. + *

In short: the entry with the highest weight is preferred. * *

    *
  • add() - O(lg n) - *
  • remove() - O(lg n) - *
  • pick() - O(lg n) with worst case O(n) + *
  • pick() - O(lg n) *
* */ @@ -336,7 +341,7 @@ static final class EdfScheduler { private final PriorityQueue prioQueue; /** - * Weights below this value will be logged and upped to this minimum weight. + * Weights below this value will be upped to this minimum weight. */ private static final double MINIMUM_WEIGHT = 0.0001; @@ -349,25 +354,24 @@ static final class EdfScheduler { EdfScheduler(int initialCapacity) { this.prioQueue = new PriorityQueue(initialCapacity, (o1, o2) -> { if (o1.deadline == o2.deadline) { - return o1.index - o2.index; - } else if (o1.deadline < o2.deadline) { - return -1; + return Double.compare(o1.index, o2.index); } else { - return 1; + return Double.compare(o1.deadline, o2.deadline); } }); } /** - * Adds (or updates) the item in the scheduler. This is not thread safe. + * Adds the item in the scheduler. This is not thread safe. * - * @param index The field {@link ObjectState#index} to be added/updated - * @param weight positive weight for the added/updated object + * @param index The field {@link ObjectState#index} to be added + * @param weight positive weight for the added object */ void add(int index, double weight) { checkArgument(weight > 0.0, "Weights need to be positive."); ObjectState state = new ObjectState(Math.max(weight, MINIMUM_WEIGHT), index); state.deadline = 1 / state.weight; + // TODO(zivy): randomize the initial deadline. prioQueue.add(state); } @@ -398,21 +402,21 @@ static class ObjectState { } static final class WeightedRoundRobinLoadBalancerConfig { - final Long blackoutPeriodNanos; - final Long weightExpirationPeriodNanos; - final Boolean enableOobLoadReport; - final Long oobReportingPeriodNanos; - final Long weightUpdatePeriodNanos; + final long blackoutPeriodNanos; + final long weightExpirationPeriodNanos; + final boolean enableOobLoadReport; + final long oobReportingPeriodNanos; + final long weightUpdatePeriodNanos; public static Builder newBuilder() { return new Builder(); } - private WeightedRoundRobinLoadBalancerConfig(Long blackoutPeriodNanos, - Long weightExpirationPeriodNanos, - Boolean enableOobLoadReport, - Long oobReportingPeriodNanos, - Long weightUpdatePeriodNanos) { + private WeightedRoundRobinLoadBalancerConfig(long blackoutPeriodNanos, + long weightExpirationPeriodNanos, + boolean enableOobLoadReport, + long oobReportingPeriodNanos, + long weightUpdatePeriodNanos) { this.blackoutPeriodNanos = blackoutPeriodNanos; this.weightExpirationPeriodNanos = weightExpirationPeriodNanos; this.enableOobLoadReport = enableOobLoadReport; @@ -421,42 +425,37 @@ private WeightedRoundRobinLoadBalancerConfig(Long blackoutPeriodNanos, } static final class Builder { - Long blackoutPeriodNanos = 10_000_000_000L; // 10s - Long weightExpirationPeriodNanos = 180_000_000_000L; //3min - Boolean enableOobLoadReport = false; - Long oobReportingPeriodNanos = 10_000_000_000L; // 10s - Long weightUpdatePeriodNanos = 1_000_000_000L; // 1s + long blackoutPeriodNanos = 10_000_000_000L; // 10s + long weightExpirationPeriodNanos = 180_000_000_000L; //3min + boolean enableOobLoadReport = false; + long oobReportingPeriodNanos = 10_000_000_000L; // 10s + long weightUpdatePeriodNanos = 1_000_000_000L; // 1s private Builder() { } - Builder setBlackoutPeriodNanos(Long blackoutPeriodNanos) { - checkArgument(blackoutPeriodNanos != null); + Builder setBlackoutPeriodNanos(long blackoutPeriodNanos) { this.blackoutPeriodNanos = blackoutPeriodNanos; return this; } - Builder setWeightExpirationPeriodNanos(Long weightExpirationPeriodNanos) { - checkArgument(weightExpirationPeriodNanos != null); + Builder setWeightExpirationPeriodNanos(long weightExpirationPeriodNanos) { this.weightExpirationPeriodNanos = weightExpirationPeriodNanos; return this; } - Builder setEnableOobLoadReport(Boolean enableOobLoadReport) { - checkArgument(enableOobLoadReport != null); + Builder setEnableOobLoadReport(boolean enableOobLoadReport) { this.enableOobLoadReport = enableOobLoadReport; return this; } - Builder setOobReportingPeriodNanos(Long oobReportingPeriodNanos) { - checkArgument(oobReportingPeriodNanos != null); + Builder setOobReportingPeriodNanos(long oobReportingPeriodNanos) { this.oobReportingPeriodNanos = oobReportingPeriodNanos; return this; } - Builder setWeightUpdatePeriodNanos(Long weightUpdatePeriodNanos) { - checkArgument(weightUpdatePeriodNanos != null); + Builder setWeightUpdatePeriodNanos(long weightUpdatePeriodNanos) { this.weightUpdatePeriodNanos = weightUpdatePeriodNanos; return this; } diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java index 2b7aaabaa77..58f03795d46 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java @@ -18,6 +18,7 @@ import com.google.common.annotations.VisibleForTesting; import io.grpc.ExperimentalApi; +import io.grpc.Internal; import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancerProvider; @@ -28,9 +29,10 @@ import java.util.Map; /** - * Providers a {@link WeightedRoundRobinLoadBalancer}. + * Provides a {@link WeightedRoundRobinLoadBalancer}. * */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/9885") +@Internal public final class WeightedRoundRobinLoadBalancerProvider extends LoadBalancerProvider { @VisibleForTesting @@ -60,12 +62,12 @@ public String getPolicyName() { @Override public ConfigOrError parseLoadBalancingPolicyConfig(Map rawConfig) { - Long blackoutPeriodNanos = JsonUtil.getNumberAsLong(rawConfig, "blackoutPeriod"); + Long blackoutPeriodNanos = JsonUtil.getStringAsDuration(rawConfig, "blackoutPeriod"); Long weightExpirationPeriodNanos = - JsonUtil.getNumberAsLong(rawConfig, "weightExpirationPeriod"); - Long oobReportingPeriodNanos = JsonUtil.getNumberAsLong(rawConfig, "oobReportingPeriod"); + JsonUtil.getStringAsDuration(rawConfig, "weightExpirationPeriod"); + Long oobReportingPeriodNanos = JsonUtil.getStringAsDuration(rawConfig, "oobReportingPeriod"); Boolean enableOobLoadReport = JsonUtil.getBoolean(rawConfig, "enableOobLoadReport"); - Long weightUpdatePeriodNanos = JsonUtil.getNumberAsLong(rawConfig, "weightUpdatePeriod"); + Long weightUpdatePeriodNanos = JsonUtil.getStringAsDuration(rawConfig, "weightUpdatePeriod"); WeightedRoundRobinLoadBalancerConfig.Builder configBuilder = WeightedRoundRobinLoadBalancerConfig.newBuilder(); diff --git a/xds/src/test/java/io/grpc/xds/LoadBalancerConfigFactoryTest.java b/xds/src/test/java/io/grpc/xds/LoadBalancerConfigFactoryTest.java index a34710c9d94..d18d665d317 100644 --- a/xds/src/test/java/io/grpc/xds/LoadBalancerConfigFactoryTest.java +++ b/xds/src/test/java/io/grpc/xds/LoadBalancerConfigFactoryTest.java @@ -119,7 +119,7 @@ public class LoadBalancerConfigFactoryTest { private static final LbConfig VALID_WRR_CONFIG = new LbConfig("wrr_locality_experimental", ImmutableMap.of("childPolicy", ImmutableList.of( ImmutableMap.of("weighted_round_robin_experimental", - ImmutableMap.of("blackoutPeriod",287000000000.0, "enableOobLoadReport", true ))))); + ImmutableMap.of("blackoutPeriod","287s", "enableOobLoadReport", true ))))); private static final LbConfig VALID_RING_HASH_CONFIG = new LbConfig("ring_hash_experimental", ImmutableMap.of("minRingSize", (double) RING_HASH_MIN_RING_SIZE, "maxRingSize", (double) RING_HASH_MAX_RING_SIZE)); diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProviderTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProviderTest.java index 7e82a70ac7f..db72d855258 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProviderTest.java @@ -74,11 +74,11 @@ public void providesLoadBalancer() { @Test public void parseLoadBalancingConfig() throws IOException { String lbConfig = - "{\"blackoutPeriod\" : 20000000000," - + " \"weightExpirationPeriod\" : 300000000000," - + " \"oobReportingPeriod\" : 100000000000," + "{\"blackoutPeriod\" : \"20s\"," + + " \"weightExpirationPeriod\" : \"300s\"," + + " \"oobReportingPeriod\" : \"100s\"," + " \"enableOobLoadReport\" : true," - + " \"weightUpdatePeriod\" : 2000000000" + + " \"weightUpdatePeriod\" : \"2s\"" + " }"; ConfigOrError configOrError = provider.parseLoadBalancingPolicyConfig( @@ -95,7 +95,7 @@ public void parseLoadBalancingConfig() throws IOException { @Test public void parseLoadBalancingConfigDefaultValues() throws IOException { - String lbConfig = "{\"weightUpdatePeriod\" : 2000000}"; + String lbConfig = "{\"weightUpdatePeriod\" : \"0.02s\"}"; ConfigOrError configOrError = provider.parseLoadBalancingPolicyConfig( parseJsonObject(lbConfig)); @@ -108,6 +108,7 @@ public void parseLoadBalancingConfigDefaultValues() throws IOException { assertThat(config.weightUpdatePeriodNanos).isEqualTo(100_000_000L); } + @SuppressWarnings("unchecked") private static Map parseJsonObject(String json) throws IOException { return (Map) JsonParser.parse(json); diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index 2a9ce65099e..1d0d16fc579 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -209,6 +209,14 @@ public void wrrLifeCycle() { assertThat(weightedPicker.pickSubchannel(mockArgs) .getSubchannel()).isEqualTo(weightedSubchannel1); assertThat(fakeClock.getPendingTasks().size()).isEqualTo(1); + weightedConfig = WeightedRoundRobinLoadBalancerConfig.newBuilder() + .setWeightUpdatePeriodNanos(500_000_000L) //.5s + .build(); + syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() + .setAddresses(servers).setLoadBalancingPolicyConfig(weightedConfig) + .setAttributes(affinity).build())); + assertThat(fakeClock.getPendingTasks().size()).isEqualTo(1); + syncContext.execute(() -> wrr.shutdown()); for (Subchannel subchannel: subchannels.values()) { verify(subchannel).shutdown(); @@ -402,6 +410,58 @@ public void blackoutPeriod() { .isAtMost(0.001); } + @Test + public void updateWeightTimer() { + syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() + .setAddresses(servers).setLoadBalancingPolicyConfig(weightedConfig) + .setAttributes(affinity).build())); + verify(helper, times(3)).createSubchannel( + any(CreateSubchannelArgs.class)); + assertThat(fakeClock.getPendingTasks().size()).isEqualTo(1); + + Iterator it = subchannels.values().iterator(); + Subchannel readySubchannel1 = it.next(); + subchannelStateListeners.get(readySubchannel1).onSubchannelState(ConnectivityStateInfo + .forNonError(ConnectivityState.READY)); + Subchannel readySubchannel2 = it.next(); + subchannelStateListeners.get(readySubchannel2).onSubchannelState(ConnectivityStateInfo + .forNonError(ConnectivityState.READY)); + Subchannel connectingSubchannel = it.next(); + subchannelStateListeners.get(connectingSubchannel).onSubchannelState(ConnectivityStateInfo + .forNonError(ConnectivityState.CONNECTING)); + verify(helper, times(2)).updateBalancingState( + eq(ConnectivityState.READY), pickerCaptor.capture()); + assertThat(pickerCaptor.getAllValues().size()).isEqualTo(2); + assertThat(pickerCaptor.getAllValues().get(0).getList().size()).isEqualTo(1); + WeightedRoundRobinPicker weightedPicker = pickerCaptor.getAllValues().get(1); + assertThat(weightedPicker.getList().size()).isEqualTo(2); + WrrSubchannel weightedSubchannel1 = (WrrSubchannel) weightedPicker.getList().get(0); + WrrSubchannel weightedSubchannel2 = (WrrSubchannel) weightedPicker.getList().get(1); + weightedSubchannel1.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.1, 0.1, 1, new HashMap<>(), new HashMap<>())); + weightedSubchannel2.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.2, 0.1, 1, new HashMap<>(), new HashMap<>())); + assertThat(fakeClock.forwardTime(11, TimeUnit.SECONDS)).isEqualTo(1); + assertThat(weightedPicker.pickSubchannel(mockArgs) + .getSubchannel()).isEqualTo(weightedSubchannel1); + assertThat(fakeClock.getPendingTasks().size()).isEqualTo(1); + weightedConfig = WeightedRoundRobinLoadBalancerConfig.newBuilder() + .setWeightUpdatePeriodNanos(500_000_000L) //.5s + .build(); + syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() + .setAddresses(servers).setLoadBalancingPolicyConfig(weightedConfig) + .setAttributes(affinity).build())); + assertThat(fakeClock.getPendingTasks().size()).isEqualTo(1); + weightedSubchannel1.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.2, 0.1, 1, new HashMap<>(), new HashMap<>())); + weightedSubchannel2.onLoadReport(InternalCallMetricRecorder.createMetricReport( + 0.1, 0.1, 1, new HashMap<>(), new HashMap<>())); + //timer fires, new weight updated + assertThat(fakeClock.forwardTime(500, TimeUnit.MILLISECONDS)).isEqualTo(1); + assertThat(weightedPicker.pickSubchannel(mockArgs) + .getSubchannel()).isEqualTo(weightedSubchannel2); + } + @Test public void weightExpired() { syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() @@ -577,7 +637,7 @@ public void edfScheduler() { pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); } for (int i = 0; i < capacity; i++) { - assertThat(Math.abs(pickCount.get(i) / 1000.0 - weights[i] / totalWeight) ).isAtMost(0.001); + assertThat(Math.abs(pickCount.get(i) / 1000.0 - weights[i] / totalWeight) ).isAtMost(0.01); } } @@ -589,6 +649,16 @@ public void edsScheduler_sameWeight() { assertThat(scheduler.pick()).isEqualTo(0); } + @Test(expected = NullPointerException.class) + public void wrrConfig_TimeValueNonNull() { + WeightedRoundRobinLoadBalancerConfig.newBuilder().setBlackoutPeriodNanos((Long) null); + } + + @Test(expected = NullPointerException.class) + public void wrrConfig_BooleanValueNonNull() { + WeightedRoundRobinLoadBalancerConfig.newBuilder().setEnableOobLoadReport((Boolean) null); + } + private static class FakeSocketAddress extends SocketAddress { final String name; diff --git a/xds/src/test/java/io/grpc/xds/XdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/XdsClientImplDataTest.java index 3293e865842..a39ecb46fa7 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientImplDataTest.java @@ -132,6 +132,7 @@ import io.grpc.xds.VirtualHost.Route.RouteAction.HashPolicy; import io.grpc.xds.VirtualHost.Route.RouteMatch; import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher; +import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinLoadBalancerConfig; import io.grpc.xds.XdsClientImpl.ResourceInvalidException; import io.grpc.xds.XdsClusterResource.CdsUpdate; import io.grpc.xds.XdsResourceType.StructOrError; @@ -167,7 +168,6 @@ public class XdsClientImplDataTest { private boolean originalEnableRbac; private boolean originalEnableRouteLookup; private boolean originalEnableLeastRequest; - private boolean originalEnableWrr; @Before @@ -2024,6 +2024,14 @@ public void parseCluster_WrrLbPolicy_defaultLbConfig() throws ResourceInvalidExc List childConfigs = ServiceConfigUtil.unwrapLoadBalancingConfigList( JsonUtil.getListOfObjects(lbConfig.getRawConfigValue(), "childPolicy")); assertThat(childConfigs.get(0).getPolicyName()).isEqualTo("weighted_round_robin_experimental"); + WeightedRoundRobinLoadBalancerConfig result = (WeightedRoundRobinLoadBalancerConfig) + new WeightedRoundRobinLoadBalancerProvider().parseLoadBalancingPolicyConfig( + childConfigs.get(0).getRawConfigValue()).getConfig(); + assertThat(result.blackoutPeriodNanos).isEqualTo(17_000_000_000L); + assertThat(result.enableOobLoadReport).isTrue(); + assertThat(result.oobReportingPeriodNanos).isEqualTo(10_000_000_000L); + assertThat(result.weightUpdatePeriodNanos).isEqualTo(1_000_000_000L); + assertThat(result.weightExpirationPeriodNanos).isEqualTo(180_000_000_000L); } @Test From fc0d3ddb39c09bdc58f2d66a4e9125ea48e799c7 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Thu, 23 Feb 2023 23:34:03 -0800 Subject: [PATCH 23/25] bazel checksum, and use ticker --- repositories.bzl | 2 +- .../xds/WeightedRoundRobinLoadBalancer.java | 54 ++++++++++--------- ...eightedRoundRobinLoadBalancerProvider.java | 4 +- .../WeightedRoundRobinLoadBalancerTest.java | 2 +- 4 files changed, 34 insertions(+), 28 deletions(-) diff --git a/repositories.bzl b/repositories.bzl index 1c0759223dc..32454277376 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -137,7 +137,7 @@ def grpc_java_repositories(): if not native.existing_rule("envoy_api"): http_archive( name = "envoy_api", - sha256 = "a0c58442cc2038ccccad9616dd1bab5ff1e65da2bbc0ae41020ef6010119eb0e", + sha256 = "74156c0d8738d0469f23047f0fd0f8846fdd0d59d7b55c76cd8cb9ebf2fa3a01", strip_prefix = "data-plane-api-b1d2e441133c00bfe8412dfd6e93ea85e66da9bb", urls = [ "https://github.com/envoyproxy/data-plane-api/archive/b1d2e441133c00bfe8412dfd6e93ea85e66da9bb.tar.gz", diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 7a53d02809e..cddf1120a81 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -24,6 +24,7 @@ import com.google.common.base.Preconditions; import io.grpc.ConnectivityState; import io.grpc.ConnectivityStateInfo; +import io.grpc.Deadline.Ticker; import io.grpc.EquivalentAddressGroup; import io.grpc.ExperimentalApi; import io.grpc.LoadBalancer; @@ -31,7 +32,6 @@ import io.grpc.Status; import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext.ScheduledHandle; -import io.grpc.internal.TimeProvider; import io.grpc.services.MetricReport; import io.grpc.util.ForwardingLoadBalancerHelper; import io.grpc.util.ForwardingSubchannel; @@ -61,10 +61,16 @@ final class WeightedRoundRobinLoadBalancer extends RoundRobinLoadBalancer { private ScheduledHandle weightUpdateTimer; private final Runnable updateWeightTask; private final Random random; + private static long INF_TIME = Long.MAX_VALUE; - public WeightedRoundRobinLoadBalancer(Helper helper, TimeProvider timeProvider) { - super(new WrrHelper(OrcaOobUtil.newOrcaReportingHelper(helper), - checkNotNull(timeProvider, "timeProvider"))); + public WeightedRoundRobinLoadBalancer(Helper helper, Ticker ticker) { + this(new WrrHelper(OrcaOobUtil.newOrcaReportingHelper(helper), + checkNotNull(ticker, "ticker"))); + } + + public WeightedRoundRobinLoadBalancer(WrrHelper helper) { + super(helper); + helper.setLoadBalancer(this); this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext"); this.timeService = checkNotNull(helper.getScheduledExecutorService(), "timeService"); this.updateWeightTask = new UpdateWeightTask(); @@ -111,7 +117,6 @@ public void run() { private void afterAcceptAddresses() { for (Subchannel subchannel : getSubchannels()) { WrrSubchannel weightedSubchannel = (WrrSubchannel) subchannel; - weightedSubchannel.setConfig(config); if (config.enableOobLoadReport) { OrcaOobUtil.setListener(weightedSubchannel, weightedSubchannel.oobListener, OrcaOobUtil.OrcaReportingConfig.newBuilder() @@ -133,11 +138,16 @@ public void shutdown() { private static final class WrrHelper extends ForwardingLoadBalancerHelper { private final Helper delegate; - private final TimeProvider timeProvider; + private final Ticker ticker; + private WeightedRoundRobinLoadBalancer wrr; - WrrHelper(Helper helper, TimeProvider timeProvider) { + WrrHelper(Helper helper, Ticker ticker) { this.delegate = helper; - this.timeProvider = timeProvider; + this.ticker = ticker; + } + + void setLoadBalancer(WeightedRoundRobinLoadBalancer lb) { + this.wrr = lb; } @Override @@ -147,28 +157,23 @@ protected Helper delegate() { @Override public Subchannel createSubchannel(CreateSubchannelArgs args) { - return new WrrSubchannel(delegate().createSubchannel(args), timeProvider); + return wrr.new WrrSubchannel(delegate().createSubchannel(args), ticker); } } @VisibleForTesting - static final class WrrSubchannel extends ForwardingSubchannel { + final class WrrSubchannel extends ForwardingSubchannel { private final Subchannel delegate; - private final TimeProvider timeProvider; + private final Ticker ticker; private final OrcaOobReportListener oobListener = this::onLoadReport; private final OrcaPerRequestReportListener perRpcListener = this::onLoadReport; private volatile long lastUpdated; private volatile long nonEmptySince; private volatile double weight; - private WeightedRoundRobinLoadBalancerConfig config; - WrrSubchannel(Subchannel delegate, TimeProvider timeProvider) { + WrrSubchannel(Subchannel delegate, Ticker ticker) { this.delegate = checkNotNull(delegate, "delegate"); - this.timeProvider = checkNotNull(timeProvider, "timeProvider"); - } - - private void setConfig(WeightedRoundRobinLoadBalancerConfig config) { - this.config = config; + this.ticker = checkNotNull(ticker, "ticker"); } @VisibleForTesting @@ -178,10 +183,11 @@ void onLoadReport(MetricReport report) { if (newWeight == 0) { return; } - if (nonEmptySince == Integer.MAX_VALUE) { - nonEmptySince = timeProvider.currentTimeNanos(); + if (nonEmptySince == INF_TIME) { + + nonEmptySince = ticker.nanoTime(); } - lastUpdated = timeProvider.currentTimeNanos(); + lastUpdated = ticker.nanoTime(); weight = newWeight; } @@ -191,7 +197,7 @@ public void start(SubchannelStateListener listener) { @Override public void onSubchannelState(ConnectivityStateInfo newState) { if (newState.getState().equals(ConnectivityState.READY)) { - nonEmptySince = Integer.MAX_VALUE; + nonEmptySince = INF_TIME; } listener.onSubchannelState(newState); } @@ -202,9 +208,9 @@ private double getWeight() { if (config == null) { return 0; } - long now = timeProvider.currentTimeNanos(); + long now = ticker.nanoTime(); if (now - lastUpdated >= config.weightExpirationPeriodNanos) { - nonEmptySince = Integer.MAX_VALUE; + nonEmptySince = INF_TIME; return 0; } else if (now - nonEmptySince < config.blackoutPeriodNanos && config.blackoutPeriodNanos > 0) { diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java index 58f03795d46..b1d16d39049 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProvider.java @@ -17,6 +17,7 @@ package io.grpc.xds; import com.google.common.annotations.VisibleForTesting; +import io.grpc.Deadline; import io.grpc.ExperimentalApi; import io.grpc.Internal; import io.grpc.LoadBalancer; @@ -24,7 +25,6 @@ import io.grpc.LoadBalancerProvider; import io.grpc.NameResolver.ConfigOrError; import io.grpc.internal.JsonUtil; -import io.grpc.internal.TimeProvider; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinLoadBalancerConfig; import java.util.Map; @@ -42,7 +42,7 @@ public final class WeightedRoundRobinLoadBalancerProvider extends LoadBalancerPr @Override public LoadBalancer newLoadBalancer(Helper helper) { - return new WeightedRoundRobinLoadBalancer(helper, TimeProvider.SYSTEM_TIME_PROVIDER); + return new WeightedRoundRobinLoadBalancer(helper, Deadline.getSystemTicker()); } @Override diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index 1d0d16fc579..ed8540ff135 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -171,7 +171,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { return subchannel; } }); - wrr = new WeightedRoundRobinLoadBalancer(helper, fakeClock.getTimeProvider()); + wrr = new WeightedRoundRobinLoadBalancer(helper, fakeClock.getDeadlineTicker()); } @Test From f728281d389879c561cf6b7998bd79f820c1fc5b Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Fri, 24 Feb 2023 12:43:40 -0800 Subject: [PATCH 24/25] infTime = nanoTime() + MAX_VALUE --- .../io/grpc/xds/WeightedRoundRobinLoadBalancer.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index cddf1120a81..e25bf1970da 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -61,7 +61,7 @@ final class WeightedRoundRobinLoadBalancer extends RoundRobinLoadBalancer { private ScheduledHandle weightUpdateTimer; private final Runnable updateWeightTask; private final Random random; - private static long INF_TIME = Long.MAX_VALUE; + private final long infTime; public WeightedRoundRobinLoadBalancer(Helper helper, Ticker ticker) { this(new WrrHelper(OrcaOobUtil.newOrcaReportingHelper(helper), @@ -71,6 +71,7 @@ public WeightedRoundRobinLoadBalancer(Helper helper, Ticker ticker) { public WeightedRoundRobinLoadBalancer(WrrHelper helper) { super(helper); helper.setLoadBalancer(this); + this.infTime = helper.ticker.nanoTime() + Long.MAX_VALUE; this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext"); this.timeService = checkNotNull(helper.getScheduledExecutorService(), "timeService"); this.updateWeightTask = new UpdateWeightTask(); @@ -183,8 +184,7 @@ void onLoadReport(MetricReport report) { if (newWeight == 0) { return; } - if (nonEmptySince == INF_TIME) { - + if (nonEmptySince == infTime) { nonEmptySince = ticker.nanoTime(); } lastUpdated = ticker.nanoTime(); @@ -197,7 +197,7 @@ public void start(SubchannelStateListener listener) { @Override public void onSubchannelState(ConnectivityStateInfo newState) { if (newState.getState().equals(ConnectivityState.READY)) { - nonEmptySince = INF_TIME; + nonEmptySince = infTime; } listener.onSubchannelState(newState); } @@ -210,7 +210,7 @@ private double getWeight() { } long now = ticker.nanoTime(); if (now - lastUpdated >= config.weightExpirationPeriodNanos) { - nonEmptySince = INF_TIME; + nonEmptySince = infTime; return 0; } else if (now - nonEmptySince < config.blackoutPeriodNanos && config.blackoutPeriodNanos > 0) { From 1ef6221c220c908d3edfc7a27cd4bb7a82fae11e Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Mon, 27 Feb 2023 09:27:36 -0800 Subject: [PATCH 25/25] minor fix --- .../grpc/xds/LoadBalancerConfigFactory.java | 19 +++++++----- .../xds/WeightedRoundRobinLoadBalancer.java | 31 ++++++++----------- .../xds/LoadBalancerConfigFactoryTest.java | 18 +++++++++++ 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/LoadBalancerConfigFactory.java b/xds/src/main/java/io/grpc/xds/LoadBalancerConfigFactory.java index f0c3cb751de..4b919a4e6ff 100644 --- a/xds/src/main/java/io/grpc/xds/LoadBalancerConfigFactory.java +++ b/xds/src/main/java/io/grpc/xds/LoadBalancerConfigFactory.java @@ -265,13 +265,18 @@ static class LoadBalancingPolicyConverter { private static ImmutableMap convertWeightedRoundRobinConfig( ClientSideWeightedRoundRobin wrr) throws ResourceInvalidException { - return buildWrrConfig( - wrr.hasBlackoutPeriod() ? Durations.toString(wrr.getBlackoutPeriod()) : null, - wrr.hasWeightExpirationPeriod() - ? Durations.toString(wrr.getWeightExpirationPeriod()) : null, - wrr.hasOobReportingPeriod() ? Durations.toString(wrr.getOobReportingPeriod()) : null, - wrr.hasEnableOobLoadReport() ? wrr.getEnableOobLoadReport().getValue() : null, - wrr.hasWeightUpdatePeriod() ? Durations.toString(wrr.getWeightUpdatePeriod()) : null); + try { + return buildWrrConfig( + wrr.hasBlackoutPeriod() ? Durations.toString(wrr.getBlackoutPeriod()) : null, + wrr.hasWeightExpirationPeriod() + ? Durations.toString(wrr.getWeightExpirationPeriod()) : null, + wrr.hasOobReportingPeriod() ? Durations.toString(wrr.getOobReportingPeriod()) : null, + wrr.hasEnableOobLoadReport() ? wrr.getEnableOobLoadReport().getValue() : null, + wrr.hasWeightUpdatePeriod() ? Durations.toString(wrr.getWeightUpdatePeriod()) : null); + } catch (IllegalArgumentException ex) { + throw new ResourceInvalidException("Invalid duration in weighted round robin config: " + + ex.getMessage()); + } } /** diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index e25bf1970da..60804fec7b1 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -46,7 +46,6 @@ import java.util.Random; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; /** * A {@link LoadBalancer} that provides weighted-round-robin load-balancing over @@ -62,16 +61,17 @@ final class WeightedRoundRobinLoadBalancer extends RoundRobinLoadBalancer { private final Runnable updateWeightTask; private final Random random; private final long infTime; + private final Ticker ticker; public WeightedRoundRobinLoadBalancer(Helper helper, Ticker ticker) { - this(new WrrHelper(OrcaOobUtil.newOrcaReportingHelper(helper), - checkNotNull(ticker, "ticker"))); + this(new WrrHelper(OrcaOobUtil.newOrcaReportingHelper(helper)), ticker); } - public WeightedRoundRobinLoadBalancer(WrrHelper helper) { + public WeightedRoundRobinLoadBalancer(WrrHelper helper, Ticker ticker) { super(helper); helper.setLoadBalancer(this); - this.infTime = helper.ticker.nanoTime() + Long.MAX_VALUE; + this.ticker = checkNotNull(ticker, "ticker"); + this.infTime = ticker.nanoTime() + Long.MAX_VALUE; this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext"); this.timeService = checkNotNull(helper.getScheduledExecutorService(), "timeService"); this.updateWeightTask = new UpdateWeightTask(); @@ -139,12 +139,10 @@ public void shutdown() { private static final class WrrHelper extends ForwardingLoadBalancerHelper { private final Helper delegate; - private final Ticker ticker; private WeightedRoundRobinLoadBalancer wrr; - WrrHelper(Helper helper, Ticker ticker) { + WrrHelper(Helper helper) { this.delegate = helper; - this.ticker = ticker; } void setLoadBalancer(WeightedRoundRobinLoadBalancer lb) { @@ -158,23 +156,21 @@ protected Helper delegate() { @Override public Subchannel createSubchannel(CreateSubchannelArgs args) { - return wrr.new WrrSubchannel(delegate().createSubchannel(args), ticker); + return wrr.new WrrSubchannel(delegate().createSubchannel(args)); } } @VisibleForTesting final class WrrSubchannel extends ForwardingSubchannel { private final Subchannel delegate; - private final Ticker ticker; private final OrcaOobReportListener oobListener = this::onLoadReport; private final OrcaPerRequestReportListener perRpcListener = this::onLoadReport; private volatile long lastUpdated; private volatile long nonEmptySince; private volatile double weight; - WrrSubchannel(Subchannel delegate, Ticker ticker) { + WrrSubchannel(Subchannel delegate) { this.delegate = checkNotNull(delegate, "delegate"); - this.ticker = checkNotNull(ticker, "ticker"); } @VisibleForTesting @@ -229,14 +225,13 @@ protected Subchannel delegate() { @VisibleForTesting final class WeightedRoundRobinPicker extends ReadyPicker { private final List list; - private final AtomicReference schedulerRef; - private volatile boolean rrMode = true; + private volatile EdfScheduler scheduler; + private volatile boolean rrMode; WeightedRoundRobinPicker(List list, int startIndex) { super(checkNotNull(list, "list"), startIndex); Preconditions.checkArgument(!list.isEmpty(), "empty list"); this.list = list; - this.schedulerRef = new AtomicReference<>(null); updateWeight(); } @@ -245,7 +240,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { if (rrMode) { return super.pickSubchannel(args); } - int pickIndex = schedulerRef.get().pick(); + int pickIndex = scheduler.pick(); WrrSubchannel subchannel = (WrrSubchannel) list.get(pickIndex); if (!config.enableOobLoadReport) { return PickResult.withSubchannel( @@ -278,7 +273,7 @@ private void updateWeight() { double newWeight = subchannel.getWeight(); scheduler.add(i, newWeight > 0 ? newWeight : avgWeight); } - schedulerRef.set(scheduler); + this.scheduler = scheduler; rrMode = false; } @@ -360,7 +355,7 @@ static final class EdfScheduler { EdfScheduler(int initialCapacity) { this.prioQueue = new PriorityQueue(initialCapacity, (o1, o2) -> { if (o1.deadline == o2.deadline) { - return Double.compare(o1.index, o2.index); + return Integer.compare(o1.index, o2.index); } else { return Double.compare(o1.deadline, o2.deadline); } diff --git a/xds/src/test/java/io/grpc/xds/LoadBalancerConfigFactoryTest.java b/xds/src/test/java/io/grpc/xds/LoadBalancerConfigFactoryTest.java index d18d665d317..884f04b2f22 100644 --- a/xds/src/test/java/io/grpc/xds/LoadBalancerConfigFactoryTest.java +++ b/xds/src/test/java/io/grpc/xds/LoadBalancerConfigFactoryTest.java @@ -152,6 +152,24 @@ public void weightedRoundRobin() throws ResourceInvalidException { assertThat(newLbConfig(cluster, true, true, true)).isEqualTo(VALID_WRR_CONFIG); } + @Test + public void weightedRoundRobin_invalid() throws ResourceInvalidException { + Cluster cluster = newCluster(buildWrrPolicy(Policy.newBuilder() + .setTypedExtensionConfig(TypedExtensionConfig.newBuilder() + .setName("backend") + .setTypedConfig( + Any.pack(ClientSideWeightedRoundRobin.newBuilder() + .setBlackoutPeriod(Duration.newBuilder().setNanos(1000000000).build()) + .setEnableOobLoadReport( + BoolValue.newBuilder().setValue(true).build()) + .build())) + .build()) + .build())); + + assertResourceInvalidExceptionThrown(cluster, true, true, true, + "Invalid duration in weighted round robin config"); + } + @Test public void weightedRoundRobin_fallback_roundrobin() throws ResourceInvalidException { Cluster cluster = newCluster(buildWrrPolicy(WRR_POLICY, ROUND_ROBIN_POLICY));