Google Cloud Firestore で Channel ManagedChannelImpl was not shutdown properly!!! が発生する

## 概要

- Google Cloud Firestore API Client Library for Java で「Channel ManagedChannelImpl was not shutdown properly!!!」というエラーメッセージが出力される
- 今回の環境: Java 11 + Google Cloud Firestore API Client Library for Java 1.31.0 

## エラーメッセージ例

```
2019-11-25 05:57:46.557 ERROR 41081 --- [nio-8080-exec-5] i.g.i.ManagedChannelOrphanWrapper        : *~*~*~ Channel ManagedChannelImpl{logId=5, target=firestore.googleapis.com:443} was not shutdown properly!!! ~*~*~*
  Make sure to call shutdown()/shutdownNow() and wait until awaitTermination() returns true.
```

```
java.lang.RuntimeException: ManagedChannel allocation site
  at io.grpc.internal.ManagedChannelOrphanWrapper$ManagedChannelReference.<init>(ManagedChannelOrphanWrapper.java:94)
  at io.grpc.internal.ManagedChannelOrphanWrapper.<init>(ManagedChannelOrphanWrapper.java:52)
  at io.grpc.internal.ManagedChannelOrphanWrapper.<init>(ManagedChannelOrphanWrapper.java:43)
  at io.grpc.internal.AbstractManagedChannelImplBuilder.build(AbstractManagedChannelImplBuilder.java:512)
  at com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.createSingleChannel(InstantiatingGrpcChannelProvider.java:296)
  at com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.createChannel(InstantiatingGrpcChannelProvider.java:194)
  at com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.getTransportChannel(InstantiatingGrpcChannelProvider.java:186)
  at com.google.api.gax.rpc.ClientContext.create(ClientContext.java:155)
  at com.google.api.gax.rpc.ClientContext.create(ClientContext.java:122)
  at com.google.cloud.firestore.spi.v1.GrpcFirestoreRpc.<init>(GrpcFirestoreRpc.java:122)
  at com.google.cloud.firestore.FirestoreOptions$DefaultFirestoreRpcFactory.create(FirestoreOptions.java:90)
  at com.google.cloud.firestore.FirestoreOptions$DefaultFirestoreRpcFactory.create(FirestoreOptions.java:82)
  at com.google.cloud.ServiceOptions.getRpc(ServiceOptions.java:538)
  at com.google.cloud.firestore.FirestoreOptions.getFirestoreRpc(FirestoreOptions.java:385)
  at com.google.cloud.firestore.FirestoreImpl.<init>(FirestoreImpl.java:77)
  at com.google.cloud.firestore.FirestoreOptions$DefaultFirestoreFactory.create(FirestoreOptions.java:73)
  at com.google.cloud.firestore.FirestoreOptions$DefaultFirestoreFactory.create(FirestoreOptions.java:66)
  at com.google.cloud.ServiceOptions.getService(ServiceOptions.java:518)
```

## エラーメッセージ部分の抜粋

エラーメッセージを読むと、「チャネルが適切にシャットダウンされていない。shutdown や shutdownNow をコールして awaitTermination が true を返すまで待つ必要がある」とのこと。

```
Channel ManagedChannelImpl was not shutdown properly!!!
Make sure to call shutdown()/shutdownNow() and wait until awaitTermination() returns true.
```

## リソースを解放せずにガベージコレクションの対象となった場合にエラーメッセージを出力している

shutdown や shutdownNow メソッドを呼び出さずに放置したオブジェクトがガベージコレクトの対象となったときに出力されるという情報がある。

