Firestoreからエクスポートしたデータがエミュレータにインポートできなくなった問題に対処する

Firestoreにはエミュレータが用意されていて、手元の環境でも似たようなものが動かせるようになっています。開発するにはこれは必須といったところで、例えば開発環境のデータを手元の環境にインポートして使うということもよくしています。

エミュレータにインポートする方法は簡単で、まずは以下のようにgcloudコマンドでデータをエクスポートし、gsutilコマンドでエクスポート先のディレクトリを保存します。

gcloud firestore export gs://<bucket_name>
gsutil -m cp -r "gs://<bucket_name>/<export_dir>" .

あとはfirebaseコマンドでエミュレータを起動する際に--importオプションでエスクポートしたディレクトリを指定すればそのまま動かせます。

firebase --project demo- --only firestore emulators:start --import <exported_dir>

ただ、ある日から(2023/10/21時点では直っていない)インポートしようとすると以下のエラーメッセージでエミュレータが起動できなくなってしまいました。

Oct 21, 2023 8:49:22 PM com.google.cloud.datastore.emulator.firestore.CloudFirestore main
SEVERE: Exiting due to unexpected exception.
com.google.cloud.datastore.core.exception.DatastoreException: Message missing required fields: kind_info[0].kind
at com.google.cloud.datastore.util.leveldb.ExportImportUtil.parseBackupFile(ExportImportUtil.java:378)
at com.google.cloud.datastore.util.leveldb.ExportImportUtil.fetchEntities(ExportImportUtil.java:88)
at com.google.cloud.datastore.emulator.firestore.CloudFirestore.init(CloudFirestore.java:181)
at com.google.cloud.datastore.emulator.firestore.CloudFirestore.startLocally(CloudFirestore.java:115)
at com.google.cloud.datastore.emulator.firestore.CloudFirestore.main(CloudFirestore.java:96)
Caused by: com.google.protobuf.InvalidProtocolBufferException: Message missing required fields: kind_info[0].kind
at com.google.protobuf.UninitializedMessageException.asInvalidProtocolBufferException(UninitializedMessageException.java:79)
at com.google.protobuf.AbstractParser.checkMessageInitialized(AbstractParser.java:73)
at com.google.protobuf.AbstractParser.parseFrom(AbstractParser.java:91)
at com.google.protobuf.AbstractParser.parseFrom(AbstractParser.java:96)
at com.google.protobuf.AbstractParser.parseFrom(AbstractParser.java:48)
at com.google.cloud.datastore.util.leveldb.ExportImportUtil.parseBackupFile(ExportImportUtil.java:376)
... 4 more

対処方法

github.com

対処は簡単、このリポジトリから以下のコマンドを実行するだけです。あとは通常通りエミュレータにインポートすればそのまま起動します。

poetry install
poetry run python workaround.py <export_dir>

せっかくなので、原因の詳細を書いておきます。まず、エクスポートされたディレクトリは以下のような構造になっています。そもそもFirestoreのデータの内部構造としては一部はProtocol Bufferで、そのほかはLevelDBのlog formatの形式で保存されています。

2023-10-21T10:49:07_61766
├── 2023-10-21T10:49:07_61766.overall_export_metadata
└── all_namespaces
    └── all_kinds
        ├── all_namespaces_all_kinds.export_metadata
        └── output-0

all_namespaces_all_kinds.export_metadataは単にProtocol Bufferでシリアライズされたデータなのでprotocコマンドでダンプできます。

% protoc --decode_raw < 2023-10-21T10:49:07_61766/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata
1 {
  1: "2023-10-21T10:49:07_61766"
  2: 1697885347297211
}
2 {
  2: "output-0"
  5: 1004733923
}

ただ、これだけだとフィールド番号だけなのでなんとなくの構造しかわかりません。Firebaseエミュレータのjarの中にはProtocol Bufferのdescriptor dataが含まれているので、それを利用して.protoの定義を生成して*1フィールド名を復元するとこのようになっています。

backup_info {
  backup_name: "2023-10-21T10:49:07_61766"
  start_timestamp: 1697885347297211
}
kind_info {
  file: "output-0"
}

エミュレータにインポートできていた過去のデータを確認してみると、以下のようにbackup_info.start_timestamp, kind_info.kind, kind_info.entity_schema.kindのフィールドが存在していました。

backup_info {
  backup_name: "2023-10-21T10:49:07_61766"
  start_timestamp: 1697885347297211
  end_timestamp: ...
}
kind_info {
  kind: "__all__"
  file: "output-0"
  entity_schema {
    kind: "__all__"
  }
}

Message missing required fields: kind_info[0].kindというエラーメッセージからも明らかにこのフィールドがないことからインポートできなくなってしまってことがわかります。

つまりは消えてしまったフィールドを無理矢理足してしまえばエミュレータにインポートできることにはなります。なんとなくend_timestampがないという状況を察すると、インポート処理が正しく終了状態になっていないバグのような気がしているのですが、これは後ほどバグ報告をしようと思います(ただ、そもそもエミュレータにはインポートできないという仕様であったりすると悲しいのですが)。

*1:.protoの形式になっているとそもそも読みやすい、かつ言語ごとにコード生成ができるのが便利です。いいかんじに戻すツールを自作していたのですが紹介はまたの機会に……