binder: Add fault injection hooksunit tests for BinderClientTransport (#10928)

This commit is contained in:
John Cormie 2024-02-27 14:28:50 -08:00 committed by GitHub
parent af117e9764
commit a654d2ebb3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 231 additions and 7 deletions

View File

@ -19,7 +19,9 @@ package io.grpc.binder.internal;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.content.Context; import android.content.Context;
import android.os.DeadObjectException;
import android.os.Parcel; import android.os.Parcel;
import android.os.RemoteException;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -41,13 +43,14 @@ import io.grpc.binder.HostServices;
import io.grpc.binder.InboundParcelablePolicy; import io.grpc.binder.InboundParcelablePolicy;
import io.grpc.binder.SecurityPolicies; import io.grpc.binder.SecurityPolicies;
import io.grpc.binder.SecurityPolicy; import io.grpc.binder.SecurityPolicy;
import io.grpc.binder.internal.OneWayBinderProxies.BlockingBinderDecorator;
import io.grpc.binder.internal.OneWayBinderProxies.ThrowingOneWayBinderProxy;
import io.grpc.internal.ClientStream; import io.grpc.internal.ClientStream;
import io.grpc.internal.ClientStreamListener; import io.grpc.internal.ClientStreamListener;
import io.grpc.internal.FixedObjectPool; import io.grpc.internal.FixedObjectPool;
import io.grpc.internal.ManagedClientTransport; import io.grpc.internal.ManagedClientTransport;
import io.grpc.internal.ObjectPool; import io.grpc.internal.ObjectPool;
import io.grpc.internal.StreamListener; import io.grpc.internal.StreamListener;
import io.grpc.internal.StreamListener.MessageProducer;
import io.grpc.protobuf.lite.ProtoLiteUtils; import io.grpc.protobuf.lite.ProtoLiteUtils;
import io.grpc.stub.ServerCalls; import io.grpc.stub.ServerCalls;
import java.io.IOException; import java.io.IOException;
@ -140,12 +143,19 @@ public final class BinderClientTransportTest {
private class BinderClientTransportBuilder { private class BinderClientTransportBuilder {
private SecurityPolicy securityPolicy = SecurityPolicies.internalOnly(); private SecurityPolicy securityPolicy = SecurityPolicies.internalOnly();
private OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR;
public BinderClientTransportBuilder setSecurityPolicy(SecurityPolicy securityPolicy) { public BinderClientTransportBuilder setSecurityPolicy(SecurityPolicy securityPolicy) {
this.securityPolicy = securityPolicy; this.securityPolicy = securityPolicy;
return this; return this;
} }
public BinderClientTransportBuilder setBinderDecorator(
OneWayBinderProxy.Decorator binderDecorator) {
this.binderDecorator = binderDecorator;
return this;
}
public BinderTransport.BinderClientTransport build() { public BinderTransport.BinderClientTransport build() {
return new BinderTransport.BinderClientTransport( return new BinderTransport.BinderClientTransport(
appContext, appContext,
@ -158,6 +168,7 @@ public final class BinderClientTransportTest {
executorServicePool, executorServicePool,
securityPolicy, securityPolicy,
InboundParcelablePolicy.DEFAULT, InboundParcelablePolicy.DEFAULT,
binderDecorator,
Attributes.EMPTY); Attributes.EMPTY);
} }
} }
@ -273,6 +284,62 @@ public final class BinderClientTransportTest {
transportListener.awaitReady(); transportListener.awaitReady();
} }
@Test
public void testTxnFailureDuringSetup() throws InterruptedException {
BlockingBinderDecorator<ThrowingOneWayBinderProxy> decorator = new BlockingBinderDecorator<>();
transport = new BinderClientTransportBuilder()
.setBinderDecorator(decorator)
.build();
transport.start(transportListener).run();
ThrowingOneWayBinderProxy endpointBinder = new ThrowingOneWayBinderProxy(
decorator.takeNextRequest());
DeadObjectException doe = new DeadObjectException("ouch");
endpointBinder.setRemoteException(doe);
decorator.putNextResult(endpointBinder);
Status shutdownStatus = transportListener.awaitShutdown();
assertThat(shutdownStatus.getCode()).isEqualTo(Code.UNAVAILABLE);
assertThat(shutdownStatus.getCause()).isInstanceOf(RemoteException.class);
transportListener.awaitTermination();
ClientStream stream =
transport.newStream(streamingMethodDesc, new Metadata(), CallOptions.DEFAULT, tracers);
stream.start(streamListener);
Status streamStatus = streamListener.awaitClose();
assertThat(streamStatus.getCode()).isEqualTo(Code.UNAVAILABLE);
assertThat(streamStatus.getCause()).isSameInstanceAs(doe);
}
@Test
public void testTxnFailurePostSetup() throws InterruptedException {
BlockingBinderDecorator<ThrowingOneWayBinderProxy> decorator = new BlockingBinderDecorator<>();
transport = new BinderClientTransportBuilder()
.setBinderDecorator(decorator)
.build();
transport.start(transportListener).run();
ThrowingOneWayBinderProxy endpointBinder = new ThrowingOneWayBinderProxy(
decorator.takeNextRequest());
decorator.putNextResult(endpointBinder);
ThrowingOneWayBinderProxy serverBinder = new ThrowingOneWayBinderProxy(
decorator.takeNextRequest());
DeadObjectException doe = new DeadObjectException("ouch");
serverBinder.setRemoteException(doe);
decorator.putNextResult(serverBinder);
transportListener.awaitReady();
ClientStream stream =
transport.newStream(streamingMethodDesc, new Metadata(), CallOptions.DEFAULT, tracers);
stream.start(streamListener);
stream.writeMessage(marshaller.stream(Empty.getDefaultInstance()));
stream.halfClose();
stream.request(1);
Status streamStatus = streamListener.awaitClose();
assertThat(streamStatus.getCode()).isEqualTo(Code.UNAVAILABLE);
assertThat(streamStatus.getCause()).isSameInstanceAs(doe);
}
private static void startAndAwaitReady( private static void startAndAwaitReady(
BinderTransport.BinderClientTransport transport, TestTransportListener transportListener) { BinderTransport.BinderClientTransport transport, TestTransportListener transportListener) {
transport.start(transportListener).run(); transport.start(transportListener).run();
@ -288,13 +355,28 @@ public final class BinderClientTransportTest {
public boolean terminated; public boolean terminated;
@Override @Override
public void transportShutdown(Status shutdownStatus) { public synchronized void transportShutdown(Status shutdownStatus) {
this.shutdownStatus = shutdownStatus; this.shutdownStatus = shutdownStatus;
notifyAll();
}
public synchronized Status awaitShutdown() throws InterruptedException {
while (shutdownStatus == null) {
wait();
}
return shutdownStatus;
} }
@Override @Override
public void transportTerminated() { public synchronized void transportTerminated() {
terminated = true; terminated = true;
notifyAll();
}
public synchronized void awaitTermination() throws InterruptedException {
while (!terminated) {
wait();
}
} }
@Override @Override

View File

@ -108,6 +108,7 @@ public final class BinderTransportTest extends AbstractTransportTest {
offloadExecutorPool, offloadExecutorPool,
SecurityPolicies.internalOnly(), SecurityPolicies.internalOnly(),
InboundParcelablePolicy.DEFAULT, InboundParcelablePolicy.DEFAULT,
OneWayBinderProxy.IDENTITY_DECORATOR,
eagAttrs()); eagAttrs());
} }

View File

@ -0,0 +1,100 @@
/*
* Copyright 2024 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.binder.internal;
import android.os.RemoteException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import javax.annotation.Nullable;
/**
* A collection of {@link OneWayBinderProxy}-related test helpers.
*/
public final class OneWayBinderProxies {
/**
* A {@link OneWayBinderProxy.Decorator} that blocks calling threads while an (external) test
* provides the actual decoration.
*/
public static final class BlockingBinderDecorator<T extends OneWayBinderProxy> implements
OneWayBinderProxy.Decorator {
private final BlockingQueue<OneWayBinderProxy> requests = new LinkedBlockingQueue<>();
private final BlockingQueue<T> results = new LinkedBlockingQueue<>();
/**
* Returns the next {@link OneWayBinderProxy} that needs decorating, blocking if it hasn't yet
* been provided to {@link #decorate}.
*
* <p>Follow this with a call to {@link #putNextResult(OneWayBinderProxy)} to provide
* the result of {@link #decorate} and unblock the waiting caller.
*/
public OneWayBinderProxy takeNextRequest() throws InterruptedException {
return requests.take();
}
/**
* Provides the next value to return from {@link #decorate}.
*/
public void putNextResult(T next) throws InterruptedException {
results.put(next);
}
@Override
public OneWayBinderProxy decorate(OneWayBinderProxy in) {
try {
requests.put(in);
return results.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
}
/**
* A {@link OneWayBinderProxy} decorator whose transact method can artificially throw.
*/
public static final class ThrowingOneWayBinderProxy extends OneWayBinderProxy {
private final OneWayBinderProxy wrapped;
@Nullable
private RemoteException remoteException;
ThrowingOneWayBinderProxy(OneWayBinderProxy wrapped) {
super(wrapped.getDelegate());
this.wrapped = wrapped;
}
/**
* Causes all future invocations of transact to throw `remoteException`.
*
* <p>Users are responsible for ensuring their calls "happen-before" the relevant calls to
* {@link #transact(int, ParcelHolder)}.
*/
public void setRemoteException(RemoteException remoteException) {
this.remoteException = remoteException;
}
@Override
public void transact(int code, ParcelHolder data) throws RemoteException {
if (remoteException != null) {
throw remoteException;
}
wrapped.transact(code, data);
}
}
// Cannot be instantiated.
private OneWayBinderProxies() {};
}

View File

@ -30,6 +30,7 @@ import io.grpc.ForwardingChannelBuilder;
import io.grpc.ManagedChannel; import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder; import io.grpc.ManagedChannelBuilder;
import io.grpc.binder.internal.BinderTransport; import io.grpc.binder.internal.BinderTransport;
import io.grpc.binder.internal.OneWayBinderProxy;
import io.grpc.internal.ClientTransportFactory; import io.grpc.internal.ClientTransportFactory;
import io.grpc.internal.ConnectionClientTransport; import io.grpc.internal.ConnectionClientTransport;
import io.grpc.internal.FixedObjectPool; import io.grpc.internal.FixedObjectPool;
@ -384,6 +385,7 @@ public final class BinderChannelBuilder
offloadExecutorPool, offloadExecutorPool,
securityPolicy, securityPolicy,
inboundParcelablePolicy, inboundParcelablePolicy,
OneWayBinderProxy.IDENTITY_DECORATOR,
options.getEagAttributes()); options.getEagAttributes());
} }

