Skip to content

Commit

Permalink
Add LifecycleOnDestroyHelper to support shutdown of channel/server on…
Browse files Browse the repository at this point in the history
… Android lifecycle changes (#8568)
  • Loading branch information
markb74 authored Sep 29, 2021
1 parent 28f2647 commit fcc7b96
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 0 deletions.
3 changes: 3 additions & 0 deletions binder/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
85 changes: 85 additions & 0 deletions binder/src/main/java/io/grpc/binder/LifecycleOnDestroyHelper.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
});
}
}
}
Original file line number Diff line number Diff line change
@@ -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 {}
}
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit fcc7b96

Please sign in to comment.