LoginSignup
6
1

More than 5 years have passed since last update.

Firebase を駆使して作った Slack の slash command を改良してみた

Last updated at Posted at 2018-12-05

概要

こんにちは。Aidemyの刀根です。
以前、Firebaseを使ってslackにslash commandを追加しました。その記事はこちら

しかし、同じようなコマンドをしばらく運用した結果timeoutエラーがちょいちょい出てきてしまうことがわかりました。

今回の記事は、その原因と解決法です。

timeoutする原因

結論から言うと、 Firebaseの仕様 でした。

Firebaseでは負荷に応じてvmをスケーリングするなどインフラ調整を自動で行なっているのですが、その中には

あまりにもアクセスが少ないリソースはスリープ状態にする

というものがありました。
スリープ状態になると、Cloud Functionを実行するとき一度リソースを起動してから関数を実行する必要があるので、httpリクエスト受信から応答までの時間が長くなってしまいtimeoutが発生していたと言うわけですね。

対策

今回はhttpリクエストに対する応答さえ早く返してしまえばいいので、 Google Cloud Pubsub とAzureに立ち上げたVMを一つ利用して以下のような構成にしました。
資料ベース2.jpg

処理の順序としては、
1. vmにhttpリクエストを送る
2. vmがhttpレスポンスを返す
3. vmがtopicメッセージを発行する
4. topicメッセージの発行に呼応してfirebase上でslackへの投稿を実行

という流れになります。

VM側で実行するコード

VM側ではExpress+Typescriptでhttpリクエストを受け付ける簡易APIサーバーを立てました。ソースはこちら。

app.ts

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で実行するコードですが、こちらは改良前のソースコードとの差分を載せておきます。

before.ts
// 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);
  }
})
after.ts
// トピックメッセージ受信で発火する関数
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の手を借りて改善しました。
今ではすっかり社内インフラの一つとして浸透しています。

今後も誰かに快適に使ってもらえるものを作っていきたいですね。

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1