Google Cloud Firestore で IllegalStateException: Firestore client has already been closed が発生する

## 概要

- Google Cloud Firestore API Client Library for Java で IllegalStateException: Firestore client has already been closed が発生する
- Firestore オブジェクトの close メソッドを呼び出した後で、 FirestoreOptions の getService メソッドで取得した Firestore オブジェクトを使用すると発生する
- 今回の環境: Java 11 + Google Cloud Firestore API Client Library for Java 1.31.0 

## エラーメッセージ例

```
java.lang.IllegalStateException: Firestore client has already been closed
  at com.google.common.base.Preconditions.checkState(Preconditions.java:511)
  at com.google.cloud.firestore.FirestoreImpl.streamRequest(FirestoreImpl.java:473)
  at com.google.cloud.firestore.Query.stream(Query.java:1141)
  at com.google.cloud.firestore.Query.get(Query.java:1181)
  at com.google.cloud.firestore.Query.get(Query.java:1151)
```

## Firestore インターフェースの close メソッド

close メソッドをコールすると、関連付けられた gRPC チャネルがクローズされ、この Firestore オブジェクトが使用できなくなる。

[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 メソッドを呼び出すことができる。

[try\-with\-resources 文](https://docs.oracle.com/javase/jp/7/technotes/guides/language/try-with-resources.html)

> try-with-resources 文は、1 つ以上のリソースを宣言する try 文です。リソースは、プログラムでの使用が終わったら閉じられなければいけないオブジェクトです。try-with-resources 文は、文の終わりで各リソースが確実に閉じられるようにします。java.io.Closeable を実装しているすべてのオブジェクトも含め、java.lang.AutoCloseable インタフェースを実装しているオブジェクトはリソースとして使用できます。

## エラー発生コードの例

以下のようなコードでエラーが発生していた。

```java
import com.google.api.core.ApiFuture;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.firestore.CollectionReference;
import com.google.cloud.firestore.DocumentSnapshot;
import com.google.cloud.firestore.Firestore;
import com.google.cloud.firestore.FirestoreOptions;
import com.google.cloud.firestore.Query;
import com.google.cloud.firestore.QuerySnapshot;

public class MyFirestore {

  private final String projectId;
  private final GoogleCredentials credentials;
  private Firestore firestore;
  private final String collectionPath;
  private final FirestoreOptions firestoreOptions;

  public MyFirestore(String projectId, GoogleCredentials credentials, String collectionPath) {
    this.projectId = projectId;
    this.credentials = credentials;
    this.collectionPath = collectionPath;
    this.firestoreOptions =
      FirestoreOptions.newBuilder()
        .setCredentials(credentials)
        .setProjectId(projectId)
        .build();
  }

  public <T> T get(String id, Class<T> type) throws Exception {
    // try-with-resources: 使用後に自動で Firestore#close を呼び出す
    try (Firestore firestore = firestoreOptions.getService()) {
      // コレクションを取得
      CollectionReference c = firestore.collection(collectionPath);
      // コレクションからドキュメントを取得
      Query query = c.whereEqualTo("mykey", id);
      ApiFuture<QuerySnapshot> querySnapshot = query.get();
      for (DocumentSnapshot doc : querySnapshot.get().getDocuments()) {
        return doc.toObject(type);
      }
      return null;
    }
  }
}
```

## 解決策

### 都度 FirestoreOptions を生成する

Firestore オブジェクトを close した場合は、再度 FirestoreOptions を生成する必要がある。

毎回 Firestore#close をコールするのであれば、以下のように FirestoreOptions も都度生成して Firestore を取得するようにする。

```java
private static Firestore getFirestore(String projectId, GoogleCredentials credentials) {
  FirestoreOptions firestoreOptions =
    FirestoreOptions.newBuilder()
      .setCredentials(credentials)
      .setProjectId(projectId)
      .build();
  return firestoreOptions.getService();
}
```

### Firestore を使い回す

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

## FirestoreOptions クラスの getService メソッド

FirestoreOptions のドキュメントには何も載っていない。

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

FirestoreOptions の親クラスである ServiceOptions には発生する例外についての説明は載っていない。

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

> Returns a Service object for the current service. For instance, when using Google Cloud Storage, it returns a Storage object.

ソースコードを見ると getService メソッドが他のクラスに委譲してオブジェクトを生成して返しているのがわかる。

[java\-core/ServiceOptions\.java at v1\.91\.2 · googleapis/java\-core · GitHub](https://github.com/googleapis/java-core/blob/v1.91.2/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java)

```java
private transient ServiceT service;
```

```java
public ServiceT getService() {
  if (shouldRefreshService(service)) {
    service = serviceFactory.create((OptionsT) this);
  }
  return service;
}
```

生成した Service オブジェクトは ServiceOptions オブジェクトが所有している。
再度 getService メソッドが呼び出されたときにはすでに生成済みの同じ Service オブジェクトを返す仕組みになっている。

## 参考資料

- [Overview \(Google Cloud 0\.119\.0\-alpha API\)](https://googleapis.dev/java/google-cloud-clients/latest/)
- [google\-cloud\-java/google\-cloud\-clients/google\-cloud\-firestore at master · googleapis/google\-cloud\-java · GitHub](https://github.com/googleapis/google-cloud-java/tree/master/google-cloud-clients/google-cloud-firestore)