Edited at
AidemyDay 6

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


概要

こんにちは。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の手を借りて改善しました。

今ではすっかり社内インフラの一つとして浸透しています。

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