Firebase CloudFunctionsでキャッシュを良い感じにする方法

### 追記：
コメント欄でご指摘いただいた内容についてなど、ここに書いておきます。

Q, Storageに貯めておけば良いのでは？？
A, キャッシュから返す方が若干早いかも知れないですが、気にするレベルではないと思います。
同時アクセス数が跳ね上がって、インスタンス数が増えることを想定していなかったので、その面でもStorageに貯めておく方が良さそうですね。

Q, クライアント側のキャッシュで持てば良いのでは？？
A, WebアプリとiOSアプリでクライアント側が分かれている以上、クライアント側よりもAPI部分でまるっと対応した方が良いと考えました。

Q, Firebase Hostingのキャッシュにのせれば良いのでは？？
A, firebase.jsonでrewriteを定義して、hosting側からfunctionsを叩くようにすれば確かにできます。しかし、１つ大きな問題点があり、それは
"Firebase Hosting は、us-central1 でのみ Cloud Functions をサポートします。"
と記載されていることです(2020/09現在)
https://firebase.google.com/docs/hosting/functions?hl=ja#direct_requests_to_function
これではfunctionを叩く時にキャッシュが効いても肝心の通信時間が 延びる/不安定になる のでつらいです...。

### 結論：
ひとまずはこの方法で良いかも知れないけれど、将来的なことを考えたら素直にstorageに貯めるのが良さそうですね。asia-northeast1をサポートしてくれれば、そちらでも良いかも知れません。

--------------------------------

こんにちは。[nisshii](https://twitter.com/nisshii0313)です。
FirebaseのCloudFunctionで、レスポンスに良い感じにキャッシュを効かせたかった話をします。
`うぇるかむ いんぐりっしゅ`な属性の方は、こちらへ
https://yvonnickfrin.dev/how-to-add-cache-to-your-gcp-cloud-functions

今回の記事は概ね↑このサイトで解説されている実装方法を自分で試してみた、という内容です。


# 目次
- はじめに
- 実装してみた
- 結果

## はじめに <a id="anchor1"></a>

現在、友人と協力して[Gizmee](https://itunes.apple.com/us/app/itunes-u/id1509696718?mt=8)というアプリを作成しています。
[Web版](https://gizmee.app)と[iOS版](https://itunes.apple.com/us/app/itunes-u/id1509696718?mt=8)とあって、自分はWeb版クライアントサイドの実装と、両方で使用するapiをCloudFunctionsで書いたりしてます。
(Web版の進捗がアレなのは生暖かく見守ってもらえたらと思います...w)

その中で、iOSでリリースした機能の１つにガジェットに関するニュースの掲載というものがあります。
この機能の実装本体は、単にクローリングをしているだけなのですが、ユーザーがこのタブをアプリ内で開く度に

- cloudfunctionsにリクエストが送られる
- リクエストを元にクローリングに行く
- クローリングして得た情報を整形してレスポンスを返す
- iOS側でレスポンスを処理して表示する

となってしまうのは非効率な気がしていました
(主要なニュースサイトの情報更新頻度は多くて1時間に1度とかなので、毎回クローリングしに行く必要はなさそう...)

そこで、クローリングした結果をキャッシュに持って、一定期間はそこからレスポンスを返すように実装しようと思いました。

しかしながら、Firebase CloudFunctions単体ではキャッシュの仕組みはなく(hostingと組み合わせればhosting側のキャッシュに乗せるのは可能...)、自身でキャッシュを実装する必要がありました。



## 実装してみた <a id="anchor2"></a>

ネットサーフィンをしていたら、すでに似たようなことを考えていた先人が記事にまとめていたので、それを参考にして実装しました。
https://yvonnickfrin.dev/how-to-add-cache-to-your-gcp-cloud-functions

Firebase CloudFunctionsでリクエストを受け取ったりレスポンスを返したりするあたりは端折ります。

まずはキャッシュに入れておきたいデータとその有効期限を格納しておくcacheオブジェクトを定義します

```
const cache = {
  data: [],
  ttl: new Date(),
}
```
dataのところは配列でもkey-valueなMapオブジェクトでも何でも好きにすれば良いと思います。

次に、キャッシュにデータを乗せる部分です

```
cache.data = data
// Store a TTL for the data
const dateInOneHour = new Date()
ttl.setHours(ttl.getHours() + 1);
cache.ttl = dateInOneHour 
res.status(200).send(data);
```

ここで、一定期間保存しておきたいデータをcacheオブジェクトの中に詰めています。
気にするべきはキャッシュの設定時間ですね。自分の場合は先述したようにクロール先のサイトの更新頻度が多くて1時間に1回程度だったので、1時間に設定しています。

最後に、キャッシュからデータを返したい場合の処理を書きます。

```
if (cache.data.length > 0 && cache.ttl > new Date()) {
  return res.status(200).send(cache.data);
}
```
最初に定義した、キャッシュに乗せるdataの構造によって変わってきますが、やるべきことはキャッシュに保存しておいたデータのnullチェックと有効期限のチェックですね。
両方通れば、キャッシュからシュッとデータを返してあげることができ、有効期限を過ぎた、または前回の実行時にうまくキャッシュにデータを保存できなかったという時は、再びデータを用意する処理が走るようにしてあげます


簡単にまとめると、持っておきたいデータと有効期限を予め持ってしまっておいて、有効期限内かつデータが有効なら、キャッシュからレスポンスを返してしまうif文を書くだけです。

ちなみにGizmeeではもう一手間加えていて、CloudFunctionsの定期実行の仕組みを用いて、ユーザーからのアクセスがなくても定期的に関数を呼び出してキャッシュを更新させるようにしています。
また、クローリングの処理自体も並列に書くことで万が一ユーザーからのリクエストについてキャッシュから返せないタイミングが出てきてしまっても何とかなるようにしています
(コンピューティング時間の削減も少しだけ見込めます...)

## 結果 <a id="anchor3"></a>
特に面白味もないのですが、普通に早くなりました
(元々処理に長くて1s程度かかっていましたが、キャッシュに乗ると数十ms程度で返るようになり、apiとして実用性が出ました)

以上です。