View File

@ -162,7 +162,9 @@ public final class BinderServer implements InternalServer, LeakSafeOneWayBinder.
// Create a new transport and let our listener know about it. // Create a new transport and let our listener know about it.
BinderTransport.BinderServerTransport transport = BinderTransport.BinderServerTransport transport =
new BinderTransport.BinderServerTransport( new BinderTransport.BinderServerTransport(
executorServicePool, attrsBuilder.build(), streamTracerFactories, callbackBinder); executorServicePool, attrsBuilder.build(), streamTracerFactories,
OneWayBinderProxy.IDENTITY_DECORATOR,
callbackBinder);
transport.setServerTransportListener(listener.transportCreated(transport)); transport.setServerTransportListener(listener.transportCreated(transport));
return true; return true;
} }

View File

@ -191,6 +191,7 @@ public abstract class BinderTransport
private final LeakSafeOneWayBinder incomingBinder; private final LeakSafeOneWayBinder incomingBinder;
protected final ConcurrentHashMap<Integer, Inbound<?>> ongoingCalls; protected final ConcurrentHashMap<Integer, Inbound<?>> ongoingCalls;
protected final OneWayBinderProxy.Decorator binderDecorator;
@GuardedBy("this") @GuardedBy("this")
private final LinkedHashSet<Integer> callIdsToNotifyWhenReady = new LinkedHashSet<>(); private final LinkedHashSet<Integer> callIdsToNotifyWhenReady = new LinkedHashSet<>();
@ -218,7 +219,9 @@ public abstract class BinderTransport
private BinderTransport( private BinderTransport(
ObjectPool<ScheduledExecutorService> executorServicePool, ObjectPool<ScheduledExecutorService> executorServicePool,
Attributes attributes, Attributes attributes,
OneWayBinderProxy.Decorator binderDecorator,
InternalLogId logId) { InternalLogId logId) {
this.binderDecorator = binderDecorator;
this.executorServicePool = executorServicePool; this.executorServicePool = executorServicePool;
this.attributes = attributes; this.attributes = attributes;
this.logId = logId; this.logId = logId;
@ -283,6 +286,7 @@ public abstract class BinderTransport
@GuardedBy("this") @GuardedBy("this")
protected boolean setOutgoingBinder(OneWayBinderProxy binder) { protected boolean setOutgoingBinder(OneWayBinderProxy binder) {
binder = binderDecorator.decorate(binder);
this.outgoingBinder = binder; this.outgoingBinder = binder;
try { try {
binder.getDelegate().linkToDeath(this, 0); binder.getDelegate().linkToDeath(this, 0);
@ -566,6 +570,12 @@ public abstract class BinderTransport
@GuardedBy("this") @GuardedBy("this")
private int latestCallId = FIRST_CALL_ID; private int latestCallId = FIRST_CALL_ID;
/**
* Constructs a new transport instance.
*
* @param binderDecorator used to decorate both the "endpoint" and "server" binders, for fault
* injection.
*/
public BinderClientTransport( public BinderClientTransport(
Context sourceContext, Context sourceContext,
BinderChannelCredentials channelCredentials, BinderChannelCredentials channelCredentials,
@ -577,10 +587,12 @@ public abstract class BinderTransport
ObjectPool<? extends Executor> offloadExecutorPool, ObjectPool<? extends Executor> offloadExecutorPool,
SecurityPolicy securityPolicy, SecurityPolicy securityPolicy,
InboundParcelablePolicy inboundParcelablePolicy, InboundParcelablePolicy inboundParcelablePolicy,
OneWayBinderProxy.Decorator binderDecorator,
Attributes eagAttrs) { Attributes eagAttrs) {
super( super(
executorServicePool, executorServicePool,
buildClientAttributes(eagAttrs, sourceContext, targetAddress, inboundParcelablePolicy), buildClientAttributes(eagAttrs, sourceContext, targetAddress, inboundParcelablePolicy),
binderDecorator,
buildLogId(sourceContext, targetAddress)); buildLogId(sourceContext, targetAddress));
this.offloadExecutorPool = offloadExecutorPool; this.offloadExecutorPool = offloadExecutorPool;
this.securityPolicy = securityPolicy; this.securityPolicy = securityPolicy;
@ -607,7 +619,7 @@ public abstract class BinderTransport
@Override @Override
public synchronized void onBound(IBinder binder) { public synchronized void onBound(IBinder binder) {
sendSetupTransaction(OneWayBinderProxy.wrap(binder, offloadExecutor)); sendSetupTransaction(binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor)));
} }
@Override @Override
@ -819,12 +831,18 @@ public abstract class BinderTransport
private final List<ServerStreamTracer.Factory> streamTracerFactories; private final List<ServerStreamTracer.Factory> streamTracerFactories;
@Nullable private ServerTransportListener serverTransportListener; @Nullable private ServerTransportListener serverTransportListener;
/**
* Constructs a new transport instance.
*
* @param binderDecorator used to decorate 'callbackBinder', for fault injection.
*/
public BinderServerTransport( public BinderServerTransport(
ObjectPool<ScheduledExecutorService> executorServicePool, ObjectPool<ScheduledExecutorService> executorServicePool,
Attributes attributes, Attributes attributes,
List<ServerStreamTracer.Factory> streamTracerFactories, List<ServerStreamTracer.Factory> streamTracerFactories,
OneWayBinderProxy.Decorator binderDecorator,
IBinder callbackBinder) { IBinder callbackBinder) {
super(executorServicePool, attributes, buildLogId(attributes)); super(executorServicePool, attributes, binderDecorator, buildLogId(attributes));
this.streamTracerFactories = streamTracerFactories; this.streamTracerFactories = streamTracerFactories;
// TODO(jdcormie): Plumb in the Server's executor() and use it here instead. // TODO(jdcormie): Plumb in the Server's executor() and use it here instead.
setOutgoingBinder(OneWayBinderProxy.wrap(callbackBinder, getScheduledExecutorService())); setOutgoingBinder(OneWayBinderProxy.wrap(callbackBinder, getScheduledExecutorService()));

View File

@ -46,7 +46,7 @@ public abstract class OneWayBinderProxy {
private static final Logger logger = Logger.getLogger(OneWayBinderProxy.class.getName()); private static final Logger logger = Logger.getLogger(OneWayBinderProxy.class.getName());
protected final IBinder delegate; protected final IBinder delegate;
private OneWayBinderProxy(IBinder iBinder) { protected OneWayBinderProxy(IBinder iBinder) {
this.delegate = iBinder; this.delegate = iBinder;
} }
@ -64,6 +64,24 @@ public abstract class OneWayBinderProxy {
: new OutOfProcessImpl(iBinder); : new OutOfProcessImpl(iBinder);
} }
/**
* An abstract function that decorates instances of {@link OneWayBinderProxy}.
*
* <p>See https://en.wikipedia.org/wiki/Decorator_pattern.
*/
public interface Decorator {
/**
* Returns an instance of {@link OneWayBinderProxy} that decorates {@code input} with some
* new behavior.
*/
OneWayBinderProxy decorate(OneWayBinderProxy input);
}
/**
* A {@link Decorator} that does nothing.
*/
public static final Decorator IDENTITY_DECORATOR = (x) -> x;
/** /**
* Enqueues a transaction for the wrapped {@link IBinder} with guaranteed "oneway" semantics. * Enqueues a transaction for the wrapped {@link IBinder} with guaranteed "oneway" semantics.
* *

View File

@ -71,6 +71,7 @@ public final class BinderServerTransportTest {
new FixedObjectPool<>(executorService), new FixedObjectPool<>(executorService),
Attributes.EMPTY, Attributes.EMPTY,
ImmutableList.of(), ImmutableList.of(),
OneWayBinderProxy.IDENTITY_DECORATOR,
mockBinder); mockBinder);
} }