Skip to content

Commit 91f3f4d

Browse files
authored
Fix a BinderServerTransport crash in the rare shutdown-before-start case (#12440)
The `isShutdown()` clause of `BinderServerTransport#start()` code was completely untested and did not in fact work. The problem is that if the listener does in fact arrive via start() after shutdown, BinderTransport's `shutdownInternal()` has already set the state to `SHUTDOWN_TERMINATED` (which is not a valid transition from itself). It has also already scheduled a call to notifyTerminated() and releaseExecutors(). This causes a duplicate call to `transportTerminated` and releasing the same executor twice. This commit changes `start()` to leave changing state and releasing executors to `shutdownInternal()`'s. `notifyTerminated()` either runs then (if already started) or from within `start()` (if not yet started) Fixes #12439.
1 parent b769f96 commit 91f3f4d

File tree

4 files changed

+275
-15
lines changed

4 files changed

+275
-15
lines changed

binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@
1515
*/
1616
package io.grpc.binder.internal;
1717

18-
import static com.google.common.base.Preconditions.checkNotNull;
19-
import static com.google.common.base.Preconditions.checkState;
20-
2118
import android.os.IBinder;
2219
import com.google.errorprone.annotations.concurrent.GuardedBy;
2320
import io.grpc.Attributes;
@@ -41,7 +38,9 @@
4138
public final class BinderServerTransport extends BinderTransport implements ServerTransport {
4239

4340
private final List<ServerStreamTracer.Factory> streamTracerFactories;
44-
@Nullable private ServerTransportListener serverTransportListener;
41+
42+
@GuardedBy("this")
43+
private final SimplePromise<ServerTransportListener> listenerPromise = new SimplePromise<>();
4544

4645
/**
4746
* Constructs a new transport instance.
@@ -69,13 +68,8 @@ public BinderServerTransport(
6968
* @param serverTransportListener where this transport will report events
7069
*/
7170
public synchronized void start(ServerTransportListener serverTransportListener) {
72-
checkState(this.serverTransportListener == null, "Already started!");
73-
this.serverTransportListener = checkNotNull(serverTransportListener, "serverTransportListener");
74-
if (isShutdown()) {
75-
setState(TransportState.SHUTDOWN_TERMINATED);
76-
notifyTerminated();
77-
releaseExecutors();
78-
} else {
71+
this.listenerPromise.set(serverTransportListener);
72+
if (!isShutdown()) {
7973
sendSetupTransaction();
8074
// Check we're not shutdown again, since a failure inside sendSetupTransaction (or a callback
8175
// it triggers), could have shut us down.
@@ -90,11 +84,16 @@ StatsTraceContext createStatsTraceContext(String methodName, Metadata headers) {
9084
return StatsTraceContext.newServerContext(streamTracerFactories, methodName, headers);
9185
}
9286

87+
/**
88+
* Reports a new ServerStream requested by the remote client.
89+
*
90+
* <p>Precondition: {@link #start(ServerTransportListener)} must already have been called.
91+
*/
9392
synchronized Status startStream(ServerStream stream, String methodName, Metadata headers) {
9493
if (isShutdown()) {
9594
return Status.UNAVAILABLE.withDescription("transport is shutdown");
9695
} else {
97-
serverTransportListener.streamCreated(stream, methodName, headers);
96+
listenerPromise.get().streamCreated(stream, methodName, headers);
9897
return Status.OK;
9998
}
10099
}
@@ -108,9 +107,7 @@ void notifyShutdown(Status status) {
108107
@Override
109108
@GuardedBy("this")
110109
void notifyTerminated() {
111-
if (serverTransportListener != null) {
112-
serverTransportListener.transportTerminated();
113-
}
110+
listenerPromise.runWhenSet(ServerTransportListener::transportTerminated);
114111
}
115112

116113
@Override
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2025 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.binder.internal;
18+
19+
import static com.google.common.base.Preconditions.checkNotNull;
20+
import static com.google.common.base.Preconditions.checkState;
21+
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
25+
/**
26+
* Placeholder for an object that will be provided later.
27+
*
28+
* <p>Similar to {@link com.google.common.util.concurrent.SettableFuture}, except it cannot fail or
29+
* be cancelled. Most importantly, this class guarantees that {@link Listener}s run one-at-a-time
30+
* and in the same order that they were scheduled. This conveniently matches the expectations of
31+
* most listener interfaces in the io.grpc universe.
32+
*
33+
* <p>Not safe for concurrent use by multiple threads. Thread-compatible for callers that provide
34+
* synchronization externally.
35+
*/
36+
public class SimplePromise<T> {
37+
private T value;
38+
private List<Listener<T>> pendingListeners; // Allocated lazily in the hopes it's never needed.
39+
40+
/**
41+
* Provides the promised object and runs any pending listeners.
42+
*
43+
* @throws IllegalStateException if this method has already been called
44+
* @throws RuntimeException if some pending listener threw when we tried to run it
45+
*/
46+
public void set(T value) {
47+
checkNotNull(value, "value");
48+
checkState(this.value == null, "Already set!");
49+
this.value = value;
50+
if (pendingListeners != null) {
51+
for (Listener<T> listener : pendingListeners) {
52+
listener.notify(value);
53+
}
54+
pendingListeners = null;
55+
}
56+
}
57+
58+
/**
59+
* Returns the promised object, under the assumption that it's already been set.
60+
*
61+
* <p>Compared to {@link #runWhenSet(Listener)}, this method may be a more efficient way to access
62+
* the promised value in the case where you somehow know externally that {@link #set(T)} has
63+
* "happened-before" this call.
64+
*
65+
* @throws IllegalStateException if {@link #set(T)} has not yet been called
66+
*/
67+
public T get() {
68+
checkState(value != null, "Not yet set!");
69+
return value;
70+
}
71+
72+
/**
73+
* Runs the given listener when this promise is fulfilled, or immediately if already fulfilled.
74+
*
75+
* @throws RuntimeException if already fulfilled and 'listener' threw when we tried to run it
76+
*/
77+
public void runWhenSet(Listener<T> listener) {
78+
if (value != null) {
79+
listener.notify(value);
80+
} else {
81+
if (pendingListeners == null) {
82+
pendingListeners = new ArrayList<>();
83+
}
84+
pendingListeners.add(listener);
85+
}
86+
}
87+
88+
/**
89+
* An object that wants to get notified when a SimplePromise has been fulfilled.
90+
*/
91+
public interface Listener<T> {
92+
/**
93+
* Indicates that the associated SimplePromise has been fulfilled with the given `value`.
94+
*/
95+
void notify(T value);
96+
}
97+
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
import static org.mockito.ArgumentMatchers.anyInt;
2222
import static org.mockito.ArgumentMatchers.isNull;
2323
import static org.mockito.Mockito.doThrow;
24+
import static org.mockito.Mockito.when;
2425
import static org.robolectric.Shadows.shadowOf;
2526

27+
import android.os.DeadObjectException;
2628
import android.os.IBinder;
2729
import android.os.Looper;
2830
import android.os.Parcel;
@@ -94,6 +96,27 @@ public void testSetupTransactionFailureReportsMultipleTerminations_b153460678()
9496
assertThat(transportListener.isTerminated()).isTrue();
9597
}
9698

99+
@Test
100+
public void testStartAfterShutdownAndIdle() throws Exception {
101+
transport = newBinderServerTransportBuilder().build();
102+
transport.shutdownNow(Status.UNKNOWN.withDescription("reasons"));
103+
shadowOf(Looper.getMainLooper()).idle();
104+
transport.start(transportListener);
105+
shadowOf(Looper.getMainLooper()).idle();
106+
107+
assertThat(transportListener.isTerminated()).isTrue();
108+
}
109+
110+
@Test
111+
public void testStartAfterShutdownNoIdle() throws Exception {
112+
transport = newBinderServerTransportBuilder().build();
113+
transport.shutdownNow(Status.UNKNOWN.withDescription("reasons"));
114+
transport.start(transportListener);
115+
shadowOf(Looper.getMainLooper()).idle();
116+
117+
assertThat(transportListener.isTerminated()).isTrue();
118+
}
119+
97120
static class BinderServerTransportBuilder {
98121
ObjectPool<ScheduledExecutorService> executorServicePool;
99122
Attributes attributes;
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright 2025 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.binder.internal;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
import static org.junit.Assert.assertThrows;
21+
import static org.mockito.Mockito.inOrder;
22+
import static org.mockito.Mockito.never;
23+
import static org.mockito.Mockito.times;
24+
import static org.mockito.Mockito.verify;
25+
26+
import io.grpc.binder.internal.SimplePromise.Listener;
27+
import org.junit.Before;
28+
import org.junit.Rule;
29+
import org.junit.Test;
30+
import org.junit.runner.RunWith;
31+
import org.junit.runners.JUnit4;
32+
import org.mockito.InOrder;
33+
import org.mockito.Mock;
34+
import org.mockito.junit.MockitoJUnit;
35+
import org.mockito.junit.MockitoRule;
36+
37+
@RunWith(JUnit4.class)
38+
public final class SimplePromiseTest {
39+
40+
private static final String FULFILLED_VALUE = "a fulfilled value";
41+
42+
@Mock private Listener<String> mockListener1;
43+
@Mock private Listener<String> mockListener2;
44+
@Rule public final MockitoRule mocks = MockitoJUnit.rule();
45+
46+
private SimplePromise<String> promise = new SimplePromise<>();
47+
48+
@Before
49+
public void setUp() {
50+
}
51+
52+
@Test
53+
public void get_beforeFulfilled_throws() {
54+
IllegalStateException e = assertThrows(IllegalStateException.class, () -> promise.get());
55+
assertThat(e).hasMessageThat().isEqualTo("Not yet set!");
56+
}
57+
58+
@Test
59+
public void get_afterFulfilled_returnsValue() {
60+
promise.set(FULFILLED_VALUE);
61+
assertThat(promise.get()).isEqualTo(FULFILLED_VALUE);
62+
}
63+
64+
@Test
65+
public void set_withNull_throws() {
66+
assertThrows(NullPointerException.class, () -> promise.set(null));
67+
}
68+
69+
@Test
70+
public void set_calledTwice_throws() {
71+
promise.set(FULFILLED_VALUE);
72+
IllegalStateException e =
73+
assertThrows(IllegalStateException.class, () -> promise.set("another value"));
74+
assertThat(e).hasMessageThat().isEqualTo("Already set!");
75+
}
76+
77+
@Test
78+
public void runWhenSet_beforeFulfill_listenerIsNotifiedUponSet() {
79+
promise.runWhenSet(mockListener1);
80+
81+
// Should not have been called yet.
82+
verify(mockListener1, never()).notify(FULFILLED_VALUE);
83+
84+
promise.set(FULFILLED_VALUE);
85+
86+
// Now it should be called.
87+
verify(mockListener1, times(1)).notify(FULFILLED_VALUE);
88+
}
89+
90+
@Test
91+
public void runWhenSet_afterSet_listenerIsNotifiedImmediately() {
92+
promise.set(FULFILLED_VALUE);
93+
promise.runWhenSet(mockListener1);
94+
95+
// Should have been called immediately.
96+
verify(mockListener1, times(1)).notify(FULFILLED_VALUE);
97+
}
98+
99+
@Test
100+
public void multipleListeners_addedBeforeSet_allNotifiedInOrder() {
101+
promise.runWhenSet(mockListener1);
102+
promise.runWhenSet(mockListener2);
103+
104+
promise.set(FULFILLED_VALUE);
105+
106+
InOrder inOrder = inOrder(mockListener1, mockListener2);
107+
inOrder.verify(mockListener1).notify(FULFILLED_VALUE);
108+
inOrder.verify(mockListener2).notify(FULFILLED_VALUE);
109+
}
110+
111+
@Test
112+
public void listenerThrows_duringSet_propagatesException() {
113+
// A listener that will throw when notified.
114+
Listener<String> throwingListener =
115+
(value) -> {
116+
throw new UnsupportedOperationException("Listener failed");
117+
};
118+
119+
promise.runWhenSet(throwingListener);
120+
121+
// Fulfilling the promise should now throw the exception from the listener.
122+
UnsupportedOperationException e =
123+
assertThrows(UnsupportedOperationException.class, () -> promise.set(FULFILLED_VALUE));
124+
assertThat(e).hasMessageThat().isEqualTo("Listener failed");
125+
}
126+
127+
@Test
128+
public void listenerThrows_whenAddedAfterSet_propagatesException() {
129+
promise.set(FULFILLED_VALUE);
130+
131+
// A listener that will throw when notified.
132+
Listener<String> throwingListener =
133+
(value) -> {
134+
throw new UnsupportedOperationException("Listener failed");
135+
};
136+
137+
// Running the listener should throw immediately because the promise is already fulfilled.
138+
UnsupportedOperationException e =
139+
assertThrows(
140+
UnsupportedOperationException.class, () -> promise.runWhenSet(throwingListener));
141+
assertThat(e).hasMessageThat().isEqualTo("Listener failed");
142+
}
143+
}

0 commit comments

Comments
 (0)