Skip to content

Commit 0b2d440

Browse files
authored
Introduce custom NameResolver.Args (#11669)
grpc-binder's upcoming AndroidIntentNameResolver needs to know the target Android user so it can resolve target URIs in the correct place. Unfortunately, Android's built in intent:// URI scheme has no way to specify a user and in fact the android.os.UserHandle object can't reasonably be encoded as a String at all. We solve this problem by extending NameResolver.Args with the same type-safe and domain-specific Key<T> pattern used by CallOptions, Context and CreateSubchannelArgs. New "custom" arguments could apply to all NameResolvers of a certain URI scheme, to all NameResolvers producing a particular type of java.net.SocketAddress, or even to a specific NameResolver subclass.
1 parent ef7c2d5 commit 0b2d440

File tree

8 files changed

+179
-33
lines changed

8 files changed

+179
-33
lines changed

api/src/main/java/io/grpc/ForwardingChannelBuilder2.java

+6
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,12 @@ protected T addMetricSink(MetricSink metricSink) {
263263
return thisT();
264264
}
265265

266+
@Override
267+
public <X> T setNameResolverArg(NameResolver.Args.Key<X> key, X value) {
268+
delegate().setNameResolverArg(key, value);
269+
return thisT();
270+
}
271+
266272
/**
267273
* Returns the {@link ManagedChannel} built by the delegate by default. Overriding method can
268274
* return different value.

api/src/main/java/io/grpc/ManagedChannelBuilder.java

+17
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,23 @@ protected T addMetricSink(MetricSink metricSink) {
633633
throw new UnsupportedOperationException();
634634
}
635635

636+
/**
637+
* Provides a "custom" argument for the {@link NameResolver}, if applicable, replacing any 'value'
638+
* previously provided for 'key'.
639+
*
640+
* <p>NB: If the selected {@link NameResolver} does not understand 'key', or target URI resolution
641+
* isn't needed at all, your custom argument will be silently ignored.
642+
*
643+
* <p>See {@link NameResolver.Args#getArg(NameResolver.Args.Key)} for more.
644+
*
645+
* @param key identifies the argument in a type-safe manner
646+
* @param value the argument itself
647+
* @return this
648+
*/
649+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/1770")
650+
public <X> T setNameResolverArg(NameResolver.Args.Key<X> key, X value) {
651+
throw new UnsupportedOperationException();
652+
}
636653

637654
/**
638655
* Builds a channel using the given parameters.

api/src/main/java/io/grpc/NameResolver.java

+100-25
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@
2828
import java.lang.annotation.RetentionPolicy;
2929
import java.net.URI;
3030
import java.util.Collections;
31+
import java.util.IdentityHashMap;
3132
import java.util.List;
3233
import java.util.Map;
3334
import java.util.concurrent.Executor;
3435
import java.util.concurrent.ScheduledExecutorService;
3536
import javax.annotation.Nullable;
37+
import javax.annotation.concurrent.Immutable;
3638
import javax.annotation.concurrent.ThreadSafe;
3739

3840
/**
@@ -276,7 +278,12 @@ public Status onResult2(ResolutionResult resolutionResult) {
276278
/**
277279
* Information that a {@link Factory} uses to create a {@link NameResolver}.
278280
*
279-
* <p>Note this class doesn't override neither {@code equals()} nor {@code hashCode()}.
281+
* <p>Args applicable to all {@link NameResolver}s are defined here using ordinary setters and
282+
* getters. This container can also hold externally-defined "custom" args that aren't so widely
283+
* useful or that would be inappropriate dependencies for this low level API. See {@link
284+
* Args#getArg} for more.
285+
*
286+
* <p>Note this class overrides neither {@code equals()} nor {@code hashCode()}.
280287
*
281288
* @since 1.21.0
282289
*/
@@ -291,26 +298,20 @@ public static final class Args {
291298
@Nullable private final Executor executor;
292299
@Nullable private final String overrideAuthority;
293300
@Nullable private final MetricRecorder metricRecorder;
301+
@Nullable private final IdentityHashMap<Key<?>, Object> customArgs;
294302

295-
private Args(
296-
Integer defaultPort,
297-
ProxyDetector proxyDetector,
298-
SynchronizationContext syncContext,
299-
ServiceConfigParser serviceConfigParser,
300-
@Nullable ScheduledExecutorService scheduledExecutorService,
301-
@Nullable ChannelLogger channelLogger,
302-
@Nullable Executor executor,
303-
@Nullable String overrideAuthority,
304-
@Nullable MetricRecorder metricRecorder) {
305-
this.defaultPort = checkNotNull(defaultPort, "defaultPort not set");
306-
this.proxyDetector = checkNotNull(proxyDetector, "proxyDetector not set");
307-
this.syncContext = checkNotNull(syncContext, "syncContext not set");
308-
this.serviceConfigParser = checkNotNull(serviceConfigParser, "serviceConfigParser not set");
309-
this.scheduledExecutorService = scheduledExecutorService;
310-
this.channelLogger = channelLogger;
311-
this.executor = executor;
312-
this.overrideAuthority = overrideAuthority;
313-
this.metricRecorder = metricRecorder;
303+
private Args(Builder builder) {
304+
this.defaultPort = checkNotNull(builder.defaultPort, "defaultPort not set");
305+
this.proxyDetector = checkNotNull(builder.proxyDetector, "proxyDetector not set");
306+
this.syncContext = checkNotNull(builder.syncContext, "syncContext not set");
307+
this.serviceConfigParser =
308+
checkNotNull(builder.serviceConfigParser, "serviceConfigParser not set");
309+
this.scheduledExecutorService = builder.scheduledExecutorService;
310+
this.channelLogger = builder.channelLogger;
311+
this.executor = builder.executor;
312+
this.overrideAuthority = builder.overrideAuthority;
313+
this.metricRecorder = builder.metricRecorder;
314+
this.customArgs = cloneCustomArgs(builder.customArgs);
314315
}
315316

316317
/**
@@ -319,6 +320,7 @@ private Args(
319320
*
320321
* @since 1.21.0
321322
*/
323+
// <p>TODO: Only meaningful for InetSocketAddress producers. Make this a custom arg?
322324
public int getDefaultPort() {
323325
return defaultPort;
324326
}
@@ -371,6 +373,30 @@ public ServiceConfigParser getServiceConfigParser() {
371373
return serviceConfigParser;
372374
}
373375

376+
/**
377+
* Returns the value of a custom arg named 'key', or {@code null} if it's not set.
378+
*
379+
* <p>While ordinary {@link Args} should be universally useful and meaningful, custom arguments
380+
* can apply just to resolvers of a certain URI scheme, just to resolvers producing a particular
381+
* type of {@link java.net.SocketAddress}, or even an individual {@link NameResolver} subclass.
382+
* Custom args are identified by an instance of {@link Args.Key} which should be a constant
383+
* defined in a java package and class appropriate for the argument's scope.
384+
*
385+
* <p>{@link Args} are normally reserved for information in *support* of name resolution, not
386+
* the name to be resolved itself. However, there are rare cases where all or part of the target
387+
* name can't be represented by any standard URI scheme or can't be encoded as a String at all.
388+
* Custom args, in contrast, can hold arbitrary Java types, making them a useful work around in
389+
* these cases.
390+
*
391+
* <p>Custom args can also be used simply to avoid adding inappropriate deps to the low level
392+
* io.grpc package.
393+
*/
394+
@SuppressWarnings("unchecked") // Cast is safe because all put()s go through the setArg() API.
395+
@Nullable
396+
public <T> T getArg(Key<T> key) {
397+
return customArgs != null ? (T) customArgs.get(key) : null;
398+
}
399+
374400
/**
375401
* Returns the {@link ChannelLogger} for the Channel served by this NameResolver.
376402
*
@@ -424,6 +450,7 @@ public String toString() {
424450
.add("proxyDetector", proxyDetector)
425451
.add("syncContext", syncContext)
426452
.add("serviceConfigParser", serviceConfigParser)
453+
.add("customArgs", customArgs)
427454
.add("scheduledExecutorService", scheduledExecutorService)
428455
.add("channelLogger", channelLogger)
429456
.add("executor", executor)
@@ -448,6 +475,7 @@ public Builder toBuilder() {
448475
builder.setOffloadExecutor(executor);
449476
builder.setOverrideAuthority(overrideAuthority);
450477
builder.setMetricRecorder(metricRecorder);
478+
builder.customArgs = cloneCustomArgs(customArgs);
451479
return builder;
452480
}
453481

@@ -475,6 +503,7 @@ public static final class Builder {
475503
private Executor executor;
476504
private String overrideAuthority;
477505
private MetricRecorder metricRecorder;
506+
private IdentityHashMap<Key<?>, Object> customArgs;
478507

479508
Builder() {
480509
}
@@ -561,6 +590,17 @@ public Builder setOverrideAuthority(String authority) {
561590
return this;
562591
}
563592

593+
/** See {@link Args#getArg(Key)}. */
594+
public <T> Builder setArg(Key<T> key, T value) {
595+
checkNotNull(key, "key");
596+
checkNotNull(value, "value");
597+
if (customArgs == null) {
598+
customArgs = new IdentityHashMap<>();
599+
}
600+
customArgs.put(key, value);
601+
return this;
602+
}
603+
564604
/**
565605
* See {@link Args#getMetricRecorder()}. This is an optional field.
566606
*/
@@ -575,11 +615,40 @@ public Builder setMetricRecorder(MetricRecorder metricRecorder) {
575615
* @since 1.21.0
576616
*/
577617
public Args build() {
578-
return
579-
new Args(
580-
defaultPort, proxyDetector, syncContext, serviceConfigParser,
581-
scheduledExecutorService, channelLogger, executor, overrideAuthority,
582-
metricRecorder);
618+
return new Args(this);
619+
}
620+
}
621+
622+
/**
623+
* Identifies an externally-defined custom argument that can be stored in {@link Args}.
624+
*
625+
* <p>Uses reference equality so keys should be defined as global constants.
626+
*
627+
* @param <T> type of values that can be stored under this key
628+
*/
629+
@Immutable
630+
@SuppressWarnings("UnusedTypeParameter")
631+
public static final class Key<T> {
632+
private final String debugString;
633+
634+
private Key(String debugString) {
635+
this.debugString = debugString;
636+
}
637+
638+
@Override
639+
public String toString() {
640+
return debugString;
641+
}
642+
643+
/**
644+
* Creates a new instance of {@link Key}.
645+
*
646+
* @param debugString a string used to describe the key, used for debugging.
647+
* @param <T> Key type
648+
* @return a new instance of Key
649+
*/
650+
public static <T> Key<T> create(String debugString) {
651+
return new Key<>(debugString);
583652
}
584653
}
585654
}
@@ -877,4 +946,10 @@ public String toString() {
877946
}
878947
}
879948
}
949+
950+
@Nullable
951+
private static IdentityHashMap<Args.Key<?>, Object> cloneCustomArgs(
952+
@Nullable IdentityHashMap<Args.Key<?>, Object> customArgs) {
953+
return customArgs != null ? new IdentityHashMap<>(customArgs) : null;
954+
}
880955
}

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

