Skip to content

Commit 791f894

Browse files
authoredJun 5, 2024··
binder: Add a connection timeout (#11255)
Timeout is initially infinite so that we can release/import the supporting code and the behavior change independently. Fixes #11137
1 parent 2264246 commit 791f894

File tree

4 files changed

+173
-47
lines changed

4 files changed

+173
-47
lines changed
 

Diff for: ‎binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java

+86-46
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@
2222
import android.os.DeadObjectException;
2323
import android.os.Parcel;
2424
import android.os.RemoteException;
25-
import androidx.core.content.ContextCompat;
2625
import androidx.test.core.app.ApplicationProvider;
2726
import androidx.test.ext.junit.runners.AndroidJUnit4;
27+
import com.google.common.util.concurrent.SettableFuture;
2828
import com.google.protobuf.Empty;
29-
import io.grpc.Attributes;
3029
import io.grpc.CallOptions;
3130
import io.grpc.ClientStreamTracer;
3231
import io.grpc.Metadata;
@@ -36,13 +35,10 @@
3635
import io.grpc.Status;
3736
import io.grpc.Status.Code;
3837
import io.grpc.binder.AndroidComponentAddress;
39-
import io.grpc.binder.BindServiceFlags;
40-
import io.grpc.binder.BinderChannelCredentials;
4138
import io.grpc.binder.BinderServerBuilder;
4239
import io.grpc.binder.HostServices;
43-
import io.grpc.binder.InboundParcelablePolicy;
44-
import io.grpc.binder.SecurityPolicies;
4540
import io.grpc.binder.SecurityPolicy;
41+
import io.grpc.binder.internal.OneWayBinderProxies.BlackHoleOneWayBinderProxy;
4642
import io.grpc.binder.internal.OneWayBinderProxies.BlockingBinderDecorator;
4743
import io.grpc.binder.internal.OneWayBinderProxies.ThrowingOneWayBinderProxy;
4844
import io.grpc.internal.ClientStream;
@@ -59,9 +55,11 @@
5955
import java.util.ArrayDeque;
6056
import java.util.Deque;
6157
import java.util.concurrent.BlockingQueue;
58+
import java.util.concurrent.ExecutorService;
6259
import java.util.concurrent.Executors;
6360
import java.util.concurrent.LinkedBlockingQueue;
6461
import java.util.concurrent.ScheduledExecutorService;
62+
import java.util.concurrent.TimeUnit;
6563
import javax.annotation.Nullable;
6664
import javax.annotation.concurrent.GuardedBy;
6765
import org.junit.After;
@@ -77,6 +75,8 @@
7775
*/
7876
@RunWith(AndroidJUnit4.class)
7977
public final class BinderClientTransportTest {
78+
private static final long TIMEOUT_SECONDS = 5;
79+
8080
private static final ClientStreamTracer[] tracers = new ClientStreamTracer[] {
8181
new ClientStreamTracer() {}
8282
};
@@ -100,9 +100,12 @@ public final class BinderClientTransportTest {
100100

101101
AndroidComponentAddress serverAddress;
102102
BinderTransport.BinderClientTransport transport;
103+
BlockingSecurityPolicy blockingSecurityPolicy = new BlockingSecurityPolicy();
103104

104105
private final ObjectPool<ScheduledExecutorService> executorServicePool =
105106
new FixedObjectPool<>(Executors.newScheduledThreadPool(1));
107+
private final ObjectPool<ScheduledExecutorService> offloadServicePool =
108+
new FixedObjectPool<>(Executors.newScheduledThreadPool(1));
106109
private final TestTransportListener transportListener = new TestTransportListener();
107110
private final TestStreamListener streamListener = new TestStreamListener();
108111

@@ -146,7 +149,7 @@ private class BinderClientTransportBuilder {
146149
final BinderClientTransportFactory.Builder factoryBuilder = new BinderClientTransportFactory.Builder()
147150
.setSourceContext(appContext)
148151
.setScheduledExecutorPool(executorServicePool)
149-
.setOffloadExecutorPool(executorServicePool);
152+
.setOffloadExecutorPool(offloadServicePool);
150153

151154
public BinderClientTransportBuilder setSecurityPolicy(SecurityPolicy securityPolicy) {
152155
factoryBuilder.setSecurityPolicy(securityPolicy);
@@ -159,6 +162,11 @@ public BinderClientTransportBuilder setBinderDecorator(
159162
return this;
160163
}
161164

165+
public BinderClientTransportBuilder setReadyTimeoutMillis(int timeoutMillis) {
166+
factoryBuilder.setReadyTimeoutMillis(timeoutMillis);
167+
return this;
168+
}
169+
162170
public BinderTransport.BinderClientTransport build() {
163171
return factoryBuilder.buildClientTransportFactory()
164172
.newClientTransport(serverAddress, new ClientTransportOptions(), null);
@@ -167,9 +175,19 @@ public BinderTransport.BinderClientTransport build() {
167175

168176
@After
169177
public void tearDown() throws Exception {
178+
blockingSecurityPolicy.provideNextCheckAuthorizationResult(Status.ABORTED);
170179
transport.shutdownNow(Status.OK);
171180
HostServices.awaitServiceShutdown();
172-
executorServicePool.getObject().shutdownNow();
181+
shutdownAndTerminate(executorServicePool.getObject());
182+
shutdownAndTerminate(offloadServicePool.getObject());
183+
}
184+
185+
private static void shutdownAndTerminate(ExecutorService executorService)
186+
throws InterruptedException {
187+
executorService.shutdownNow();
188+
if (!executorService.awaitTermination(TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
189+
throw new AssertionError("executor failed to terminate promptly");
190+
}
173191
}
174192

175193
@Test
@@ -261,23 +279,22 @@ public void testMessageProducerClosedAfterStream_b169313545() throws Exception {
261279
}
262280

263281
@Test
264-
public void testNewStreamBeforeTransportReadyFails() throws InterruptedException {
282+
public void testNewStreamBeforeTransportReadyFails() throws Exception {
265283
// Use a special SecurityPolicy that lets us act before the transport is setup/ready.
266-
BlockingSecurityPolicy bsp = new BlockingSecurityPolicy();
267-
transport = new BinderClientTransportBuilder().setSecurityPolicy(bsp).build();
284+
transport = new BinderClientTransportBuilder().setSecurityPolicy(blockingSecurityPolicy).build();
268285
transport.start(transportListener).run();
269286
ClientStream stream =
270287
transport.newStream(streamingMethodDesc, new Metadata(), CallOptions.DEFAULT, tracers);
271288
stream.start(streamListener);
272289
assertThat(streamListener.awaitClose().getCode()).isEqualTo(Code.INTERNAL);
273290

274291
// Unblock the SETUP_TRANSPORT handshake and make sure it becomes ready in the usual way.
275-
bsp.provideNextCheckAuthorizationResult(Status.OK);
292+
blockingSecurityPolicy.provideNextCheckAuthorizationResult(Status.OK);
276293
transportListener.awaitReady();
277294
}
278295

279296
@Test
280-
public void testTxnFailureDuringSetup() throws InterruptedException {
297+
public void testTxnFailureDuringSetup() throws Exception {
281298
BlockingBinderDecorator<ThrowingOneWayBinderProxy> decorator = new BlockingBinderDecorator<>();
282299
transport = new BinderClientTransportBuilder()
283300
.setBinderDecorator(decorator)
@@ -304,7 +321,7 @@ public void testTxnFailureDuringSetup() throws InterruptedException {
304321
}
305322

306323
@Test
307-
public void testTxnFailurePostSetup() throws InterruptedException {
324+
public void testTxnFailurePostSetup() throws Exception {
308325
BlockingBinderDecorator<ThrowingOneWayBinderProxy> decorator = new BlockingBinderDecorator<>();
309326
transport = new BinderClientTransportBuilder()
310327
.setBinderDecorator(decorator)
@@ -332,59 +349,82 @@ public void testTxnFailurePostSetup() throws InterruptedException {
332349
assertThat(streamStatus.getCause()).isSameInstanceAs(doe);
333350
}
334351

352+
@Test
353+
public void testBlackHoleEndpointConnectTimeout() throws Exception {
354+
BlockingBinderDecorator<BlackHoleOneWayBinderProxy> decorator = new BlockingBinderDecorator<>();
355+
transport = new BinderClientTransportBuilder()
356+
.setBinderDecorator(decorator)
357+
.setReadyTimeoutMillis(1_234)
358+
.build();
359+
transport.start(transportListener).run();
360+
BlackHoleOneWayBinderProxy endpointBinder = new BlackHoleOneWayBinderProxy(
361+
decorator.takeNextRequest());
362+
endpointBinder.dropAllTransactions(true);
363+
decorator.putNextResult(endpointBinder);
364+
Status transportStatus = transportListener.awaitShutdown();
365+
assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED);
366+
assertThat(transportStatus.getDescription()).contains("1234");
367+
transportListener.awaitTermination();
368+
}
369+
370+
@Test
371+
public void testBlackHoleSecurityPolicyConnectTimeout() throws Exception {
372+
transport = new BinderClientTransportBuilder()
373+
.setSecurityPolicy(blockingSecurityPolicy)
374+
.setReadyTimeoutMillis(1_234)
375+
.build();
376+
transport.start(transportListener).run();
377+
Status transportStatus = transportListener.awaitShutdown();
378+
assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED);
379+
assertThat(transportStatus.getDescription()).contains("1234");
380+
transportListener.awaitTermination();
381+
blockingSecurityPolicy.provideNextCheckAuthorizationResult(Status.OK);
382+
}
383+
335384
private static void startAndAwaitReady(
336-
BinderTransport.BinderClientTransport transport, TestTransportListener transportListener) {
385+
BinderTransport.BinderClientTransport transport, TestTransportListener transportListener)
386+
throws Exception {
337387
transport.start(transportListener).run();
338388
transportListener.awaitReady();
339389
}
340390

341391
private static final class TestTransportListener implements ManagedClientTransport.Listener {
342-
@GuardedBy("this")
343-
private boolean ready;
344-
345392
public boolean inUse;
346-
@Nullable public Status shutdownStatus;
347-
public boolean terminated;
393+
private final SettableFuture<Boolean> isReady = SettableFuture.create();
394+
private final SettableFuture<Status> shutdownStatus = SettableFuture.create();
395+
private final SettableFuture<Boolean> isTerminated = SettableFuture.create();
348396

349397
@Override
350-
public synchronized void transportShutdown(Status shutdownStatus) {
351-
this.shutdownStatus = shutdownStatus;
352-
notifyAll();
398+
public void transportShutdown(Status shutdownStatus) {
399+
if (!this.shutdownStatus.set(shutdownStatus)) {
400+
throw new IllegalStateException("transportShutdown() already called");
401+
}
353402
}
354403

355-
public synchronized Status awaitShutdown() throws InterruptedException {
356-
while (shutdownStatus == null) {
357-
wait();
358-
}
359-
return shutdownStatus;
404+
public Status awaitShutdown() throws Exception {
405+
return shutdownStatus.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
360406
}
361407

362408
@Override
363-
public synchronized void transportTerminated() {
364-
terminated = true;
365-
notifyAll();
409+
public void transportTerminated() {
410+
if (!isTerminated.set(true)) {
411+
throw new IllegalStateException("isTerminated() already called");
412+
}
366413
}
367414

368-
public synchronized void awaitTermination() throws InterruptedException {
369-
while (!terminated) {
370-
wait();
371-
}
415+
public void awaitTermination() throws Exception {
416+
isTerminated.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
372417
}
373418

374419
@Override
375-
public synchronized void transportReady() {
376-
ready = true;
377-
notifyAll();
420+
public void transportReady() {
421+
if (!isReady.set(true)) {
422+
throw new IllegalStateException("isTerminated() already called");
423+
}
378424
}
379425

380-
public synchronized void awaitReady() {
381-
while (!ready) {
382-
try {
383-
wait();
384-
} catch (InterruptedException inte) {
385-
throw new AssertionError("Interrupted waiting for ready");
386-
}
387-
}
426+
public void awaitReady() throws Exception {
427+
isReady.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
388428
}
389429

390430
@Override

Diff for: ‎binder/src/androidTest/java/io/grpc/binder/internal/OneWayBinderProxies.java

+31
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,37 @@ public void transact(int code, ParcelHolder data) throws RemoteException {
9595
}
9696
}
9797

98+
/**
99+
* A {@link OneWayBinderProxy} decorator whose transact method can be configured to silently drop.
100+
*/
101+
public static final class BlackHoleOneWayBinderProxy extends OneWayBinderProxy {
102+
103+
private final OneWayBinderProxy wrapped;
104+
private boolean dropAllTransactions;
105+
106+
BlackHoleOneWayBinderProxy(OneWayBinderProxy wrapped) {
107+
super(wrapped.getDelegate());
108+
this.wrapped = wrapped;
109+
}
110+
111+
/**
112+
* Causes all future invocations of transact to be silently dropped.
113+
*
114+
* <p>Users are responsible for ensuring their calls "happen-before" the relevant calls to
115+
* {@link #transact(int, ParcelHolder)}.
116+
*/
117+
public void dropAllTransactions(boolean dropAllTransactions) {
118+
this.dropAllTransactions = dropAllTransactions;
119+
}
120+
121+
@Override
122+
public void transact(int code, ParcelHolder data) throws RemoteException {
123+
if (!dropAllTransactions) {
124+
wrapped.transact(code, data);
125+
}
126+
}
127+
}
128+
98129
// Cannot be instantiated.
99130
private OneWayBinderProxies() {};
100131
}

Diff for: ‎binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java

+29
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
5757
final BindServiceFlags bindServiceFlags;
5858
final InboundParcelablePolicy inboundParcelablePolicy;
5959
final OneWayBinderProxy.Decorator binderDecorator;
60+
final long readyTimeoutMillis;
6061

6162
ScheduledExecutorService executorService;
6263
Executor offloadExecutor;
@@ -74,6 +75,7 @@ private BinderClientTransportFactory(Builder builder) {
7475
bindServiceFlags = checkNotNull(builder.bindServiceFlags);
7576
inboundParcelablePolicy = checkNotNull(builder.inboundParcelablePolicy);
7677
binderDecorator = checkNotNull(builder.binderDecorator);
78+
readyTimeoutMillis = builder.readyTimeoutMillis;
7779

7880
executorService = scheduledExecutorPool.getObject();
7981
offloadExecutor = offloadExecutorPool.getObject();
@@ -129,6 +131,7 @@ public static final class Builder implements ClientTransportFactoryBuilder {
129131
BindServiceFlags bindServiceFlags = BindServiceFlags.DEFAULTS;
130132
InboundParcelablePolicy inboundParcelablePolicy = InboundParcelablePolicy.DEFAULT;
131133
OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR;
134+
long readyTimeoutMillis = -1; // TODO(jdcormie) Set an non-infinite default in a separate PR.
132135

133136
@Override
134137
public BinderClientTransportFactory buildClientTransportFactory() {
@@ -191,5 +194,31 @@ public Builder setBinderDecorator(OneWayBinderProxy.Decorator binderDecorator) {
191194
this.binderDecorator = checkNotNull(binderDecorator, "binderDecorator");
192195
return this;
193196
}
197+
198+
/**
199+
* Limits how long it can take to for a new transport to become ready after being started.
200+
*
201+
* <p>This process currently includes:
202+
* <ul>
203+
* <li>Creating an Android binding.
204+
* <li>Waiting for Android to create the server process.
205+
* <li>Waiting for the remote Service to be created and handle onBind().
206+
* <li>Exchanging handshake transactions according to the wire protocol.
207+
* <li>Evaluating a {@link SecurityPolicy} on both sides.
208+
* </ul>
209+
*
210+
* <p>This setting doesn't change the need for deadlines at the call level. It merely ensures
211+
* that gRPC features like
212+
* <a href="https://github.com/grpc/grpc/blob/master/doc/load-balancing.md">load balancing</a>
213+
* and <a href="https://github.com/grpc/grpc/blob/master/doc/wait-for-ready.md">fail-fast</a>
214+
* work as expected despite certain edge cases that could otherwise stall the transport
215+
* indefinitely.
216+
*
217+
* <p>Optional. Use a negative value to wait indefinitely.
218+
*/
219+
public Builder setReadyTimeoutMillis(long readyTimeoutMillis) {
220+
this.readyTimeoutMillis = readyTimeoutMillis;
221+
return this;
222+
}
194223
}
195224
}

Diff for: ‎binder/src/main/java/io/grpc/binder/internal/BinderTransport.java

+27-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static com.google.common.base.Preconditions.checkNotNull;
2020
import static com.google.common.base.Preconditions.checkState;
2121
import static com.google.common.util.concurrent.Futures.immediateFuture;
22+
import static java.util.concurrent.TimeUnit.MILLISECONDS;
2223

2324
import android.content.Context;
2425
import android.os.Binder;
@@ -69,6 +70,7 @@
6970
import java.util.concurrent.ConcurrentHashMap;
7071
import java.util.concurrent.Executor;
7172
import java.util.concurrent.ScheduledExecutorService;
73+
import java.util.concurrent.ScheduledFuture;
7274
import java.util.concurrent.atomic.AtomicInteger;
7375
import java.util.concurrent.atomic.AtomicLong;
7476
import java.util.logging.Level;
@@ -560,13 +562,15 @@ public static final class BinderClientTransport extends BinderTransport
560562
private final Bindable serviceBinding;
561563
/** Number of ongoing calls which keep this transport "in-use". */
562564
private final AtomicInteger numInUseStreams;
563-
565+
private final long readyTimeoutMillis;
564566
private final PingTracker pingTracker;
565567

566568
@Nullable private ManagedClientTransport.Listener clientTransportListener;
567569

568570
@GuardedBy("this")
569571
private int latestCallId = FIRST_CALL_ID;
572+
@GuardedBy("this")
573+
private ScheduledFuture<?> readyTimeoutFuture; // != null iff timeout scheduled.
570574

571575
/**
572576
* Constructs a new transport instance.
@@ -588,6 +592,7 @@ public BinderClientTransport(
588592
this.offloadExecutorPool = factory.offloadExecutorPool;
589593
this.securityPolicy = factory.securityPolicy;
590594
this.offloadExecutor = offloadExecutorPool.getObject();
595+
this.readyTimeoutMillis = factory.readyTimeoutMillis;
591596
numInUseStreams = new AtomicInteger();
592597
pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id));
593598

@@ -627,11 +632,24 @@ public synchronized Runnable start(ManagedClientTransport.Listener clientTranspo
627632
if (inState(TransportState.NOT_STARTED)) {
628633
setState(TransportState.SETUP);
629634
serviceBinding.bind();
635+
if (readyTimeoutMillis >= 0) {
636+
readyTimeoutFuture = getScheduledExecutorService().schedule(
637+
BinderClientTransport.this::onReadyTimeout, readyTimeoutMillis, MILLISECONDS);
638+
}
630639
}
631640
}
632641
};
633642
}
634643

644+
private synchronized void onReadyTimeout() {
645+
if (inState(TransportState.SETUP)) {
646+
readyTimeoutFuture = null;
647+
shutdownInternal(Status.DEADLINE_EXCEEDED
648+
.withDescription("Connect timeout " + readyTimeoutMillis + "ms lapsed"),
649+
true);
650+
}
651+
}
652+
635653
@Override
636654
public synchronized ClientStream newStream(
637655
final MethodDescriptor<?, ?> method,
@@ -712,6 +730,10 @@ public void notifyTerminated() {
712730
if (numInUseStreams.getAndSet(0) > 0) {
713731
clientTransportListener.transportInUse(false);
714732
}
733+
if (readyTimeoutFuture != null) {
734+
readyTimeoutFuture.cancel(false);
735+
readyTimeoutFuture = null;
736+
}
715737
serviceBinding.unbind();
716738
clientTransportListener.transportTerminated();
717739
}
@@ -761,6 +783,10 @@ private void checkSecurityPolicy(IBinder binder) {
761783
setState(TransportState.READY);
762784
attributes = clientTransportListener.filterTransport(attributes);
763785
clientTransportListener.transportReady();
786+
if (readyTimeoutFuture != null) {
787+
readyTimeoutFuture.cancel(false);
788+
readyTimeoutFuture = null;
789+
}
764790
}
765791
}
766792
}

0 commit comments

Comments
 (0)
Please sign in to comment.