firestoreのページネーションプラクティス
以下のようなシンプルなデータセットに対してのページネーションを考える。
DocumentID | likeCount |
---|---|
0 | 20 |
1 | 10 |
2 | 30 |
3 | 20 |
4 | 10 |
5 | 20 |
6 | 10 |
これを2件ずつlikeCountで降順ソートしたページネーションを実現したい場合は以下のような形になる。
Page | DocumentID(likeCount) |
---|---|
1 | 2(30) |
1 | 5(20) |
2 | 3(20) |
2 | 0(20) |
3 | 6(10) |
3 | 4(10) |
4 | 1(10) |
これを実現する手段で思い浮かぶのはオフセットだと思うが、firestoreではオフセットの利用が推奨されていない*1。
オフセットは使用しないでください。その代わりにカーソルを使用します。オフセットを使用すると、スキップされたドキュメントがアプリケーションに返されなくなりますが、内部ではスキップされたドキュメントも引き続き取得されています。スキップされたドキュメントはクエリのレイテンシに影響し、このようなドキュメントの取得に必要な読み取りオペレーションは課金対象になります。
refs: Cloud Firestore のベスト プラクティス | Firebase
ここで出てくるカーソルは クエリカーソルを使用したデータのページ設定 | Firebase で説明されている。 ページネーションを実現する上で気をつけないといけないことは
- 同じデータが複数ページに跨って出ないようにする
- 同じ値(e.g. likeCount)の違うデータがページを跨ぐときにスキップされないようにする
という点だと思う。 しかし上記のリファレンスからは2点を達成するための方法がやや気づきにくい。
解法の一つとして Firestore で いいね順(Score順)Sort + Paging するポイント - Qiita の記事では createdAt
を使った複数のOrderByで解決する方法が記載されている。
最初に記載したデータセットに createdAt
のフィールドを追加すれば複数フィールドOrderByで大体のケースは正しいページネーションが行える。
今回は更新順の取得 => createdAt の値が下がっていく方向に進むので、開始日時を 1ms 減算
ただ複数フィールドのOrderByを行う方法はいくつかデメリットがある。
createdAt
が1ms以内に収まるドキュメントが複数あった場合に期待通りの結果にならないケースがある(はず)createdAt
がないと期待したページネーションができない*2- 複数フィールドOrderByを行うために複合インデックスを明示的に作成する必要がある
このような問題はカーソルの指定にDocumentSnapshotを指定することで解決できる。
var lastDoc *firestore.DocumentSnapshot for { client.Collection("collectionID").OrderBy("likeCount", firestore.Desc).Limit(2) if lastDoc != nil { q = q.StartAfter(lastDoc) } dss, _ := q.Documents(ctx).GetAll() if len(dss) == 0 { break } lastDoc = dss[len(dss)-1] }
これをすると最初の表に記載した期待通りの順番でドキュメントが返ってくる。
likeCountを使うものに書き換えると以下のようになる。
var lastLikeCount int64 = -1 for { client.Collection("collectionID").OrderBy("likeCount", firestore.Desc).Limit(2) if lastLikeCount != -1 { q = q.StartAfter(lastLikeCount) } dss, _ := q.Documents(ctx).GetAll() if len(dss) == 0 { break } if v, ok := dss[len(dss)-1].Data()["likeCount"].(int64); ok { lastLikeCount = v } }
これを実行すると以下のような結果が取得される。
赤字のデータは期待していたが実際には取得できなかったデータを示す。
Page | DocumentID(likeCount) |
---|---|
1 | 2(30) |
1 | 5(20) |
- | 3(20) |
- | 0(20) |
2 | 6(10) |
2 | 4(10) |
- | 1(10) |
期待通りの結果からいくつかドキュメントが欠損してしまっていることがわかる。*3 ちなみにStartAfterからStartAtに変更すると無限ループが発生してしまうのでStartAfterを使っている。
likeCountをcursorに指定する場合と比べ、ドキュメントを指定する場合はgoクライアントのリクエストで以下のような差分がある。
...
StartAt: &pb.Cursor{
Values: []*pb.Value{
{ValueType: &pb.Value_IntegerValue{IntegerValue: 20}},
+ {ValueType: &pb.Value_ReferenceValue{ReferenceValue: "projects/:projectID/databases/(default)/documents/collectionID/5"}},
},
},
...
上記の通り、ReferenceValueによるソートが入っている。 これは以下の点でメリットがある。
- このようなケースに関しては複合インデックスが不要
- 不要なフィールドを指定しなくて良い
- 1ms以内にドキュメントが複数作成されていても期待通りの結果が返る
ReferenceValueは恐らくfirestoreの中で特別な立ち位置にいるため複合インデックス無しでこのような事ができるのだと思う。
少なくともReferenceValueはソートされていると思われるレスポンスが返ってきている。*4
事実上複数フィールドに対してOrderByをしているのだが、ReferenceValueに関しては複合インデックスの制約を受けないようだ。
このあたりはもしかしたらドキュメントにちゃんと書かれてるかもしれないので見つけた人がいたらぜひ教えて下さい。
firebase hostingのSSL証明書の更新が遅れていた
firebase hostingで静的配信しているコンテンツに対してユーザから証明書のエラーが起きたという報告がありました。 この記事は同じ問題を踏んでいる人がいるかもしれないのでメモがてら書いています。
最初に結論を述べておくとGoogleの不具合だったようです。 以下は調査したメモですがGoogleの不具合だったようなのであまり参考にはならないと思います。
背景として
- カスタムドメインは未使用(
*.web.app
を使用) - 報告があった時刻と証明書の更新時刻が近い
といったことを確認しています。
まずカスタムドメインは利用していないので証明書に関してもよしなにfirebase hostingが更新してくれると思っています。
Firebase automatically provisions SSL certificates for all your domains so that all your content is served securely.
refs: Firebase Hosting | Firebase
報告された時刻は2/16の11:17なのですが、使用して動かないなと思い調べた上で証明書の問題という報告をしてくれたと推測できるので、 実際に現象を確認したのはもう少し前だと思われます。 現在確認した証明書の情報としては以下のようなものでした。
証明書の更新周期に関してあまり詳しくないのですが、Let's Encryptと同じ挙動をしているとするなら
- 証明書を更新した時刻がタイムスタンプ
- 有効になる日付は更新した時刻の1時間前から有効
- 証明書を配信した段階からvalidである
といった形だと思います。
そのことから実際に証明書が更新されたタイミングは11:12に更新されたものと思われます。 そして仮にGoogleが3ヶ月周期でbatchを回しているとなると10:12-11:12の間は証明書がinvalidだったのではないかと推測しています。 ただ、これに関しては既に終わってしまったことなので検知しようがないため、やれることといえば次回更新と思われる5/16に同様の観測をするぐらいしかないですね...。
とここまで書いた後に同様の症状を観測している人がいないかを調べたところ、他にも観測していた人がいるようです。
This should be fixed now, can you confirm? Our apologies for the inconvenience.
— Michael Bleigh (@mbleigh) February 16, 2020
こっちもhttps://t.co/RE8mBATvcHドメインの方でSSL証明書が無効になってますね。https://t.co/AZ5Z6yvZVBの方だと問題なく観れるみたいですけど
— YASU@オトモハンター (@yasu2704) February 16, 2020
なおったっぽい
— ふくい 👨💻 (@var_fukui) February 16, 2020
11:45 復旧確認しましたー
— 夕星/ゆうづつ (@yudutu_ps) February 16, 2020
どうやら02/16 6:50JST-02/16 11:45JST頃まで何かバグっていたのが原因だったようです。
恐らく次回の更新では大丈夫でしょう。
(02/16 16:30時点ではhttps://status.firebase.google.com/summary にインシデントはまだ載っていない模様...)
じぶん ReleaseNote v1.0
2020年、1月版です。
年を跨いだのでメジャーバージョンが上がりました。
来年も引き続き更新できればメジャーバージョンが上がります。
Tech寄り
- firebaseの静的サイトの自動デプロイ
- masterにマージされたら自動でデプロイするようにした
- bundler2系への更新
- 2.1.x系がexecutable path周りの不具合があってずっと見送っていた
- Ruby 2.7に上げたタイミングで元々入っている2.1.2を試したら直っていたので手元も含めて2系へ
- preemptive VMのstopをハンドリングする
- GKEで作っているクラスタでpreemptive VMを利用している
- 止められたときにサービスが落ちる症状を見て見ぬ振りしていたが根本対処
- 詳しくは 趣味のGKEクラスタを安価に運用する - 839の日記
- deschedulerでRemoveDuplicatesが動作していない問題の修正
- いつの間にか動かなくなっていたことに気づく
- emptyDirを使っていると特殊なフラグが必要だった
- GitHub - GoogleCloudPlatform/k8s-node-termination-handler: A solution to gracefully handle GCE VM terminations in kubernetes clusters のhelm chart化
- yamlをバラバラにデプロイするのが不安症候群なのでchart化
- GitHub - GoogleCloudPlatform/k8s-node-termination-handler: A solution to gracefully handle GCE VM terminations in kubernetes clusters にslack通知機能を追加
- PR出したらシュッとマージされた(長らくレビュアー不在だったのでリマインドはしたけど)
- terraformの0.11->0.12移行
- 主に警告修正
- 次はgoogle providerのメジャーバージョンアップ更新もしないと...
- Ruby 2.7.0化
- Frozenバグとか踏んだりしたけど無事上がった
- Ruby 2.7.0に上げてFrozenErrorを踏んだ - 839の日記
- あるサービスのサポート切りによるES6コードの排除
- ES6側のコードを使っている部分の機能提供が終わったので除去
- ようやくTSに統一できた
- 特定ディレクトリ以下が変更されたときだけデプロイする機能の追加
- src以下が変更されたときだけデプロイとかしたかった
- go 1.13.5への更新
- TypeScriptの
hoge === null || hoge === undefined
をhoge == null
に統一- 前に記事で困っていると書いたらtwitterで教えてもらったやつ
1ヶ月経ったのでGKEクラスタのサービスSLA確認したらほぼ100%でした(99.99%)。
preemptive VM使ってもいい感じに安定してきた。
椅子
自宅用の椅子を買いました。
高いやつじゃなくてニトリで売ってる4万弱ぐらいのやつ。
買い替えた理由は今使っている椅子がもう寿命で本格的にやばかったからです。
椅子といえばアーロンチェア感はありますが、自分の中では4万弱の椅子で正直十分体感が良かった。
というかそのくらいからもはやアーロンチェアとの違いがわからなくなってくるのでとりあえず自宅は安めのやつを購入しました。
会社の椅子も実はずっと変えたいと思っているので今回買った椅子がよかったら同じの買おうかな。
2月中旬ぐらいに届く予定なので楽しみ。
ポケモン剣盾
久々にスイッチやりまくってはまってます。
こんなにコンシューマゲームやったのは久しぶりで既にSplatoon2のプレイ時間を超えている…。
今月進捗が悪かった主な原因はポケモンです。
ルビサファ以来のカムバック勢で今作レート戦初デビュー。
色々と考えることが多くて難しいと思いつつ1月のレート戦で無事マスボまでは上がれました。
今月もレート戦頑張っていきたい、目指せ4桁。
リリースノートが遅れたのもポケモンのせいです。