Skip to content

Commit dca89b2

Browse files
authored
util: Pass an AtomicInteger to RR's ReadyPicker
We already do this for WRR. Notably, we are no longer trying to avoid the modulus each pick. It was of questionable value, and removing it is necessary to continue sharing the same integer when the list size changes. The change means we can implement a stronger isEquivalentTo() by comparing the AtomicInteger references. It is strong enough that the operation aligns with normal equals(). Using equals() instead of isEquivalentTo() also made more obvious an equals() optimization that uses the hashCode() before the more expensive HashSet creation; equals() should now be very fast except when they are (very likely) equal.
1 parent 43e0637 commit dca89b2

File tree

3 files changed

+103
-110
lines changed

3 files changed

+103
-110
lines changed

util/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java

Lines changed: 47 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -24,44 +24,38 @@
2424

2525
import com.google.common.annotations.VisibleForTesting;
2626
import com.google.common.base.MoreObjects;
27-
import com.google.common.base.Objects;
2827
import com.google.common.base.Preconditions;
2928
import io.grpc.ConnectivityState;
3029
import io.grpc.EquivalentAddressGroup;
3130
import io.grpc.Internal;
3231
import io.grpc.LoadBalancer;
3332
import io.grpc.NameResolver;
34-
import io.grpc.Status;
3533
import java.util.ArrayList;
3634
import java.util.Collection;
3735
import java.util.HashSet;
3836
import java.util.List;
3937
import java.util.Map;
4038
import java.util.Random;
41-
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
42-
import javax.annotation.Nonnull;
39+
import java.util.concurrent.atomic.AtomicInteger;
4340

4441
/**
4542
* A {@link LoadBalancer} that provides round-robin load-balancing over the {@link
4643
* EquivalentAddressGroup}s from the {@link NameResolver}.
4744
*/
4845
@Internal
4946
public class RoundRobinLoadBalancer extends MultiChildLoadBalancer {
50-
private final Random random;
51-
protected RoundRobinPicker currentPicker = new EmptyPicker(EMPTY_OK);
47+
private final AtomicInteger sequence = new AtomicInteger(new Random().nextInt());
48+
protected SubchannelPicker currentPicker = new EmptyPicker();
5249

5350
public RoundRobinLoadBalancer(Helper helper) {
5451
super(helper);
55-
this.random = new Random();
5652
}
5753

5854
@Override
5955
protected SubchannelPicker getSubchannelPicker(Map<Object, SubchannelPicker> childPickers) {
6056
throw new UnsupportedOperationException(); // local updateOverallBalancingState doesn't use this
6157
}
6258

63-
private static final Status EMPTY_OK = Status.OK.withDescription("no subchannels ready");
64-
6559
/**
6660
* Updates picker with the list of active subchannels (state == READY).
6761
*/
@@ -82,7 +76,7 @@ protected void updateOverallBalancingState() {
8276
}
8377

8478
if (isConnecting) {
85-
updateBalancingState(CONNECTING, new EmptyPicker(Status.OK));
79+
updateBalancingState(CONNECTING, new EmptyPicker());
8680
} else {
8781
updateBalancingState(TRANSIENT_FAILURE, createReadyPicker(getChildLbStates()));
8882
}
@@ -91,45 +85,45 @@ protected void updateOverallBalancingState() {
9185
}
9286
}
9387

