xds: Replace isEquivalentTo with equals in LeastRequest

This is similar to the changes to round robin in dca89b25.
This commit is contained in:
Eric Anderson 2024-02-09 22:40:04 -08:00
parent 7e72413233
commit 7787673992
2 changed files with 60 additions and 87 deletions

View File

@ -28,8 +28,6 @@ import static io.grpc.xds.LeastRequestLoadBalancerProvider.MIN_CHOICE_COUNT;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects; 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.Attributes;
import io.grpc.ClientStreamTracer; import io.grpc.ClientStreamTracer;
import io.grpc.ClientStreamTracer.StreamInfo; import io.grpc.ClientStreamTracer.StreamInfo;
@ -46,7 +44,6 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nonnull;
/** /**
* A {@link LoadBalancer} that provides least request load balancing based on * A {@link LoadBalancer} that provides least request load balancing based on
@ -57,12 +54,9 @@ import javax.annotation.Nonnull;
* the "power of two choices" (P2C). * the "power of two choices" (P2C).
*/ */
final class LeastRequestLoadBalancer extends MultiChildLoadBalancer { final class LeastRequestLoadBalancer extends MultiChildLoadBalancer {
private static final Status EMPTY_OK = Status.OK.withDescription("no subchannels ready");
private static final EmptyPicker EMPTY_LR_PICKER = new EmptyPicker(EMPTY_OK);
private final ThreadSafeRandom random; private final ThreadSafeRandom random;
private LeastRequestPicker currentPicker = EMPTY_LR_PICKER; private SubchannelPicker currentPicker = new EmptyPicker();
private int choiceCount = DEFAULT_CHOICE_COUNT; private int choiceCount = DEFAULT_CHOICE_COUNT;
LeastRequestLoadBalancer(Helper helper) { LeastRequestLoadBalancer(Helper helper) {
@ -101,11 +95,6 @@ final class LeastRequestLoadBalancer extends MultiChildLoadBalancer {
return addressAcceptanceStatus; return addressAcceptanceStatus;
} }
@Override
protected SubchannelPicker getErrorPicker(Status error) {
return new EmptyPicker(error);
}
/** /**
* Updates picker with the list of active subchannels (state == READY). * Updates picker with the list of active subchannels (state == READY).
* *
@ -132,7 +121,7 @@ final class LeastRequestLoadBalancer extends MultiChildLoadBalancer {
} }
} }
if (isConnecting) { if (isConnecting) {
updateBalancingState(CONNECTING, EMPTY_LR_PICKER); updateBalancingState(CONNECTING, new EmptyPicker());
} else { } else {
// Give it all the failing children and let it randomly pick among them // Give it all the failing children and let it randomly pick among them
updateBalancingState(TRANSIENT_FAILURE, updateBalancingState(TRANSIENT_FAILURE,
@ -149,8 +138,8 @@ final class LeastRequestLoadBalancer extends MultiChildLoadBalancer {
return new LeastRequestLbState(key, pickFirstLbProvider, policyConfig, initialPicker); return new LeastRequestLbState(key, pickFirstLbProvider, policyConfig, initialPicker);
} }
private void updateBalancingState(ConnectivityState state, LeastRequestPicker picker) { private void updateBalancingState(ConnectivityState state, SubchannelPicker picker) {
if (state != currentConnectivityState || !picker.isEquivalentTo(currentPicker)) { if (state != currentConnectivityState || !picker.equals(currentPicker)) {
getHelper().updateBalancingState(state, picker); getHelper().updateBalancingState(state, picker);
currentConnectivityState = state; currentConnectivityState = state;
currentPicker = picker; currentPicker = picker;
@ -183,21 +172,23 @@ final class LeastRequestLoadBalancer extends MultiChildLoadBalancer {
} }
@VisibleForTesting @VisibleForTesting
abstract static class LeastRequestPicker extends SubchannelPicker { static final class ReadyPicker extends SubchannelPicker {
abstract boolean isEquivalentTo(LeastRequestPicker picker);
}
@VisibleForTesting
static final class ReadyPicker extends LeastRequestPicker {
private final List<ChildLbState> childLbStates; // non-empty private final List<ChildLbState> childLbStates; // non-empty
private final int choiceCount; private final int choiceCount;
private final ThreadSafeRandom random; private final ThreadSafeRandom random;
private final int hashCode;
ReadyPicker(List<ChildLbState> childLbStates, int choiceCount, ThreadSafeRandom random) { ReadyPicker(List<ChildLbState> childLbStates, int choiceCount, ThreadSafeRandom random) {
checkArgument(!childLbStates.isEmpty(), "empty list"); checkArgument(!childLbStates.isEmpty(), "empty list");
this.childLbStates = childLbStates; this.childLbStates = childLbStates;
this.choiceCount = choiceCount; this.choiceCount = choiceCount;
this.random = checkNotNull(random, "random"); this.random = checkNotNull(random, "random");
int sum = 0;
for (ChildLbState child : childLbStates) {
sum += child.hashCode();
}
this.hashCode = sum ^ choiceCount;
} }
@Override @Override
@ -244,49 +235,48 @@ final class LeastRequestLoadBalancer extends MultiChildLoadBalancer {
return childLbStates; return childLbStates;
} }
@VisibleForTesting
@Override @Override
boolean isEquivalentTo(LeastRequestPicker picker) { public int hashCode() {
if (!(picker instanceof ReadyPicker)) { return hashCode;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ReadyPicker)) {
return false; return false;
} }
ReadyPicker other = (ReadyPicker) picker; ReadyPicker other = (ReadyPicker) o;
// the lists cannot contain duplicate subchannels if (other == this) {
return other == this return true;
|| ((childLbStates.size() == other.childLbStates.size() && new HashSet<>( }
childLbStates).containsAll(other.childLbStates)) // the lists cannot contain duplicate children
&& choiceCount == other.choiceCount); return hashCode == other.hashCode
&& choiceCount == other.choiceCount
&& childLbStates.size() == other.childLbStates.size()
&& new HashSet<>(childLbStates).containsAll(other.childLbStates);
} }
} }
@VisibleForTesting @VisibleForTesting
static final class EmptyPicker extends LeastRequestPicker { static final class EmptyPicker extends SubchannelPicker {
private final Status status;
EmptyPicker(@Nonnull Status status) {
this.status = Preconditions.checkNotNull(status, "status");
}
@Override @Override
public PickResult pickSubchannel(PickSubchannelArgs args) { public PickResult pickSubchannel(PickSubchannelArgs args) {
return status.isOk() ? PickResult.withNoResult() : PickResult.withError(status); return PickResult.withNoResult();
} }
@Override @Override
boolean isEquivalentTo(LeastRequestPicker picker) { public int hashCode() {
return picker instanceof EmptyPicker && (Objects.equal(status, ((EmptyPicker) picker).status) return getClass().hashCode();
|| (status.isOk() && ((EmptyPicker) picker).status.isOk())); }
@Override
public boolean equals(Object o) {
return o instanceof EmptyPicker;
} }
@Override @Override
public String toString() { public String toString() {
return MoreObjects.toStringHelper(EmptyPicker.class).add("status", status).toString(); return MoreObjects.toStringHelper(EmptyPicker.class).toString();
}
@VisibleForTesting
Status getStatus() {
return status;
} }
} }

View File

@ -63,7 +63,6 @@ import io.grpc.util.MultiChildLoadBalancer.ChildLbState;
import io.grpc.xds.LeastRequestLoadBalancer.EmptyPicker; import io.grpc.xds.LeastRequestLoadBalancer.EmptyPicker;
import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestConfig; import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestConfig;
import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestLbState; import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestLbState;
import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestPicker;
import io.grpc.xds.LeastRequestLoadBalancer.ReadyPicker; import io.grpc.xds.LeastRequestLoadBalancer.ReadyPicker;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.util.ArrayList; import java.util.ArrayList;
@ -365,7 +364,7 @@ public class LeastRequestLoadBalancerTest {
} }
inOrder.verify(helper) inOrder.verify(helper)
.updateBalancingState(eq(TRANSIENT_FAILURE), pickerCaptor.capture()); .updateBalancingState(eq(TRANSIENT_FAILURE), pickerCaptor.capture());
assertThat(getStatusString((LeastRequestPicker)pickerCaptor.getValue())) assertThat(getStatusString(pickerCaptor.getValue()))
.contains("Status{code=UNKNOWN, description=connection broken"); .contains("Status{code=UNKNOWN, description=connection broken");
inOrder.verifyNoMoreInteractions(); inOrder.verifyNoMoreInteractions();
@ -379,38 +378,25 @@ public class LeastRequestLoadBalancerTest {
verifyNoMoreInteractions(helper); verifyNoMoreInteractions(helper);
} }
private String getStatusString(LeastRequestPicker picker) { private String getStatusString(SubchannelPicker picker) {
if (picker == null) { if (picker == null) {
return ""; return "";
} }
if (picker instanceof EmptyPicker) { if (picker instanceof ReadyPicker) {
if (((EmptyPicker) picker).getStatus() == null) {
return "";
}
return ((EmptyPicker) picker).getStatus().toString();
} else if (picker instanceof ReadyPicker) {
List<ChildLbState> childLbStates = ((ReadyPicker)picker).getChildLbStates(); List<ChildLbState> childLbStates = ((ReadyPicker)picker).getChildLbStates();
if (childLbStates == null || childLbStates.isEmpty()) { if (childLbStates == null || childLbStates.isEmpty()) {
return ""; return "";
};
// Note that this is dependent on PickFirst's picker toString retaining the representation
// of the status, but since it is a test and we don't want to expose this value it seems
// a reasonable tradeoff
String pickerStr = childLbStates.get(0).getCurrentPicker().toString();
int beg = pickerStr.indexOf(", status=Status{");
if (beg < 0) {
return "";
}
int end = pickerStr.indexOf('}', beg);
if (end < 0) {
return "";
}
return pickerStr.substring(beg + ", status=".length(), end + 1);
} }
throw new IllegalArgumentException("Unrecognized picker: " + picker); picker = childLbStates.get(0).getCurrentPicker();
}
Status status = picker.pickSubchannel(mockArgs).getStatus();
if (status == null) {
return "";
}
return status.toString();
} }
@Test @Test
@ -508,11 +494,10 @@ public class LeastRequestLoadBalancerTest {
@Test @Test
public void pickerEmptyList() throws Exception { public void pickerEmptyList() throws Exception {
SubchannelPicker picker = new EmptyPicker(Status.UNKNOWN); SubchannelPicker picker = new EmptyPicker();
assertNull(picker.pickSubchannel(mockArgs).getSubchannel()); assertNull(picker.pickSubchannel(mockArgs).getSubchannel());
assertEquals(Status.UNKNOWN, assertEquals(Status.OK, picker.pickSubchannel(mockArgs).getStatus());
picker.pickSubchannel(mockArgs).getStatus());
} }
@Test @Test
@ -623,9 +608,8 @@ public class LeastRequestLoadBalancerTest {
@Test @Test
public void internalPickerComparisons() { public void internalPickerComparisons() {
EmptyPicker emptyOk1 = new EmptyPicker(Status.OK); EmptyPicker empty1 = new EmptyPicker();
EmptyPicker emptyOk2 = new EmptyPicker(Status.OK.withDescription("different OK")); EmptyPicker empty2 = new EmptyPicker();
EmptyPicker emptyErr = new EmptyPicker(Status.UNKNOWN.withDescription("¯\\_(ツ)_//¯"));
loadBalancer.acceptResolvedAddresses( loadBalancer.acceptResolvedAddresses(
ResolvedAddresses.newBuilder().setAddresses(servers).setAttributes(affinity).build()); ResolvedAddresses.newBuilder().setAddresses(servers).setAttributes(affinity).build());
@ -641,15 +625,14 @@ public class LeastRequestLoadBalancerTest {
ReadyPicker ready5 = new ReadyPicker(Arrays.asList(child2, child1), 2, mockRandom); ReadyPicker ready5 = new ReadyPicker(Arrays.asList(child2, child1), 2, mockRandom);
ReadyPicker ready6 = new ReadyPicker(Arrays.asList(child2, child1), 8, mockRandom); ReadyPicker ready6 = new ReadyPicker(Arrays.asList(child2, child1), 8, mockRandom);
assertTrue(emptyOk1.isEquivalentTo(emptyOk2)); assertTrue(empty1.equals(empty2));
assertFalse(emptyOk1.isEquivalentTo(emptyErr)); assertFalse(ready1.equals(ready2));
assertFalse(ready1.isEquivalentTo(ready2)); assertTrue(ready1.equals(ready3));
assertTrue(ready1.isEquivalentTo(ready3)); assertTrue(ready3.equals(ready4));
assertTrue(ready3.isEquivalentTo(ready4)); assertTrue(ready4.equals(ready5));
assertTrue(ready4.isEquivalentTo(ready5)); assertFalse(empty1.equals(ready1));
assertFalse(emptyOk1.isEquivalentTo(ready1)); assertFalse(ready1.equals(empty1));
assertFalse(ready1.isEquivalentTo(emptyOk1)); assertFalse(ready5.equals(ready6));
assertFalse(ready5.isEquivalentTo(ready6));
} }
@Test @Test