[While starting managed channels on client we get error: \`Channel for target was not shutdown properly\!\!\!\` · Issue \#4032 · grpc/grpc\-java · GitHub](https://github.com/grpc/grpc-java/issues/4032)

> It means you didn't call shutdown() or shutdownNow() on your channel, and it was garbage collected.

gRPC のリソースを解放していないままガベージコレクションされたということらしい。

[PubSub runs into \`java\.lang\.RuntimeException: ManagedChannel allocation site\` when a new publisher is created · Issue \#3648 · googleapis/google\-cloud\-java · GitHub](https://github.com/googleapis/google-cloud-java/issues/3648)

> I don't think this is actually related to creating new Publisher, though gRPC uses that as an opportunity to tell us that we're leaking channels.

## ライブラリのソースコード該当箇所

Google Cloud Firestore API Client Library for Java は gRPC-Java ライブラリを使用している。
今回のエラーメッセージは gRPC-Java ライブラリが出力している。
該当箇所のソースコードを以下に示す。

[grpc\-java/ManagedChannelOrphanWrapper\.java at v1\.24\.1 · grpc/grpc\-java · GitHub](https://github.com/grpc/grpc-java/blob/v1.24.1/core/src/main/java/io/grpc/internal/ManagedChannelOrphanWrapper.java)

```java
@Override
public ManagedChannel shutdown() {
  phantom.shutdown = true;
  phantom.clear();
  return super.shutdown();
}

@Override
public ManagedChannel shutdownNow() {
  phantom.shutdown = true;
  phantom.clear();
  return super.shutdownNow();
}
```

```java
ManagedChannelReference(
    ManagedChannelOrphanWrapper orphanable,
    ManagedChannel channel,
    ReferenceQueue<ManagedChannelOrphanWrapper> refqueue,
    ConcurrentMap<ManagedChannelReference, ManagedChannelReference> refs) {
  super(orphanable, refqueue);
  allocationSite = new SoftReference<>(
      ENABLE_ALLOCATION_TRACKING
          ? new RuntimeException("ManagedChannel allocation site")
          : missingCallSite);
  this.channelStr = channel.toString();
  this.refqueue = refqueue;
  this.refs = refs;
  this.refs.put(this, this);
  cleanQueue(refqueue);
}
```

```java
@VisibleForTesting
static int cleanQueue(ReferenceQueue<ManagedChannelOrphanWrapper> refqueue) {
  ManagedChannelReference ref;
  int orphanedChannels = 0;
  while ((ref = (ManagedChannelReference) refqueue.poll()) != null) {
    RuntimeException maybeAllocationSite = ref.allocationSite.get();
    ref.clearInternal(); // technically the reference is gone already.
    if (!ref.shutdown) {
      orphanedChannels++;
      Level level = Level.SEVERE;
      if (logger.isLoggable(level)) {
        String fmt =
            "*~*~*~ Channel {0} was not shutdown properly!!! ~*~*~*"
                + System.getProperty("line.separator")
                + "    Make sure to call shutdown()/shutdownNow() and wait "
                + "until awaitTermination() returns true.";
        LogRecord lr = new LogRecord(level, fmt);
        lr.setLoggerName(logger.getName());
        lr.setParameters(new Object[] {ref.channelStr});
        lr.setThrown(maybeAllocationSite);
        logger.log(lr);
      }
    }
  }
  return orphanedChannels;
}
```

## 解決策

### 都度 Firestore オブジェクトを close する

Firestore インターフェースの close メソッドをコールすると、関連付けられた gRPC チャネルがクローズされる。

[Firestore \(Google Cloud 0\.119\.0\-alpha API\)](https://googleapis.dev/java/google-cloud-clients/latest/com/google/cloud/firestore/Firestore.html)

> Closes the gRPC channels associated with this instance and frees up their resources. This method blocks until all channels are closed. Once this method is called, this Firestore client is no longer usable.

Firestore インターフェースは AutoCloseable を継承しているため、try-with-resources で自動的に close メソッドを呼び出すことも可能。

Firestore を close した場合は、再度生成する際には FirestoreOptions も新しく生成する必要がある。

参考: [Google Cloud Firestore で IllegalStateException: Firestore client has already been closed が発生する \- Qiita](https://qiita.com/niwasawa/items/0c53f3fc62e253b7c814)

### Firestore を使い回す

もうひとつの解決策として、Firestore を close せずにシングルトンパターンなどでオブジェクトを使い回しても良い。

## 参考資料

- [Overview \(Google Cloud 0\.119\.0\-alpha API\)](https://googleapis.dev/java/google-cloud-clients/latest/)
- [GitHub \- grpc/grpc\-java: The Java gRPC implementation\. HTTP/2 based RPC](https://github.com/grpc/grpc-java)