94-
private void updateBalancingState(ConnectivityState state, RoundRobinPicker picker) {
95-
if (state != currentConnectivityState || !picker.isEquivalentTo(currentPicker)) {
88+
private void updateBalancingState(ConnectivityState state, SubchannelPicker picker) {
89+
if (state != currentConnectivityState || !picker.equals(currentPicker)) {
9690
getHelper().updateBalancingState(state, picker);
9791
currentConnectivityState = state;
9892
currentPicker = picker;
9993
}
10094
}
10195

102-
protected RoundRobinPicker createReadyPicker(Collection<ChildLbState> children) {
103-
// initialize the Picker to a random start index to ensure that a high frequency of Picker
104-
// churn does not skew subchannel selection.
105-
int startIndex = random.nextInt(children.size());
106-
96+
protected SubchannelPicker createReadyPicker(Collection<ChildLbState> children) {
10797
List<SubchannelPicker> pickerList = new ArrayList<>();
10898
for (ChildLbState child : children) {
10999
SubchannelPicker picker = child.getCurrentPicker();
110100
pickerList.add(picker);
111101
}
112102

113-
return new ReadyPicker(pickerList, startIndex);
114-
}
115-
116-
public abstract static class RoundRobinPicker extends SubchannelPicker {
117-
public abstract boolean isEquivalentTo(RoundRobinPicker picker);
103+
return new ReadyPicker(pickerList, sequence);
118104
}
119105

120106
@VisibleForTesting
121-
static class ReadyPicker extends RoundRobinPicker {
122-
private static final AtomicIntegerFieldUpdater<ReadyPicker> indexUpdater =
123-
AtomicIntegerFieldUpdater.newUpdater(ReadyPicker.class, "index");
124-
107+
static class ReadyPicker extends SubchannelPicker {
125108
private final List<SubchannelPicker> subchannelPickers; // non-empty
126-
@SuppressWarnings("unused")
127-
private volatile int index;
109+
private final AtomicInteger index;
110+
private final int hashCode;
128111

129-
public ReadyPicker(List<SubchannelPicker> list, int startIndex) {
112+
public ReadyPicker(List<SubchannelPicker> list, AtomicInteger index) {
130113
checkArgument(!list.isEmpty(), "empty list");
131114
this.subchannelPickers = list;
132-
this.index = startIndex - 1;
115+
this.index = Preconditions.checkNotNull(index, "index");
116+
117+
// Every created picker is checked for equality in updateBalancingState() at least once.
118+
// Pre-compute the hash so it can be checked cheaply. Using the hash in equals() makes it very
119+
// fast except when the pickers are (very likely) equal.
120+
//
121+
// For equality we treat children as a set; use hash code as defined by Set
122+
int sum = 0;
123+
for (SubchannelPicker picker : subchannelPickers) {
124+
sum += picker.hashCode();
125+
}
126+
this.hashCode = sum;
133127
}
134128

135129
@Override
@@ -145,14 +139,8 @@ public String toString() {
145139
}
146140

147141
private int nextIndex() {
148-
int size = subchannelPickers.size();
149-
int i = indexUpdater.incrementAndGet(this);
150-
if (i >= size) {
151-
int oldi = i;
152-
i %= size;
153-
indexUpdater.compareAndSet(this, oldi, i);
154-
}
155-
return i;
142+
int i = index.getAndIncrement() & Integer.MAX_VALUE;
143+
return i % subchannelPickers.size();
156144
}
157145

158146
@VisibleForTesting
@@ -161,53 +149,42 @@ List<SubchannelPicker> getSubchannelPickers() {
161149
}
162150

163151
@Override
164-
public boolean isEquivalentTo(RoundRobinPicker picker) {
165-
if (!(picker instanceof ReadyPicker)) {
152+
public int hashCode() {
153+
return hashCode;
154+
}
155+
156+
@Override
157+
public boolean equals(Object o) {
158+
if (!(o instanceof ReadyPicker)) {
166159
return false;
167160
}
168-
ReadyPicker other = (ReadyPicker) picker;
161+
ReadyPicker other = (ReadyPicker) o;
162+
if (other == this) {
163+
return true;
164+
}
169165
// the lists cannot contain duplicate subchannels
170-
return other == this
171-
|| (subchannelPickers.size() == other.subchannelPickers.size() && new HashSet<>(
172-
subchannelPickers).containsAll(other.subchannelPickers));
166+
return hashCode == other.hashCode
167+
&& index == other.index
168+
&& subchannelPickers.size() == other.subchannelPickers.size()
169+
&& new HashSet<>(subchannelPickers).containsAll(other.subchannelPickers);
173170
}
174171
}
175172

176173
@VisibleForTesting
177-
static final class EmptyPicker extends RoundRobinPicker {
178-
179-
private final Status status;
180-
181-
EmptyPicker(@Nonnull Status status) {
182-
this.status = Preconditions.checkNotNull(status, "status");
183-
}
184-
174+
static final class EmptyPicker extends SubchannelPicker {
185175
@Override
186176
public PickResult pickSubchannel(PickSubchannelArgs args) {
187-
return status.isOk() ? PickResult.withNoResult() : PickResult.withError(status);
177+
return PickResult.withNoResult();
188178
}
189179

190180
@Override
191-
public boolean isEquivalentTo(RoundRobinPicker picker) {
192-
return picker instanceof EmptyPicker && (Objects.equal(status, ((EmptyPicker) picker).status)
193-
|| (status.isOk() && ((EmptyPicker) picker).status.isOk()));
181+
public int hashCode() {
182+
return getClass().hashCode();
194183
}
195184

196185
@Override
197-
public String toString() {
198-
return MoreObjects.toStringHelper(EmptyPicker.class).add("status", status).toString();
199-
}
200-
}
201-
202-
/**
203-
* A lighter weight Reference than AtomicReference.
204-
*/
205-
@VisibleForTesting
206-
static final class Ref<T> {
207-
T value;
208-
209-
Ref(T value) {
210-
this.value = value;
186+
public boolean equals(Object o) {
187+
return o instanceof EmptyPicker;
211188
}
212189
}
213190
}

util/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@
2323
import static io.grpc.ConnectivityState.SHUTDOWN;
2424
import static io.grpc.ConnectivityState.TRANSIENT_FAILURE;
2525
import static org.junit.Assert.assertEquals;
26-
import static org.junit.Assert.assertFalse;
2726
import static org.junit.Assert.assertNull;
28-
import static org.junit.Assert.assertTrue;
2927
import static org.junit.Assert.fail;
3028
import static org.mockito.AdditionalAnswers.delegatesTo;
3129
import static org.mockito.ArgumentMatchers.any;
@@ -64,6 +62,7 @@
6462
import java.util.List;
6563
import java.util.Map;
6664
import java.util.concurrent.ConcurrentHashMap;
65+
import java.util.concurrent.atomic.AtomicInteger;
6766
import org.junit.After;
6867
import org.junit.Before;
6968
import org.junit.Rule;
@@ -376,22 +375,20 @@ public void pickerRoundRobin() throws Exception {
376375
TestUtils.pickerOf(subchannel), TestUtils.pickerOf(subchannel1),
377376
TestUtils.pickerOf(subchannel2));
378377

379-
ReadyPicker picker = new ReadyPicker(Collections.unmodifiableList(pickers),
380-
0 /* startIndex */);
378+
AtomicInteger seq = new AtomicInteger(0);
379+
ReadyPicker picker = new ReadyPicker(Collections.unmodifiableList(pickers), seq);
381380

382381
assertEquals(subchannel, picker.pickSubchannel(mockArgs).getSubchannel());
383382
assertEquals(subchannel1, picker.pickSubchannel(mockArgs).getSubchannel());
384383
assertEquals(subchannel2, picker.pickSubchannel(mockArgs).getSubchannel());
385384
assertEquals(subchannel, picker.pickSubchannel(mockArgs).getSubchannel());
386-
}
387-
388-
@Test
389-
public void pickerEmptyList() throws Exception {
390-
SubchannelPicker picker = new EmptyPicker(Status.UNKNOWN);
391385

392-
assertNull(picker.pickSubchannel(mockArgs).getSubchannel());
393-
assertEquals(Status.UNKNOWN,
394-
picker.pickSubchannel(mockArgs).getStatus());
386+
seq.set(Integer.MAX_VALUE);
387+
assertEquals(subchannel1, picker.pickSubchannel(mockArgs).getSubchannel());
388+
assertEquals(subchannel, picker.pickSubchannel(mockArgs).getSubchannel());
389+
assertEquals(subchannel1, picker.pickSubchannel(mockArgs).getSubchannel());
390+
assertEquals(subchannel2, picker.pickSubchannel(mockArgs).getSubchannel());
391+
assertEquals(subchannel, picker.pickSubchannel(mockArgs).getSubchannel());
395392
}
396393

397394
@Test
@@ -481,36 +478,35 @@ public void subchannelStateIsolation() throws Exception {
481478
public void readyPicker_emptyList() {
482479
// ready picker list must be non-empty
483480
try {
484-
new ReadyPicker(Collections.emptyList(), 0);
481+
new ReadyPicker(Collections.emptyList(), new AtomicInteger(0));
485482
fail();
486483
} catch (IllegalArgumentException expected) {
487484
}
488485
}
489486

490487
@Test
491488
public void internalPickerComparisons() {
492-
EmptyPicker emptyOk1 = new EmptyPicker(Status.OK);
493-
EmptyPicker emptyOk2 = new EmptyPicker(Status.OK.withDescription("different OK"));
494-
EmptyPicker emptyErr = new EmptyPicker(Status.UNKNOWN.withDescription(\\_(ツ)_//¯"));
489+
SubchannelPicker empty1 = new EmptyPicker();
490+
SubchannelPicker empty2 = new EmptyPicker();
495491

492+
AtomicInteger seq = new AtomicInteger(0);
496493
acceptAddresses(servers, Attributes.EMPTY); // create subchannels
497494
Iterator<Subchannel> subchannelIterator = subchannels.values().iterator();
498495
SubchannelPicker sc1 = TestUtils.pickerOf(subchannelIterator.next());
499496
SubchannelPicker sc2 = TestUtils.pickerOf(subchannelIterator.next());
500-
ReadyPicker ready1 = new ReadyPicker(Arrays.asList(sc1, sc2), 0);
501-
ReadyPicker ready2 = new ReadyPicker(Arrays.asList(sc1), 0);
502-
ReadyPicker ready3 = new ReadyPicker(Arrays.asList(sc2, sc1), 1);
503-
ReadyPicker ready4 = new ReadyPicker(Arrays.asList(sc1, sc2), 1);
504-
ReadyPicker ready5 = new ReadyPicker(Arrays.asList(sc2, sc1), 0);
505-
506-
assertTrue(emptyOk1.isEquivalentTo(emptyOk2));
507-
assertFalse(emptyOk1.isEquivalentTo(emptyErr));
508-
assertFalse(ready1.isEquivalentTo(ready2));
509-
assertTrue(ready1.isEquivalentTo(ready3));
510-
assertTrue(ready3.isEquivalentTo(ready4));
511-
assertTrue(ready4.isEquivalentTo(ready5));
512-
assertFalse(emptyOk1.isEquivalentTo(ready1));
513-
assertFalse(ready1.isEquivalentTo(emptyOk1));
497+
SubchannelPicker ready1 = new ReadyPicker(Arrays.asList(sc1, sc2), seq);
498+
SubchannelPicker ready2 = new ReadyPicker(Arrays.asList(sc1), seq);
499+
SubchannelPicker ready3 = new ReadyPicker(Arrays.asList(sc2, sc1), seq);
500+
SubchannelPicker ready4 = new ReadyPicker(Arrays.asList(sc1, sc2), seq);
501+
SubchannelPicker ready5 = new ReadyPicker(Arrays.asList(sc2, sc1), new AtomicInteger(0));
502+
503+
assertThat(empty1).isEqualTo(empty2);
504+
assertThat(ready1).isNotEqualTo(ready2);
505+
assertThat(ready1).isEqualTo(ready3);
506+
assertThat(ready3).isEqualTo(ready4);
507+
assertThat(ready4).isNotEqualTo(ready5);
508+
assertThat(empty1).isNotEqualTo(ready1);
509+
assertThat(ready1).isNotEqualTo(empty1);
514510
}
515511

516512
@Test

0 commit comments

Comments
 (0)