Skip to content

Commit ed3bf6b

Browse files
committed
binder: Avoid force-terminating BinderTransport on stale unregisterCall task
When an ongoing call finishes and `unregisterCall()` observes `ongoingCalls.isEmpty()`, it schedules a task on `scheduledExecutorService` to finish transport shutdown if the transport is in `TransportState.SHUTDOWN`. However, `unregisterCall()` runs without holding `synchronized (this)` and may have scheduled this task while the transport is still in `TransportState.READY`. If a new stream is created before the transport enters `TransportState.SHUTDOWN`, the previously scheduled task can execute after `shutdown()` and call `shutdownInternal(shutdownStatus, true)`, force-terminating the newly started call with `UNAVAILABLE: Channel shutdown invoked`. Pass `forceTerminate = false` to `shutdownInternal()` so it re-checks `ongoingCalls.isEmpty()` while holding `synchronized (this)`.
1 parent 18f412e commit ed3bf6b

2 files changed

Lines changed: 39 additions & 3 deletions

File tree

‎binder/src/main/java/io/grpc/binder/internal/BinderTransport.java‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -403,13 +403,15 @@ protected void unregisterInbound(Inbound<?, ?> inbound) {
403403
final void unregisterCall(int callId) {
404404
boolean removed = (ongoingCalls.remove(callId) != null);
405405
if (removed && ongoingCalls.isEmpty()) {
406-
// Possibly shutdown (not synchronously, since inbound is held).
406+
// If inState(SHUTDOWN), we'd need to terminate (as the last call to finish). But we can't
407+
// even check our state safely right now because inbound/outbound is held. Instead, do the
408+
// check later on a clean stack.
407409
scheduledExecutorService.execute(
408410
() -> {
409411
synchronized (this) {
410412
if (inState(TransportState.SHUTDOWN)) {
411-
// No more ongoing calls, and we're shutdown. Finish the shutdown.
412-
shutdownInternal(shutdownStatus, true);
413+
// Don't forceTerminate because ongoingCalls may no longer be empty.
414+
shutdownInternal(shutdownStatus, /* forceTerminate= */ false);
413415
}
414416
}
415417
});

‎binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,23 @@
1717
package io.grpc.binder.internal;
1818

1919
import static com.google.common.truth.Truth.assertThat;
20+
import static io.grpc.binder.internal.BinderTransport.FIRST_CALL_ID;
21+
import static io.grpc.binder.internal.TransactionBuilder.newOutOfBandCloseTxnBuilder;
22+
import static io.grpc.binder.internal.TransactionBuilder.newStreamTxnToServerBuilder;
2023
import static org.mockito.ArgumentMatchers.any;
2124
import static org.mockito.ArgumentMatchers.anyInt;
2225
import static org.mockito.ArgumentMatchers.isNull;
2326
import static org.mockito.Mockito.doThrow;
2427
import static org.robolectric.Shadows.shadowOf;
2528

29+
import android.os.Binder;
2630
import android.os.IBinder;
2731
import android.os.Looper;
2832
import android.os.Parcel;
2933
import android.os.RemoteException;
3034
import com.google.common.collect.ImmutableList;
3135
import io.grpc.Attributes;
36+
import io.grpc.Metadata;
3237
import io.grpc.Status;
3338
import io.grpc.internal.FixedObjectPool;
3439
import io.grpc.internal.MockServerTransportListener;
@@ -122,4 +127,33 @@ public void testStartAfterShutdownNoIdle() throws Exception {
122127

123128
assertThat(transportListener.isTerminated()).isTrue();
124129
}
130+
131+
@Test
132+
public void testShutdownWhileCallActiveAndPreviousUnregisterPending() throws Exception {
133+
transport = newBinderServerTransportBuilder().setCallbackBinder(new Binder()).build();
134+
transport.start(transportListener);
135+
136+
// Start and finish call 1 while the transport is READY. This schedules a task on the executor
137+
// to check whether the transport should terminate if it is in the SHUTDOWN state.
138+
newStreamTxnToServerBuilder(0)
139+
.withPrefix("service/Method", new Metadata())
140+
.dispatchTo(transport, FIRST_CALL_ID);
141+
newOutOfBandCloseTxnBuilder(Status.CANCELLED).dispatchTo(transport, FIRST_CALL_ID);
142+
143+
// Before that scheduled task runs, start call 2 and initiate a graceful shutdown.
144+
newStreamTxnToServerBuilder(0)
145+
.withPrefix("service/Method", new Metadata())
146+
.dispatchTo(transport, FIRST_CALL_ID + 1);
147+
transport.shutdown();
148+
149+
// Run the pending task from call 1's unregistration. Because call 2 is still active, the
150+
// transport must not terminate yet.
151+
shadowOf(Looper.getMainLooper()).idle();
152+
assertThat(transportListener.isTerminated()).isFalse();
153+
154+
// Once call 2 finishes, the transport terminates.
155+
newOutOfBandCloseTxnBuilder(Status.CANCELLED).dispatchTo(transport, FIRST_CALL_ID + 1);
156+
shadowOf(Looper.getMainLooper()).idle();
157+
assertThat(transportListener.isTerminated()).isTrue();
158+
}
125159
}

0 commit comments

Comments
 (0)