#概要
こんにちは。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の手を借りて改善しました。
今ではすっかり社内インフラの一つとして浸透しています。
今後も誰かに快適に使ってもらえるものを作っていきたいですね。