概要
こんにちは。Aidemyの刀根です。
以前、Firebaseを使ってslackにslash commandを追加しました。その記事はこちら
しかし、同じようなコマンドをしばらく運用した結果timeoutエラーがちょいちょい出てきてしまうことがわかりました。
今回の記事は、その原因と解決法です。
timeoutする原因
結論から言うと、 Firebaseの仕様 でした。
Firebaseでは負荷に応じてvmをスケーリングするなどインフラ調整を自動で行なっているのですが、その中には
あまりにもアクセスが少ないリソースはスリープ状態にする
というものがありました。
スリープ状態になると、Cloud Functionを実行するとき一度リソースを起動してから関数を実行する必要があるので、httpリクエスト受信から応答までの時間が長くなってしまいtimeoutが発生していたと言うわけですね。
対策
今回はhttpリクエストに対する応答さえ早く返してしまえばいいので、 Google Cloud Pubsub とAzureに立ち上げたVMを一つ利用して以下のような構成にしました。

処理の順序としては、
- vmにhttpリクエストを送る
- vmがhttpレスポンスを返す
- vmがtopicメッセージを発行する
- topicメッセージの発行に呼応してfirebase上でslackへの投稿を実行
という流れになります。
VM側で実行するコード
VM側ではExpress+Typescriptでhttpリクエストを受け付ける簡易APIサーバーを立てました。ソースはこちら。
import * as bodyParser from 'body-parser';
import * as express from 'express';
import * as PubSub from '@google-cloud/pubsub';
const pubsubClient = new PubSub({
  projectId: 'your-project-id'
});
//トピックがなければ作る
pubsubClient.getTopics().then(async (results:any) => {
  try {
    const mappedTopics = results[0].map((element:any) => {
      return element.name;
    });
    if (mappedTopics.indexOf('topicName') < 0) {
      await pubsubClient.createTopic('start');
    }
  } catch (error) {
    console.error(error);
  }
}).catch((err:any) => {
  console.error('ERROR:', err);
});
const app: express.Application = express();
app.use(bodyParser.urlencoded({ extended: true }));
const publish = (topic: string, message: any) => {
  const messageBuffer = Buffer.from(JSON.stringify(message));
  pubsubClient.topic(topic)
    .publisher()
    .publish(messageBuffer)
    .then((messageId:any) => {
      console.log(`Message ${messageId} published.`);
    })
    .catch((err:any) => {
      console.error('ERROR:', err);
    });
};
app.post('/yourEndPoint', async (req, res) => {
  try {
    res.send('');
    // メッセージの内容はslackから飛んでくるbodyの内容と全く同じにする
    publish('topicName', req.body);
    
  } catch (error) {
    res.status(500).json(error);
    throw error;
  }
});
const server = app.listen(5000, async () => {
  try {
    console.log(`Example app listening on port ${5000}`);
  } catch (err) {
    console.error('Error: 起動に失敗しました');
  }
});
export { server };
Cloud Function側で実行するコード
次に、Cloud Functionで実行するコードですが、こちらは改良前のソースコードとの差分を載せておきます。
// httpリクエストで発火する関数
export const setReminder = functions.https.onRequest(async (req,res) => {
  try {
    if(req.method === 'POST'){
      const min = parseInt(req.body.text || 30);
      const user_id = req.body.user_id;
      const remindTime = myMoment().add(min, 'minutes').format('X');
      await db.collection('reminders').doc(shortid.generate()).set({
        user_id,
        remindTime
      });
      res.send('reminder is set.');
    }else{
      throw new Error('only post method is accepted');
    }
  } catch (error) {
    console.error('error');
    res.status(500).send(error);
  }
})
// トピックメッセージ受信で発火する関数
export const setReminder = functions.pubsub.topic('topicName').onPublish(async (event) => {
  const min = parseInt(event.json.text || 30);
  const user_id = event.json.user_id;
  const remindTime = myMoment().add(min, 'minutes').format('X');
  await db.collection('reminders').doc(shortid.generate()).set({
    user_id,
    remindTime
  });
}
なんかスッキリしましたね。
結果
この実装にしてからtimeoutするという問い合わせがなくなりました。快適。
まとめ
今回はFirebase Cloud Functionを駆使して作ったSlash Commandの応答速度をVMの手を借りて改善しました。
今ではすっかり社内インフラの一つとして浸透しています。
今後も誰かに快適に使ってもらえるものを作っていきたいですね。