Skip to content

Commit

Permalink
Support BinderChannelBuilder.forTarget.
Browse files Browse the repository at this point in the history
Allows this class to be used with custom name resolvers.

Add a test case for this to BinderChannelSmokeTest, and also
a test for InProcessServers via forTarget.
  • Loading branch information
markb74 committed Oct 27, 2021
1 parent e0ecd5c commit 954c2c0
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 13 deletions.
Expand Up @@ -44,6 +44,7 @@
import io.grpc.stub.ClientCalls;
import io.grpc.stub.ServerCalls;
import io.grpc.stub.StreamObserver;
import io.grpc.testing.FakeNameResolverProvider;
import io.grpc.testing.TestUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
Expand Down Expand Up @@ -117,7 +118,7 @@ public void setUp() throws Exception {
.build(),
TestUtils.recordRequestHeadersInterceptor(headersCapture));

AndroidComponentAddress serverAddress = HostServices.allocateService(appContext);
serverAddress = HostServices.allocateService(appContext);
HostServices.configureService(serverAddress,
HostServices.serviceParamsBuilder()
.setServerFactory((service, receiver) ->
Expand Down Expand Up @@ -192,6 +193,14 @@ public void testStreamingCallOptionHeaders() throws Exception {
assertThat(headersCapture.get().get(GrpcUtil.TIMEOUT_KEY)).isGreaterThan(0);
}

@Test
public void testConnectViaTargetUri() throws Exception {
String targetUri = "fake://server";
FakeNameResolverProvider.register(targetUri, serverAddress);
channel = BinderChannelBuilder.forTarget(targetUri, appContext).build();
assertThat(doCall("Hello").get()).isEqualTo("Hello");
}

private static String createLargeString(int size) {
StringBuilder sb = new StringBuilder();
while (sb.length() < size) {
Expand Down
56 changes: 44 additions & 12 deletions binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java
Expand Up @@ -67,13 +67,35 @@ public final class BinderChannelBuilder
* <p>You the caller are responsible for managing the lifecycle of any channels built by the
* resulting builder. They will not be shut down automatically.
*
* @param targetAddress the {@link AndroidComponentAddress} referencing the service to bind to.
* @param directAddress the {@link AndroidComponentAddress} referencing the service to bind to.
* @param sourceContext the context to bind from (e.g. The current Activity or Application).
* @return a new builder
*/
public static BinderChannelBuilder forAddress(
AndroidComponentAddress targetAddress, Context sourceContext) {
return new BinderChannelBuilder(targetAddress, sourceContext);
AndroidComponentAddress directAddress, Context sourceContext) {
return new BinderChannelBuilder(
checkNotNull(directAddress, "directAddress"), null, sourceContext);
}

/**
* Creates a channel builder that will bind to a remote Android service, via a string
* target name which will be resolved.
*
* <p>The underlying Android binding will be torn down when the channel becomes idle. This happens
* after 30 minutes without use by default but can be configured via {@link
* ManagedChannelBuilder#idleTimeout(long, TimeUnit)} or triggered manually with {@link
* ManagedChannel#enterIdle()}.
*
* <p>You the caller are responsible for managing the lifecycle of any channels built by the
* resulting builder. They will not be shut down automatically.
*
* @param target A target uri which should resolve into an {@link AndroidComponentAddress}
* referencing the service to bind to.
* @param sourceContext the context to bind from (e.g. The current Activity or Application).
* @return a new builder
*/
public static BinderChannelBuilder forTarget(String target, Context sourceContext) {
return new BinderChannelBuilder(null, checkNotNull(target, "target"), sourceContext);
}

/**
Expand All @@ -88,7 +110,7 @@ public static BinderChannelBuilder forAddress(String name, int port) {
/**
* Always fails. Call {@link #forAddress(AndroidComponentAddress, Context)} instead.
*/
@DoNotCall("Unsupported. Use forAddress(AndroidComponentAddress, Context) instead")
@DoNotCall("Unsupported. Use forTarget(String, Context) instead")
public static BinderChannelBuilder forTarget(String target) {
throw new UnsupportedOperationException(
"call forAddress(AndroidComponentAddress, Context) instead");
Expand All @@ -104,9 +126,11 @@ public static BinderChannelBuilder forTarget(String target) {
private BindServiceFlags bindServiceFlags;

private BinderChannelBuilder(
AndroidComponentAddress targetAddress,
@Nullable AndroidComponentAddress directAddress,
@Nullable String target,
Context sourceContext) {
mainThreadExecutor = ContextCompat.getMainExecutor(sourceContext);
mainThreadExecutor =
ContextCompat.getMainExecutor(checkNotNull(sourceContext, "sourceContext"));
securityPolicy = SecurityPolicies.internalOnly();
inboundParcelablePolicy = InboundParcelablePolicy.DEFAULT;
bindServiceFlags = BindServiceFlags.DEFAULTS;
Expand All @@ -126,12 +150,20 @@ public ClientTransportFactory buildClientTransportFactory() {
}
}

managedChannelImplBuilder =
new ManagedChannelImplBuilder(
targetAddress,
targetAddress.getAuthority(),
new BinderChannelTransportFactoryBuilder(),
null);
if (directAddress != null) {
managedChannelImplBuilder =
new ManagedChannelImplBuilder(
directAddress,
directAddress.getAuthority(),
new BinderChannelTransportFactoryBuilder(),
null);
} else {
managedChannelImplBuilder =
new ManagedChannelImplBuilder(
target,
new BinderChannelTransportFactoryBuilder(),
null);
}
}

@Override
Expand Down
109 changes: 109 additions & 0 deletions core/src/test/java/io/grpc/inprocess/AnonymousServerTest.java
@@ -0,0 +1,109 @@
/*
* Copyright 2021 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.inprocess;

import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS;

import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import io.grpc.CallOptions;
import io.grpc.MethodDescriptor;
import io.grpc.Channel;
import io.grpc.Server;
import io.grpc.ServerCallHandler;
import io.grpc.ServerServiceDefinition;
import io.grpc.ServerStreamTracer;
import io.grpc.StringMarshaller;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.InternalServer;
import io.grpc.internal.ManagedClientTransport;
import io.grpc.stub.ClientCalls;
import io.grpc.stub.ServerCalls;
import io.grpc.stub.StreamObserver;
import io.grpc.testing.FakeNameResolverProvider;
import java.util.List;
import java.util.concurrent.Executors;
import org.junit.Before;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Unit tests for anonymous in-process servers. */
@RunWith(JUnit4.class)
public final class AnonymousServerTest {

private AnonymousInProcessSocketAddress serverAddress = new AnonymousInProcessSocketAddress();

private final MethodDescriptor<String, String> method =
MethodDescriptor.newBuilder(StringMarshaller.INSTANCE, StringMarshaller.INSTANCE)
.setFullMethodName("test/method")
.setType(MethodDescriptor.MethodType.UNARY)
.build();

private Server server;

@Before
public void setUp() throws Exception {
ServerCallHandler<String, String> callHandler =
ServerCalls.asyncUnaryCall(
new ServerCalls.UnaryMethod<String, String>() {
@Override
public void invoke(String request, StreamObserver<String> responseObserver) {
responseObserver.onNext(request);
responseObserver.onCompleted();
}
});

ServerServiceDefinition serviceDef =
ServerServiceDefinition.builder("test")
.addMethod(method, callHandler)
.build();

server = InProcessServerBuilder.forAddress(serverAddress)
.addService(serviceDef)
.build();

server.start();
}

@Test
public void testCall() throws Exception {
Channel channel = InProcessChannelBuilder.forAddress(serverAddress).build();
assertThat(doCall(channel, "Hello").get()).isEqualTo("Hello");
}

@Test
public void testCallViaTargetUri() throws Exception {
String targetUri = "fake://server";
FakeNameResolverProvider.register(targetUri, serverAddress);
Channel channel = InProcessChannelBuilder.forTarget(targetUri).build();
assertThat(doCall(channel, "Hello you faker you").get()).isEqualTo("Hello you faker you");
}

@After
public void tearDown() throws InterruptedException {
server.shutdownNow();
}

private ListenableFuture<String> doCall(Channel channel, String request) {
return Futures.withTimeout(
ClientCalls.futureUnaryCall(channel.newCall(method, CallOptions.DEFAULT), request),
5L, SECONDS, Executors.newSingleThreadScheduledExecutor());
}
}
112 changes: 112 additions & 0 deletions testing/src/main/java/io/grpc/testing/FakeNameResolverProvider.java
@@ -0,0 +1,112 @@
/*
* Copyright 2021 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.testing;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.collect.ImmutableList;
import io.grpc.EquivalentAddressGroup;
import io.grpc.NameResolver;
import io.grpc.NameResolver.Args;
import io.grpc.NameResolverProvider;
import io.grpc.NameResolverRegistry;
import io.grpc.Status;
import java.net.SocketAddress;
import java.net.URI;

/** A name resolver to always resolve the given URI into the given address. */
public final class FakeNameResolverProvider extends NameResolverProvider {

private final URI targetUri;
private final SocketAddress address;


/**
* Register a new resolver.
*
* @param targetUri The URI to resolve when requested.
* @param address The address to return for the target URI.
*/
public static final void register(String targetUri, SocketAddress address) {
NameResolverRegistry.getDefaultRegistry().register(
new FakeNameResolverProvider(URI.create(targetUri), address));
}

private FakeNameResolverProvider(URI targetUri, SocketAddress address) {
this.targetUri = targetUri;
this.address = address;
}

@Override
public NameResolver newNameResolver(URI targetUri, Args args) {
if (targetUri.equals(this.targetUri)) {
return new FakeNameResolver(address);
}
return null;
}

@Override
protected boolean isAvailable() {
return true;
}

@Override
protected int priority() {
return 5; // Default
}

@Override
public String getDefaultScheme() {
return targetUri.getScheme();
}

/** A single name resolver. */
private static final class FakeNameResolver extends NameResolver {
private static final String AUTHORITY = "fake-authority";

private final SocketAddress address;
private volatile boolean shutdown;

private FakeNameResolver(SocketAddress address) {
this.address = address;
}

@Override
public void start(Listener2 listener) {
if (shutdown) {
listener.onError(Status.FAILED_PRECONDITION.withDescription("Resolver is shutdown"));
} else {
listener.onResult(
ResolutionResult.newBuilder()
.setAddresses(ImmutableList.of(new EquivalentAddressGroup(address)))
.build());
}
}

@Override
public String getServiceAuthority() {
return AUTHORITY;
}

@Override
public void shutdown() {
shutdown = true;
}
}
}

0 comments on commit 954c2c0

Please sign in to comment.