Add LifecycleOnDestroyHelper to support shutdown of channel/server on Android lifecycle changes (#8568)

This commit is contained in:
markb74 2021-09-29 20:04:47 +02:00 committed by GitHub
parent 28f2647aaf
commit fcc7b9694e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 185 additions and 0 deletions

View File

@ -49,9 +49,12 @@ dependencies {
implementation libraries.androidx_annotation
implementation libraries.androidx_core
implementation libraries.androidx_lifecycle_common
implementation libraries.guava
testImplementation libraries.androidx_core
testImplementation libraries.androidx_test
testImplementation libraries.androidx_lifecycle_common
testImplementation libraries.androidx_lifecycle_service
testImplementation libraries.junit
testImplementation libraries.mockito
testImplementation (libraries.robolectric) {

View File

@ -0,0 +1,85 @@
/*
* 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.binder;
import androidx.annotation.MainThread;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.Lifecycle.State;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import io.grpc.ManagedChannel;
import io.grpc.Server;
/**
* Helps work around certain quirks of {@link Lifecycle#addObserver} and {@link State#DESTROYED}.
*
* <p>In particular, calls to {@link Lifecycle#addObserver(LifecycleObserver)} are silently ignored
* if the owner is already destroyed.
*/
public final class LifecycleOnDestroyHelper {
private LifecycleOnDestroyHelper() {}
/**
* Arranges for {@link ManagedChannel#shutdownNow()} to be called on {@code channel} just before
* {@code lifecycle} is destroyed, or immediately if {@code lifecycle} is already destroyed.
*
* <p>Must only be called on the application's main thread.
*/
@MainThread
public static void shutdownUponDestruction(Lifecycle lifecycle, ManagedChannel channel) {
if (lifecycle.getCurrentState() == State.DESTROYED) {
channel.shutdownNow();
} else {
lifecycle.addObserver(
new LifecycleEventObserver() {
@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
source.getLifecycle().removeObserver(this);
channel.shutdownNow();
}
}
});
}
}
/**
* Arranges for {@link Server#shutdownNow()} to be called on {@code server} just before {@code
* lifecycle} is destroyed, or immediately if {@code lifecycle} is already destroyed.
*
* <p>Must only be called on the application's main thread.
*/
@MainThread
public static void shutdownUponDestruction(Lifecycle lifecycle, Server server) {
if (lifecycle.getCurrentState() == State.DESTROYED) {
server.shutdownNow();
} else {
lifecycle.addObserver(
new LifecycleEventObserver() {
@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
source.getLifecycle().removeObserver(this);
server.shutdownNow();
}
}
});
}
}
}

View File

@ -0,0 +1,96 @@
/*
* 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.binder;
import static android.os.Looper.getMainLooper;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.robolectric.Shadows.shadowOf;
import androidx.lifecycle.LifecycleService;
import io.grpc.ManagedChannel;
import io.grpc.Server;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.android.controller.ServiceController;
@RunWith(RobolectricTestRunner.class)
public final class LifecycleOnDestroyHelperTest {
@Rule public final MockitoRule mocks = MockitoJUnit.rule();
private ServiceController<MyService> sourceController;
private MyService sourceService;
@Mock ManagedChannel mockChannel;
@Mock Server mockServer;
@Before
public void setup() {
sourceController = Robolectric.buildService(MyService.class);
sourceService = sourceController.create().get();
}
@Test
public void shouldShutdownChannelUponSourceDestruction() {
LifecycleOnDestroyHelper.shutdownUponDestruction(sourceService.getLifecycle(), mockChannel);
shadowOf(getMainLooper()).idle();
verifyNoInteractions(mockChannel);
sourceController.destroy();
shadowOf(getMainLooper()).idle();
verify(mockChannel).shutdownNow();
}
@Test
public void shouldShutdownChannelForInitiallyDestroyedSource() {
sourceController.destroy();
shadowOf(getMainLooper()).idle();
LifecycleOnDestroyHelper.shutdownUponDestruction(sourceService.getLifecycle(), mockChannel);
verify(mockChannel).shutdownNow();
}
@Test
public void shouldShutdownServerUponServiceDestruction() {
LifecycleOnDestroyHelper.shutdownUponDestruction(sourceService.getLifecycle(), mockServer);
shadowOf(getMainLooper()).idle();
verifyNoInteractions(mockServer);
sourceController.destroy();
shadowOf(getMainLooper()).idle();
verify(mockServer).shutdownNow();
}
@Test
public void shouldShutdownServerForInitiallyDestroyedSource() {
sourceController.destroy();
shadowOf(getMainLooper()).idle();
LifecycleOnDestroyHelper.shutdownUponDestruction(sourceService.getLifecycle(), mockServer);
verify(mockServer).shutdownNow();
}
private static class MyService extends LifecycleService {}
}

View File

@ -191,6 +191,7 @@ subprojects {
guava_testlib: "com.google.guava:guava-testlib:${guavaVersion}",
androidx_annotation: "androidx.annotation:annotation:1.1.0",
androidx_core: "androidx.core:core:1.3.0",
androidx_lifecycle_common: "androidx.lifecycle:lifecycle-common:2.3.0",
androidx_lifecycle_service: "androidx.lifecycle:lifecycle-service:2.3.0",
androidx_test: "androidx.test:core:1.3.0",
androidx_test_rules: "androidx.test:rules:1.3.0",