GASを使ってSlack botを作る際の問題点
GAS (Google Apps Script)は無料で簡単に自動化した処理を作れるので便利です。
ただ、その分処理速度に関してはあまり早くないため、ちょっとした処理でも簡単に数秒かかってしまいます。
GASを使ってSlack botを作りたい、というのは比較的よくあると思います。
GAS→Slackの連携に関しては処理時間はあまり関係ないので良いですが、Slack→GASの場合だと、Slackのいわゆる「3秒ルール」により、3秒以内に処理が終わらず、タイムアウトエラーや処理が何度も動いてしまうなどの問題が発生すると思います。
※Slackの3秒ルールとは
Slackが呼び出す外部APIは、3秒以内にレスポンスを返さないといけないという仕様。
レスポンスがないと、イベントの種類に応じてタイムアウトエラーやリトライが発生する。
解決策:リトライを利用する
Slackのイベントは主に2つのパターンがあります。
- 3秒以内にレスポンスがないとタイムアウトエラーが出るもの(スラッシュコマンド、フォーム送信など)
- 3秒以内にレスポンスがないとリトライするもの(メンション、リアクションなど)
このうち1のイベントについては解決できないので、利用するのを諦めます。
2のリトライしてくるものについて、以下の処理をします。
- 初回に来たイベントのリクエストのIDをScriptPropertiesに記録し、その後は普通に処理を実行する
- 3秒以内に終わらず、2回目のリトライリクエストがくるので、ScriptPropertiesに記録されたIDと比較して、すでに記録済みの場合は200 OKを返す
SessionPropertyとの比較だけであれば3秒以内にレスポンスを返すことは可能なので、これで対応します。
function doPost(e) {
const contents = e.postData?.contents ? JSON.parse(e.postData.contents) : {}
const challenge = contents.challenge;
// Slackのchallengeはそのまま返す
if (challenge) {
return ContentService.createTextOutput(challenge).setMimeType(ContentService.MimeType.TEXT);
}
// 一意なIDとしてclient_msg_idを利用する
const clientMsgId = contents.event.client_msg_id
const scriptProperties = PropertiesService.getScriptProperties();
if (!!scriptProperties.getProperty(clientMsgId)) {
// 2度目以降の起動なので何もしないで200 OKを返す
return ContentService.createTextOutput("OK");
} else {
// 初回起動なのでclient_msg_idを保存する
scriptProperties.setProperty(clientMsgId, JSON.stringify(contents.event));
}
/*
* 〜〜主処理をこの辺で実施〜〜
*/
// 万が一3秒ギリギリに終わってしまうと、リトライが来るタイミングとの兼ね合いで
// 2回実行されてしまう場合があるので、3秒スリープさせる
Utilities.sleep(3000);
// 保存しておいたclient_msg_idを削除しておく
scriptProperties.deleteProperty(clientMsgId);
return ContentService.createTextOutput("OK");
}
1度必ずリトライが発生する、というのがちょっとイケてないですが、とりあえず目的は達成できたので良しとします。
キレイにやりたければ、ちゃんとAWSのLambdaとかGoogle CloudのCloud Functionsとかを使いましょう。
なお、Slackではリトライの際にX-Slack-Retry-Num
というHTTP Headerを付けてくるので、これをチェックするのが本来のやり方ですが、GASではHTTP Headerは取得できないので、このような方法を取っています。
試した方法(失敗例):トリガーを使う
通常のFaaSなどを使う場合、Queueを使ったり別のFaaSを呼び出したりして、3秒以内にレスポンスを返すのがセオリーです。
GASの場合はトリガーを使うことで同様の仕組みが作れます。
let time = new Date();
time.setHours(10);
time.setMinutes(30);
ScriptApp.newTrigger('someLazyFunction').timeBased().at(time).create();
上記コードで3秒以上かかる重い処理を別プロセスとして実行できるようになります。
ただ、このnewTrigger自体、結構時間がかかるので、3秒以内に終わらない場合がありました。
そのため、この方法ではうまくいきませんでした。
【余談】newTriggerを使うと「指定した時間内のみN分置きに処理を実行する」みたいな柔軟な自動スクリプトが実行可能になるので、仕組みは知っておくと結構有用です。
まとめ
- Slackの3秒ルールにGASで対応するため、「リトライを無視する」方法を用いた
- リトライを使えないSlackイベントでは実現できないため、GAS以外の方法を使う必要がある
- 3秒ルールにとらわれず処理できるので、GASの活用の幅が広がりそう