Skip to content

Commit 4b896d4

Browse files
committed
api: Summarize EquivalentAddressGroup toString
1 parent 228fc8e commit 4b896d4

3 files changed

Lines changed: 183 additions & 145 deletions

File tree

Lines changed: 54 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2015 The gRPC Authors
2+
* Copyright 2026 The gRPC Authors
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,151 +16,77 @@
1616

1717
package io.grpc;
1818

19-
import com.google.common.base.Preconditions;
20-
import java.lang.annotation.Documented;
21-
import java.lang.annotation.Retention;
22-
import java.lang.annotation.RetentionPolicy;
19+
import static com.google.common.truth.Truth.assertThat;
20+
21+
import java.lang.reflect.Field;
2322
import java.net.SocketAddress;
2423
import java.util.ArrayList;
25-
import java.util.Collections;
2624
import java.util.List;
25+
import org.junit.Test;
26+
import org.junit.runner.RunWith;
27+
import org.junit.runners.JUnit4;
2728

2829
/**
29-
* A group of {@link SocketAddress}es that are considered equivalent when channel makes connections.
30-
*
31-
* <p>Usually the addresses are addresses resolved from the same host name, and connecting to any of
32-
* them is equally sufficient. They do have order. An address appears earlier on the list is likely
33-
* to be tried earlier.
30+
* Unit tests for {@link EquivalentAddressGroup}.
3431
*/
35-
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/1770")
36-
public final class EquivalentAddressGroup {
37-
38-
/**
39-
* The authority to be used when constructing Subchannels for this EquivalentAddressGroup.
40-
* However, if the channel has overridden authority via
41-
* {@link ManagedChannelBuilder#overrideAuthority(String)}, the transport will use the channel's
42-
* authority override.
43-
*
44-
* <p>The authority <strong>must</strong> be from a trusted source, because if the authority is
45-
* tampered with, RPCs may be sent to attackers which may leak sensitive user data. If the
46-
* authority was acquired by doing I/O, the communication must be authenticated (e.g., via TLS).
47-
* Recognize that the server that provided the authority can trivially impersonate the service.
48-
*/
49-
@Attr
50-
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/6138")
51-
public static final Attributes.Key<String> ATTR_AUTHORITY_OVERRIDE =
52-
Attributes.Key.create("io.grpc.EquivalentAddressGroup.ATTR_AUTHORITY_OVERRIDE");
53-
/**
54-
* The name of the locality that this EquivalentAddressGroup is in.
55-
*/
56-
public static final Attributes.Key<String> ATTR_LOCALITY_NAME =
57-
Attributes.Key.create("io.grpc.EquivalentAddressGroup.LOCALITY");
58-
private final List<SocketAddress> addrs;
59-
private final Attributes attrs;
60-
61-
/**
62-
* {@link SocketAddress} docs say that the addresses are immutable, so we cache the hashCode.
63-
*/
64-
private final int hashCode;
65-
66-
/**
67-
* List constructor without {@link Attributes}.
68-
*/
69-
public EquivalentAddressGroup(List<SocketAddress> addrs) {
70-
this(addrs, Attributes.EMPTY);
71-
}
32+
@RunWith(JUnit4.class)
33+
public class EquivalentAddressGroupTest {
7234

73-
/**
74-
* List constructor with {@link Attributes}.
75-
*/
76-
public EquivalentAddressGroup(List<SocketAddress> addrs, @Attr Attributes attrs) {
77-
Preconditions.checkArgument(!addrs.isEmpty(), "addrs is empty");
78-
this.addrs = Collections.unmodifiableList(new ArrayList<>(addrs));
79-
this.attrs = Preconditions.checkNotNull(attrs, "attrs");
80-
// Attributes may contain mutable objects, which means Attributes' hashCode may change over
81-
// time, thus we don't cache Attributes' hashCode.
82-
hashCode = this.addrs.hashCode();
83-
}
35+
@Test
36+
public void toString_summarizesLargeAddressList() {
37+
int maxAddressesToString = maxAddressesToString();
38+
List<SocketAddress> addrs = new ArrayList<>();
39+
for (int i = 0; i <= maxAddressesToString; i++) {
40+
addrs.add(new FakeSocketAddress("addr" + i));
41+
}
42+
EquivalentAddressGroup eag = new EquivalentAddressGroup(addrs);
8443

85-
/**
86-
* Singleton constructor without Attributes.
87-
*/
88-
public EquivalentAddressGroup(SocketAddress addr) {
89-
this(addr, Attributes.EMPTY);
44+
StringBuilder expected = new StringBuilder();
45+
expected.append('[').append('[');
46+
for (int i = 0; i < maxAddressesToString; i++) {
47+
if (i > 0) {
48+
expected.append(", ");
49+
}
50+
expected.append(addrs.get(i));
51+
}
52+
expected.append(", ... 1 more]/{}]");
53+
assertThat(eag.toString()).isEqualTo(expected.toString());
9054
}
9155

92-
/**
93-
* Singleton constructor with Attributes.
94-
*/
95-
public EquivalentAddressGroup(SocketAddress addr, @Attr Attributes attrs) {
96-
this(Collections.singletonList(addr), attrs);
97-
}
56+
@Test
57+
public void toString_doesNotSummarizeAtMaxAddressCount() {
58+
int maxAddressesToString = maxAddressesToString();
59+
List<SocketAddress> addrs = new ArrayList<>();
60+
for (int i = 0; i < maxAddressesToString; i++) {
61+
addrs.add(new FakeSocketAddress("addr" + i));
62+
}
63+
EquivalentAddressGroup eag = new EquivalentAddressGroup(addrs);
9864

99-
/**
100-
* Returns an immutable list of the addresses.
101-
*/
102-
public List<SocketAddress> getAddresses() {
103-
return addrs;
65+
String expected = "[" + addrs + "/{}]";
66+
assertThat(eag.toString()).isEqualTo(expected);
10467
}
10568

106-
/**
107-
* Returns the attributes.
108-
*/
109-
@Attr
110-
public Attributes getAttributes() {
111-
return attrs;
69+
private static int maxAddressesToString() {
70+
try {
71+
Field field = EquivalentAddressGroup.class.getDeclaredField("MAX_ADDRESSES_TO_STRING");
72+
field.setAccessible(true);
73+
return (int) field.get(null);
74+
} catch (NoSuchFieldException | IllegalAccessException e) {
75+
throw new LinkageError("Unable to read MAX_ADDRESSES_TO_STRING", e);
76+
}
11277
}
11378

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

120-
@Override
121-
public int hashCode() {
122-
// Avoids creating an iterator on the underlying array list.
123-
return hashCode;
124-
}
81+
private final String name;
12582

126-
/**
127-
* Returns true if the given object is also an {@link EquivalentAddressGroup} with an equal
128-
* address list and equal attribute values.
129-
*
130-
* <p>Note that if the attributes include mutable values, it is possible for two objects to be
131-
* considered equal at one point in time and not equal at another (due to concurrent mutation of
132-
* attribute values).
133-
*/
134-
@Override
135-
public boolean equals(Object other) {
136-
if (this == other) {
137-
return true;
138-
}
139-
if (!(other instanceof EquivalentAddressGroup)) {
140-
return false;
83+
FakeSocketAddress(String name) {
84+
this.name = name;
14185
}
142-
EquivalentAddressGroup that = (EquivalentAddressGroup) other;
143-
if (addrs.size() != that.addrs.size()) {
144-
return false;
145-
}
146-
// Avoids creating an iterator on the underlying array list.
147-
for (int i = 0; i < addrs.size(); i++) {
148-
if (!addrs.get(i).equals(that.addrs.get(i))) {
149-
return false;
150-
}
151-
}
152-
if (!attrs.equals(that.attrs)) {
153-
return false;
86+
87+
@Override
88+
public String toString() {
89+
return name;
15490
}
155-
return true;
15691
}
157-
158-
/**
159-
* Annotation for {@link EquivalentAddressGroup}'s attributes. It follows the annotation semantics
160-
* defined by {@link Attributes}.
161-
*/
162-
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/4972")
163-
@Retention(RetentionPolicy.SOURCE)
164-
@Documented
165-
public @interface Attr {}
16692
}

api/src/test/java/io/grpc/CallOptionsTest.java

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
import static org.junit.Assert.fail;
3030
import static org.mockito.Mockito.mock;
3131

32-
import com.google.common.base.Objects;
32+
import com.google.common.truth.Fact;
33+
import com.google.common.truth.FailureMetadata;
34+
import com.google.common.truth.Subject;
3335
import io.grpc.ClientStreamTracer.StreamInfo;
3436
import io.grpc.internal.SerializingExecutor;
3537
import java.time.Duration;
@@ -106,17 +108,15 @@ public void allWiths() {
106108

107109
@Test
108110
public void noStrayModifications() {
109-
assertThat(equal(allSet, allSet.withAuthority("blah").withAuthority(sampleAuthority)))
110-
.isTrue();
111-
assertThat(
112-
equal(allSet,
113-
allSet.withDeadline(Deadline.after(314, NANOSECONDS)).withDeadline(sampleDeadline)))
114-
.isTrue();
115-
assertThat(
116-
equal(allSet,
111+
assertAbout(callOptions()).that(allSet.withAuthority("blah").withAuthority(sampleAuthority))
112+
.isEquivalentTo(allSet);
113+
assertAbout(callOptions()).that(
114+
allSet.withDeadline(Deadline.after(314, NANOSECONDS)).withDeadline(sampleDeadline))
115+
.isEquivalentTo(allSet);
116+
assertAbout(callOptions()).that(
117117
allSet.withCallCredentials(mock(CallCredentials.class))
118-
.withCallCredentials(sampleCreds)))
119-
.isTrue();
118+
.withCallCredentials(sampleCreds))
119+
.isEquivalentTo(allSet);
120120
}
121121

122122
@Test
@@ -265,12 +265,32 @@ public void getWaitForReady() {
265265
assertSame(CallOptions.DEFAULT.withoutWaitForReady().getWaitForReady(), Boolean.FALSE);
266266
}
267267

268-
// Only used in noStrayModifications()
269-
// TODO(carl-mastrangelo): consider making a CallOptionsSubject for Truth.
270-
private static boolean equal(CallOptions o1, CallOptions o2) {
271-
return Objects.equal(o1.getDeadline(), o2.getDeadline())
272-
&& Objects.equal(o1.getAuthority(), o2.getAuthority())
273-
&& Objects.equal(o1.getCredentials(), o2.getCredentials());
268+
private static Subject.Factory<CallOptionsSubject, CallOptions> callOptions() {
269+
return CallOptionsSubject::new;
270+
}
271+
272+
private static final class CallOptionsSubject extends Subject {
273+
274+
private final CallOptions actual;
275+
276+
private CallOptionsSubject(FailureMetadata metadata, CallOptions actual) {
277+
super(metadata, actual);
278+
this.actual = actual;
279+
}
280+
281+
public void isEquivalentTo(CallOptions expected) {
282+
if (actual == null) {
283+
failWithActual("expected", expected);
284+
return;
285+
}
286+
if (expected == null) {
287+
failWithoutActual(Fact.simpleFact("expected non-null CallOptions"));
288+
return;
289+
}
290+
check("deadline").that(actual.getDeadline()).isEqualTo(expected.getDeadline());
291+
check("authority").that(actual.getAuthority()).isEqualTo(expected.getAuthority());
292+
check("credentials").that(actual.getCredentials()).isEqualTo(expected.getCredentials());
293+
}
274294
}
275295

276296
private static class FakeTicker extends Deadline.Ticker {
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2026 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
21+
import java.lang.reflect.Field;
22+
import java.net.SocketAddress;
23+
import java.util.ArrayList;
24+
import java.util.List;
25+
import org.junit.Test;
26+
import org.junit.runner.RunWith;
27+
import org.junit.runners.JUnit4;
28+
29+
/**
30+
* Unit tests for {@link EquivalentAddressGroup}.
31+
*/
32+
@RunWith(JUnit4.class)
33+
public class EquivalentAddressGroupTest {
34+
35+
@Test
36+
public void toString_summarizesLargeAddressList() {
37+
int maxAddressesToString = maxAddressesToString();
38+
List<SocketAddress> addrs = new ArrayList<>();
39+
for (int i = 0; i <= maxAddressesToString; i++) {
40+
addrs.add(new FakeSocketAddress("addr" + i));
41+
}
42+
EquivalentAddressGroup eag = new EquivalentAddressGroup(addrs);
43+
44+
StringBuilder expected = new StringBuilder();
45+
expected.append('[').append('[');
46+
for (int i = 0; i < maxAddressesToString; i++) {
47+
if (i > 0) {
48+
expected.append(", ");
49+
}
50+
expected.append(addrs.get(i));
51+
}
52+
expected.append(", ... 1 more]/{}]");
53+
assertThat(eag.toString()).isEqualTo(expected.toString());
54+
}
55+
56+
@Test
57+
public void toString_doesNotSummarizeAtMaxAddressCount() {
58+
int maxAddressesToString = maxAddressesToString();
59+
List<SocketAddress> addrs = new ArrayList<>();
60+
for (int i = 0; i < maxAddressesToString; i++) {
61+
addrs.add(new FakeSocketAddress("addr" + i));
62+
}
63+
EquivalentAddressGroup eag = new EquivalentAddressGroup(addrs);
64+
65+
String expected = "[" + addrs + "/{}]";
66+
assertThat(eag.toString()).isEqualTo(expected);
67+
}
68+
69+
private static int maxAddressesToString() {
70+
try {
71+
Field field = EquivalentAddressGroup.class.getDeclaredField("MAX_ADDRESSES_TO_STRING");
72+
field.setAccessible(true);
73+
return (int) field.get(null);
74+
} catch (NoSuchFieldException | IllegalAccessException e) {
75+
throw new LinkageError("Unable to read MAX_ADDRESSES_TO_STRING", e);
76+
}
77+
}
78+
79+
private static final class FakeSocketAddress extends SocketAddress {
80+
81+
private final String name;
82+
83+
FakeSocketAddress(String name) {
84+
this.name = name;
85+
}
86+
87+
@Override
88+
public String toString() {
89+
return name;
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)