+13-3
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,13 @@ public class NameResolverTest {
4747
private static final List<EquivalentAddressGroup> ADDRESSES =
4848
Collections.singletonList(
4949
new EquivalentAddressGroup(new FakeSocketAddress("fake-address-1"), Attributes.EMPTY));
50-
private static final Attributes.Key<String> YOLO_KEY = Attributes.Key.create("yolo");
51-
private static Attributes ATTRIBUTES = Attributes.newBuilder()
52-
.set(YOLO_KEY, "To be, or not to be?").build();
50+
private static final Attributes.Key<String> YOLO_ATTR_KEY = Attributes.Key.create("yolo");
51+
private static Attributes ATTRIBUTES =
52+
Attributes.newBuilder().set(YOLO_ATTR_KEY, "To be, or not to be?").build();
53+
private static final NameResolver.Args.Key<Integer> FOO_ARG_KEY =
54+
NameResolver.Args.Key.create("foo");
55+
private static final NameResolver.Args.Key<Integer> BAR_ARG_KEY =
56+
NameResolver.Args.Key.create("bar");
5357
private static ConfigOrError CONFIG = ConfigOrError.fromConfig("foo");
5458

5559
@Rule
@@ -65,6 +69,7 @@ public class NameResolverTest {
6569
private final Executor executor = Executors.newSingleThreadExecutor();
6670
private final String overrideAuthority = "grpc.io";
6771
private final MetricRecorder metricRecorder = new MetricRecorder() {};
72+
private final int customArgValue = 42;
6873
@Mock NameResolver.Listener mockListener;
6974

7075
@Test
@@ -79,6 +84,8 @@ public void args() {
7984
assertThat(args.getOffloadExecutor()).isSameInstanceAs(executor);
8085
assertThat(args.getOverrideAuthority()).isSameInstanceAs(overrideAuthority);
8186
assertThat(args.getMetricRecorder()).isSameInstanceAs(metricRecorder);
87+
assertThat(args.getArg(FOO_ARG_KEY)).isEqualTo(customArgValue);
88+
assertThat(args.getArg(BAR_ARG_KEY)).isNull();
8289

8390
NameResolver.Args args2 = args.toBuilder().build();
8491
assertThat(args2.getDefaultPort()).isEqualTo(defaultPort);
@@ -90,6 +97,8 @@ public void args() {
9097
assertThat(args2.getOffloadExecutor()).isSameInstanceAs(executor);
9198
assertThat(args2.getOverrideAuthority()).isSameInstanceAs(overrideAuthority);
9299
assertThat(args.getMetricRecorder()).isSameInstanceAs(metricRecorder);
100+
assertThat(args.getArg(FOO_ARG_KEY)).isEqualTo(customArgValue);
101+
assertThat(args.getArg(BAR_ARG_KEY)).isNull();
93102

94103
assertThat(args2).isNotSameInstanceAs(args);
95104
assertThat(args2).isNotEqualTo(args);
@@ -106,6 +115,7 @@ private NameResolver.Args createArgs() {
106115
.setOffloadExecutor(executor)
107116
.setOverrideAuthority(overrideAuthority)
108117
.setMetricRecorder(metricRecorder)
118+
.setArg(FOO_ARG_KEY, customArgValue)
109119
.build();
110120
}
111121

core/src/main/java/io/grpc/internal/ManagedChannelImpl.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -591,8 +591,7 @@ ClientStream newSubstream(
591591
this.authorityOverride = builder.authorityOverride;
592592
this.metricRecorder = new MetricRecorderImpl(builder.metricSinks,
593593
MetricInstrumentRegistry.getDefaultRegistry());
594-
this.nameResolverArgs =
595-
NameResolver.Args.newBuilder()
594+
NameResolver.Args.Builder nameResolverArgsBuilder = NameResolver.Args.newBuilder()
596595
.setDefaultPort(builder.getDefaultPort())
597596
.setProxyDetector(proxyDetector)
598597
.setSynchronizationContext(syncContext)
@@ -601,8 +600,9 @@ ClientStream newSubstream(
601600
.setChannelLogger(channelLogger)
602601
.setOffloadExecutor(this.offloadExecutorHolder)
603602
.setOverrideAuthority(this.authorityOverride)
604-
.setMetricRecorder(this.metricRecorder)
605-
.build();
603+
.setMetricRecorder(this.metricRecorder);
604+
builder.copyAllNameResolverCustomArgsTo(nameResolverArgsBuilder);
605+
this.nameResolverArgs = nameResolverArgsBuilder.build();
606606
this.nameResolver = getNameResolver(
607607
targetUri, authorityOverride, nameResolverProvider, nameResolverArgs);
608608
this.balancerRpcExecutorPool = checkNotNull(balancerRpcExecutorPool, "balancerRpcExecutorPool");

core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java

+21
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import java.util.Arrays;
5656
import java.util.Collection;
5757
import java.util.Collections;
58+
import java.util.IdentityHashMap;
5859
import java.util.LinkedHashMap;
5960
import java.util.List;
6061
import java.util.Map;
@@ -159,6 +160,8 @@ public static ManagedChannelBuilder<?> forTarget(String target) {
159160
final ChannelCredentials channelCredentials;
160161
@Nullable
161162
final CallCredentials callCredentials;
163+
@Nullable
164+
IdentityHashMap<NameResolver.Args.Key<?>, Object> nameResolverCustomArgs;
162165

163166
@Nullable
164167
private final SocketAddress directServerAddress;
@@ -613,6 +616,24 @@ private static List<?> checkListEntryTypes(List<?> list) {
613616
return Collections.unmodifiableList(parsedList);
614617
}
615618

619+
@Override
620+
public <X> ManagedChannelImplBuilder setNameResolverArg(NameResolver.Args.Key<X> key, X value) {
621+
if (nameResolverCustomArgs == null) {
622+
nameResolverCustomArgs = new IdentityHashMap<>();
623+
}
624+
nameResolverCustomArgs.put(checkNotNull(key, "key"), checkNotNull(value, "value"));
625+
return this;
626+
}
627+
628+
@SuppressWarnings("unchecked") // This cast is safe because of setNameResolverArg()'s signature.
629+
void copyAllNameResolverCustomArgsTo(NameResolver.Args.Builder dest) {
630+
if (nameResolverCustomArgs != null) {
631+
for (Map.Entry<NameResolver.Args.Key<?>, Object> entry : nameResolverCustomArgs.entrySet()) {
632+
dest.setArg((NameResolver.Args.Key<Object>) entry.getKey(), entry.getValue());
633+
}
634+
}
635+
}
636+
616637
@Override
617638
public ManagedChannelImplBuilder disableServiceConfigLookUp() {
618639
this.lookUpServiceConfig = false;

core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java

+10
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,16 @@ public void disableNameResolverServiceConfig() {
764764
assertThat(builder.lookUpServiceConfig).isFalse();
765765
}
766766

767+
@Test
768+
public void setNameResolverExtArgs() {
769+
assertThat(builder.nameResolverCustomArgs)
770+
.isNull();
771+
772+
NameResolver.Args.Key<Integer> testKey = NameResolver.Args.Key.create("test-key");
773+
builder.setNameResolverArg(testKey, 42);
774+
assertThat(builder.nameResolverCustomArgs.get(testKey)).isEqualTo(42);
775+
}
776+
767777
@Test
768778
public void metricSinks() {
769779
MetricSink mocksink = mock(MetricSink.class);

0 commit comments

Comments
 (0)