Skip to content
Closed
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
182 changes: 54 additions & 128 deletions api/src/main/java/io/grpc/EquivalentAddressGroup.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2015 The gRPC Authors
* Copyright 2026 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.
Expand All @@ -16,151 +16,77 @@

package io.grpc;

import com.google.common.base.Preconditions;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import static com.google.common.truth.Truth.assertThat;

import java.lang.reflect.Field;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
* A group of {@link SocketAddress}es that are considered equivalent when channel makes connections.
*
* <p>Usually the addresses are addresses resolved from the same host name, and connecting to any of
* them is equally sufficient. They do have order. An address appears earlier on the list is likely
* to be tried earlier.
* Unit tests for {@link EquivalentAddressGroup}.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/1770")
public final class EquivalentAddressGroup {

/**
* The authority to be used when constructing Subchannels for this EquivalentAddressGroup.
* However, if the channel has overridden authority via
* {@link ManagedChannelBuilder#overrideAuthority(String)}, the transport will use the channel's
* authority override.
*
* <p>The authority <strong>must</strong> be from a trusted source, because if the authority is
* tampered with, RPCs may be sent to attackers which may leak sensitive user data. If the
* authority was acquired by doing I/O, the communication must be authenticated (e.g., via TLS).
* Recognize that the server that provided the authority can trivially impersonate the service.
*/
@Attr
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/6138")
public static final Attributes.Key<String> ATTR_AUTHORITY_OVERRIDE =
Attributes.Key.create("io.grpc.EquivalentAddressGroup.ATTR_AUTHORITY_OVERRIDE");
/**
* The name of the locality that this EquivalentAddressGroup is in.
*/
public static final Attributes.Key<String> ATTR_LOCALITY_NAME =
Attributes.Key.create("io.grpc.EquivalentAddressGroup.LOCALITY");
private final List<SocketAddress> addrs;
private final Attributes attrs;

/**
* {@link SocketAddress} docs say that the addresses are immutable, so we cache the hashCode.
*/
private final int hashCode;

/**
* List constructor without {@link Attributes}.
*/
public EquivalentAddressGroup(List<SocketAddress> addrs) {
this(addrs, Attributes.EMPTY);
}
@RunWith(JUnit4.class)
public class EquivalentAddressGroupTest {

/**
* List constructor with {@link Attributes}.
*/
public EquivalentAddressGroup(List<SocketAddress> addrs, @Attr Attributes attrs) {
Preconditions.checkArgument(!addrs.isEmpty(), "addrs is empty");
this.addrs = Collections.unmodifiableList(new ArrayList<>(addrs));
this.attrs = Preconditions.checkNotNull(attrs, "attrs");
// Attributes may contain mutable objects, which means Attributes' hashCode may change over
// time, thus we don't cache Attributes' hashCode.
hashCode = this.addrs.hashCode();
}
@Test
public void toString_summarizesLargeAddressList() {
int maxAddressesToString = maxAddressesToString();
List<SocketAddress> addrs = new ArrayList<>();
for (int i = 0; i <= maxAddressesToString; i++) {
addrs.add(new FakeSocketAddress("addr" + i));
}
EquivalentAddressGroup eag = new EquivalentAddressGroup(addrs);

/**
* Singleton constructor without Attributes.
*/
public EquivalentAddressGroup(SocketAddress addr) {
this(addr, Attributes.EMPTY);
StringBuilder expected = new StringBuilder();
expected.append('[').append('[');
for (int i = 0; i < maxAddressesToString; i++) {
if (i > 0) {
expected.append(", ");
}
expected.append(addrs.get(i));
}
expected.append(", ... 1 more]/{}]");
assertThat(eag.toString()).isEqualTo(expected.toString());
}

/**
* Singleton constructor with Attributes.
*/
public EquivalentAddressGroup(SocketAddress addr, @Attr Attributes attrs) {
this(Collections.singletonList(addr), attrs);
}
@Test
public void toString_doesNotSummarizeAtMaxAddressCount() {
int maxAddressesToString = maxAddressesToString();
List<SocketAddress> addrs = new ArrayList<>();
for (int i = 0; i < maxAddressesToString; i++) {
addrs.add(new FakeSocketAddress("addr" + i));
}
EquivalentAddressGroup eag = new EquivalentAddressGroup(addrs);

/**
* Returns an immutable list of the addresses.
*/
public List<SocketAddress> getAddresses() {
return addrs;
String expected = "[" + addrs + "/{}]";
assertThat(eag.toString()).isEqualTo(expected);
}

/**
* Returns the attributes.
*/
@Attr
public Attributes getAttributes() {
return attrs;
private static int maxAddressesToString() {
try {
Field field = EquivalentAddressGroup.class.getDeclaredField("MAX_ADDRESSES_TO_STRING");
field.setAccessible(true);
return (int) field.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new LinkageError("Unable to read MAX_ADDRESSES_TO_STRING", e);
}
}

@Override
public String toString() {
// TODO(zpencer): Summarize return value if addr is very large
return "[" + addrs + "/" + attrs + "]";
}
private static final class FakeSocketAddress extends SocketAddress {

@Override
public int hashCode() {
// Avoids creating an iterator on the underlying array list.
return hashCode;
}
private final String name;

/**
* Returns true if the given object is also an {@link EquivalentAddressGroup} with an equal
* address list and equal attribute values.
*
* <p>Note that if the attributes include mutable values, it is possible for two objects to be
* considered equal at one point in time and not equal at another (due to concurrent mutation of
* attribute values).
*/
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof EquivalentAddressGroup)) {
return false;
FakeSocketAddress(String name) {
this.name = name;
}
EquivalentAddressGroup that = (EquivalentAddressGroup) other;
if (addrs.size() != that.addrs.size()) {
return false;
}
// Avoids creating an iterator on the underlying array list.
for (int i = 0; i < addrs.size(); i++) {
if (!addrs.get(i).equals(that.addrs.get(i))) {
return false;
}
}
if (!attrs.equals(that.attrs)) {
return false;

@Override
public String toString() {
return name;
}
return true;
}

/**
* Annotation for {@link EquivalentAddressGroup}'s attributes. It follows the annotation semantics
* defined by {@link Attributes}.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/4972")
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface Attr {}
}
54 changes: 37 additions & 17 deletions api/src/test/java/io/grpc/CallOptionsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;

import com.google.common.base.Objects;
import com.google.common.truth.Fact;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.Subject;
import io.grpc.ClientStreamTracer.StreamInfo;
import io.grpc.internal.SerializingExecutor;
import java.time.Duration;
Expand Down Expand Up @@ -106,17 +108,15 @@ public void allWiths() {

@Test
public void noStrayModifications() {
assertThat(equal(allSet, allSet.withAuthority("blah").withAuthority(sampleAuthority)))
.isTrue();
assertThat(
equal(allSet,
allSet.withDeadline(Deadline.after(314, NANOSECONDS)).withDeadline(sampleDeadline)))
.isTrue();
assertThat(
equal(allSet,
assertAbout(callOptions()).that(allSet.withAuthority("blah").withAuthority(sampleAuthority))
.isEquivalentTo(allSet);
assertAbout(callOptions()).that(
allSet.withDeadline(Deadline.after(314, NANOSECONDS)).withDeadline(sampleDeadline))
.isEquivalentTo(allSet);
assertAbout(callOptions()).that(
allSet.withCallCredentials(mock(CallCredentials.class))
.withCallCredentials(sampleCreds)))
.isTrue();
.withCallCredentials(sampleCreds))
.isEquivalentTo(allSet);
}

@Test
Expand Down Expand Up @@ -265,12 +265,32 @@ public void getWaitForReady() {
assertSame(CallOptions.DEFAULT.withoutWaitForReady().getWaitForReady(), Boolean.FALSE);
}

// Only used in noStrayModifications()
// TODO(carl-mastrangelo): consider making a CallOptionsSubject for Truth.
private static boolean equal(CallOptions o1, CallOptions o2) {
return Objects.equal(o1.getDeadline(), o2.getDeadline())
&& Objects.equal(o1.getAuthority(), o2.getAuthority())
&& Objects.equal(o1.getCredentials(), o2.getCredentials());
private static Subject.Factory<CallOptionsSubject, CallOptions> callOptions() {
return CallOptionsSubject::new;
}

private static final class CallOptionsSubject extends Subject {

private final CallOptions actual;

private CallOptionsSubject(FailureMetadata metadata, CallOptions actual) {
super(metadata, actual);
this.actual = actual;
}

public void isEquivalentTo(CallOptions expected) {
if (actual == null) {
failWithActual("expected", expected);
return;
}
if (expected == null) {
failWithoutActual(Fact.simpleFact("expected non-null CallOptions"));
return;
}
check("deadline").that(actual.getDeadline()).isEqualTo(expected.getDeadline());
check("authority").that(actual.getAuthority()).isEqualTo(expected.getAuthority());
check("credentials").that(actual.getCredentials()).isEqualTo(expected.getCredentials());
}
}

private static class FakeTicker extends Deadline.Ticker {
Expand Down
92 changes: 92 additions & 0 deletions api/src/test/java/io/grpc/EquivalentAddressGroupTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright 2026 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;

import static com.google.common.truth.Truth.assertThat;

import java.lang.reflect.Field;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
* Unit tests for {@link EquivalentAddressGroup}.
*/
@RunWith(JUnit4.class)
public class EquivalentAddressGroupTest {

@Test
public void toString_summarizesLargeAddressList() {
int maxAddressesToString = maxAddressesToString();
List<SocketAddress> addrs = new ArrayList<>();
for (int i = 0; i <= maxAddressesToString; i++) {
addrs.add(new FakeSocketAddress("addr" + i));
}
EquivalentAddressGroup eag = new EquivalentAddressGroup(addrs);

StringBuilder expected = new StringBuilder();
expected.append('[').append('[');
for (int i = 0; i < maxAddressesToString; i++) {
if (i > 0) {
expected.append(", ");
}
expected.append(addrs.get(i));
}
expected.append(", ... 1 more]/{}]");
assertThat(eag.toString()).isEqualTo(expected.toString());
}

@Test
public void toString_doesNotSummarizeAtMaxAddressCount() {
int maxAddressesToString = maxAddressesToString();
List<SocketAddress> addrs = new ArrayList<>();
for (int i = 0; i < maxAddressesToString; i++) {
addrs.add(new FakeSocketAddress("addr" + i));
}
EquivalentAddressGroup eag = new EquivalentAddressGroup(addrs);

String expected = "[" + addrs + "/{}]";
assertThat(eag.toString()).isEqualTo(expected);
}

private static int maxAddressesToString() {
try {
Field field = EquivalentAddressGroup.class.getDeclaredField("MAX_ADDRESSES_TO_STRING");
field.setAccessible(true);
return (int) field.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new LinkageError("Unable to read MAX_ADDRESSES_TO_STRING", e);
}
}

private static final class FakeSocketAddress extends SocketAddress {

private final String name;

FakeSocketAddress(String name) {
this.name = name;
}

@Override
public String toString() {
return name;
}
}
}
Loading