Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.net.InetSocketAddress;
import java.time.Duration;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
Expand Down Expand Up @@ -97,10 +98,10 @@ public List<InetSocketAddress> rotate(String host, List<InetSocketAddress> resol

// Fast path: nothing failed recently, so the order is the plain round-robin rotation.
if (state.cooldowns.isEmpty()) {
return index == 0 ? resolved : rotateBy(resolved, index, n);
return index == 0 ? resolved : rotateBy(resolved, index);
}

List<InetSocketAddress> rotated = index == 0 ? resolved : rotateBy(resolved, index, n);
List<InetSocketAddress> rotated = index == 0 ? resolved : rotateBy(resolved, index);
return deprioritizeCooling(state, rotated);
}

Expand All @@ -127,11 +128,41 @@ private HostState stateFor(String host) {
return hosts.computeIfAbsent(host, h -> new HostState());
}

private static List<InetSocketAddress> rotateBy(List<InetSocketAddress> resolved, int index, int n) {
List<InetSocketAddress> rotated = new ArrayList<>(n);
rotated.addAll(resolved.subList(index, n));
rotated.addAll(resolved.subList(0, index));
return rotated;
// Returns a read-only view of {@code resolved} rotated left by {@code index} — element i is
// resolved.get((index + i) mod size) — instead of copying the rotation into a fresh ArrayList (plus two
// subList views), which the round-robin fast path did on ~(n-1)/n of multi-IP requests. All consumers
// only read the result (get/size/iteration — NettyChannelConnector and deprioritizeCooling), and the
// resolved list is not mutated after being wrapped, so the lightweight view is safe.
private static List<InetSocketAddress> rotateBy(List<InetSocketAddress> resolved, int index) {
return new RotatedView(resolved, index);
}

private static final class RotatedView extends AbstractList<InetSocketAddress> {

private final List<InetSocketAddress> resolved;
private final int index;
private final int size;

RotatedView(List<InetSocketAddress> resolved, int index) {
this.resolved = resolved;
this.index = index;
this.size = resolved.size();
}

@Override
public InetSocketAddress get(int i) {
if (i < 0 || i >= size) {
throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + size);
}
// index and i are both in [0, size), so index + i < 2*size — one wrap suffices (no modulo).
int j = index + i;
return resolved.get(j < size ? j : j - size);
}

@Override
public int size() {
return size;
}
}

// Stable-partition the rotated order into not-cooling (kept first) and cooling (moved to the back),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

class RoundRobinAddressSelectorTest {
Expand Down Expand Up @@ -74,6 +76,35 @@ void rotationFollowsResolverOrder() {
assertEquals("127.0.0.3", firstIp(selector.rotate("h", input)));
}

@Test
void rotatedViewMatchesFullLeftRotation() {
RoundRobinAddressSelector selector = new RoundRobinAddressSelector();
List<InetSocketAddress> input = Arrays.asList(addr("127.0.0.1"), addr("127.0.0.2"),
addr("127.0.0.3"), addr("127.0.0.4"));
int n = input.size();
// Each successive rotation is the resolver order rotated left by one more; the returned view must
// reproduce the whole order element-by-element (not just the first element).
for (int start = 0; start < n; start++) {
List<InetSocketAddress> rotated = selector.rotate("h", input);
assertEquals(n, rotated.size());
for (int i = 0; i < n; i++) {
assertEquals(input.get((start + i) % n), rotated.get(i),
"element " + i + " of the rotation starting at index " + start);
}
}
}

@Test
void rotatedResultIsAReadOnlyView() {
RoundRobinAddressSelector selector = new RoundRobinAddressSelector();
List<InetSocketAddress> input = Arrays.asList(addr("127.0.0.1"), addr("127.0.0.2"), addr("127.0.0.3"));
selector.rotate("h", input); // index 0 -> returns input as-is
List<InetSocketAddress> rotated = selector.rotate("h", input); // index 1 -> a real rotation
assertNotSame(input, rotated, "a non-zero rotation returns a distinct view");
assertThrows(UnsupportedOperationException.class, () -> rotated.set(0, addr("127.0.0.9")),
"the rotated view must be read-only (all consumers only read it)");
}

@Test
void singleAddressReturnedUnchanged() {
RoundRobinAddressSelector selector = new RoundRobinAddressSelector();
Expand Down
Loading