(BluePrism)ポケモン図鑑を作る ~Part17 めっちゃ詰まったところ編~
はいどうも、おむおむです。
もう前置きすら面倒だ!
ひたすらに記事を書く、書く、書く!
※注意:めちゃ長いです
なぜか4回結果が返ってくる
前回までで、実行する環境の構築が完了しました。
また、コントロールルームからも問題なく実行できることも確認済みです。
newgraduate19-rpa.hatenablog.com
ポケモン図鑑の起動方法は
SlackのBotにポケモンの名前をメンションすることであり、
そのためにSlackのEvent APIとGASを使用していました。
newgraduate19-rpa.hatenablog.com
newgraduate19-rpa.hatenablog.com
ただ、この状態で起動すると、、、
なぜか、4件結果が返ってくるんです。
しかも、最初に2件、ほぼ同時に来て、
約1分後に1件、その3分ぐらいあとにもう1件。。。
問題の切り分け
冷静に問題を切り分けてみましょう。
1. Blue Prism側に問題がある
プロセス自体は問題なく動きますが、
APIとして公開することで何か挙動が変わる可能性がある?
結論として、シロでした。
Blue Prismなのに、シロ(爆笑)
諸々すいませんでした。
2. GASに問題がある
今回POSTに使っているのは、
UrlFetchApp.fetchメソッド。
なんかこいつが怪しいなぁ!?
結論、こいつもシロでした。
Google先生ごめんなさい。
3. SlackのEvent API
・・・怪しいな。
なんとなくで使ってきてたけど、君、怪しいなぁ。
api.slack.com
Your app should respond to the event request with an HTTP 2xx within three seconds. If it does not, we'll consider the event delivery attempt failed. After a failure, we'll retry three times, backing off exponentially.
犯人は、お前だ!!!
解決方法を考えよう
どうやら、Event APIからPOSTを受け取ったら
3秒以内に200番を返す必要があるようです。
ところが、諸々検証してみたところ、
以下のような結果となりました。
1. UrlFetchApp.fetchメソッドは、タイムアウト時間を指定できない
2. Blue Prismは、プロセスが終了した段階でレスポンスを返す
3. 当たり前だが、冒頭にreturnを入れると後続の処理が実行されない
4. GASは現時点で非同期処理ができない
困った。。。
今回、無料枠のスペックの低いサーバ上にデプロイしているので、
どうしてもプロセスが完了するまで時間がかかってしまいます。
というか、どんなハイスペックなものだろうと
外部API叩く処理が入っているので
3秒以内にレスポンスを返すのは不可能。。。
いや、あきらめちゃだめだ!
ここまで来たんだから!
GASで非同期処理っぽいことをする
そんな中調べていたら、おあつらえ向きな記事が!
qiita.com
ざっくりとやるべきことを並べるとこんな感じ?
1. GASのcacheにPOSTされたデータをエンキューしてすぐに200番を返す
2. cacheの中のデータを引数としてこれまで作成してきた処理に渡す
3. 2番のスクリプトを定期実行する
関数の構成はこんな感じかな?
function doPost(e) { // Event APIからPOSTされたデータをキューイングして200番を返す } function addJobQueue(Value) { // cacheへポケモン名をキューイングする // (doPost関数から呼び出される) } function callProcess() { // cache内のデータだけBlue Prismのプロセスを実行 }
エンキューとレスポンス
まずは、SlackのEvent APIから送信されてきたデータから
ポケモンの名前を抽出し、
cacheへエンキューしてSlack側へは200番を返す
という関数を作ります。
今まで作ってきたものも流用していくとこんな感じ↓
function doPost(e) { // Slackの投稿メッセージ取得 var params = JSON.parse(e.postData.getDataAsString()); if (params.event.type == 'app_mention') { // メッセージ抽出 var msg = params.event.text; // 正規表現作成 const reg = new RegExp('<' + '.*?' + '>', 'g'); // 正規表現にマッチした箇所と、残った箇所のトリム var name = msg.replace(reg, '').trim(); // addJobQueue関数を呼び出して引数にポケモン名を与える addJobQueue(name); // 200番を返す return { "status": 200, "message": "OK" }; } function addJobQueue(Value) { // cacheにデータを追加する処理(後述) }
cacheの種類
では、ここからキャッシュにデータを入れていく
addJobQueue関数の中身に迫っていきます。
その前に、まずはcacheの概要から。
cacheには
DocumentCache(ドキュメント固有のキャッシュ)、
ScriptCache(スクリプト単位のキャッシュ)、
UserCache(ユーザ単位のキャッシュ)の3種類があり、
今回はScriptCacheへポケモン名を入れていきます。
developers.google.com
GASでの取得方法は以下を参照。
developers.google.com
具体例としてはこんな感じ↓
function hoge() { // cacheの取得 var cache = CacheService.getScriptCache(); // 指定したkeyに対応するvalueをString型で取得 var cached = cache.get('keyの名前'); }
データ投入の方法
cacheへは、keyとそれに対応するvalueを入れていきます。
keyとvalueを一致させてもいい気もしますが、
同じポケモンの情報を重ねてリクエストすると
上書きされて1回しか実行されなくなってしまいます。
つまり、cacheが
{ "ピカチュウ": "ピカチュウ", "カビゴン": "カビゴン" }
のときピカチュウを追加しようとすると
既に「ピカチュウ」というキーが存在するため
新規のピカチュウが追加されません。
(頭の悪い文章)
今回はリクエストをキューイングしたいので、
上に貼ったリンクのように
1つのキーに対して
String型の配列のように追加して
その要素数だけfor文でぐるぐる回すように使いたいと思います。
cacheへの追加はputメソッドを使用します。
(具体的なコードは後述)
developers.google.com
配列をいじいじする
この辺はGASというか
JavaScriptの記事が参考になります。
qiita.com
今回はキュー形式(FIFO、先入先出)でデータを処理したいので、
メソッドはpushを使用します。
developer.mozilla.org
cache内には配列は入れられないので、
cacheに入れるときはセミコロン区切りのString、
cacheから取り出したらセミコロンを区切り文字として
String型の配列に戻します。
Stringにするときはjoinメソッド、
配列にするときはsplitメソッド、
を使用します。
developer.mozilla.org
developer.mozilla.org
cacheの取得含め、具体的にGASはこんな感じ↓
function addJobQueue(value) { // 引数を入れなおし var newQueue = value; // cacheを取得 var cache = CacheService.getScriptCache(); // cacheされているデータを取得 const key = 'Name'; var cached = cache.get(key); // cacheのデータがnullの時は空の配列を作成配列 // そうでない場合はセミコロン区切りのString型の配列に変換 if (cached == null) { cached = []; } else { cached = cached.split(';'); }; // 配列の最後にnewQueueを追加 cached.push(newQueue); // 配列をセミコロン区切りのStingに変換 cached = cached.join(';'); // cacheに追加 cache.put(key, cached, 60*5); return; }
cacheのデータを使ってPOSTする
そして、cache内にキューイングされているデータを使用して
POSTするような関数を書いていきます。
といっても、上に書いたものと
今まで作ってきたものをガッチャンコするだけで良きです。
具体的には以下の通り↓
function callProcess() { // cacheからデータ取得 var cache = CacheService.getScriptCache(); const key = 'Name'; var cached = cache.get(key); // cacheの追加・削除の競合を防ぐために削除 cache.remove(key); if (cached == null) { // cacheが空ならそのまま終了 return; } else { // cacheがあればセミコロン区切りでStringの配列に変換 var names = cached.split(';'); }; // プロパティストアからユーザ名とパスワード取得 const prop = PropertiesService.getScriptProperties(); const user = prop.getProperty('user'); const password = prop.getProperty('password'); // Base64にエンコード var bsc = Utilities.base64Encode(user + ':' + password); // ヘッダー作成 var headers = { "Authorization": 'Basic ' + bsc, "Cache-Control": "no-store" }; // cacheの配列の要素数だけ繰り返し for (var i=0; i<names.length; i++) { // SOAP Envelope作成 var env = '<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:blueprism:webservice:pokedex">' + '<soapenv:Header/>' + '<soapenv:Body>' + '<urn:Pokedex soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' + '<Name xsi:type="xsd:string">' + names[i] + '</Name>' + '</urn:Pokedex>' + '</soapenv:Body>' + '</soapenv:Envelope>'; // options作成 var options = { "method": "post", "headers": headers, "payload": env, "contentType": "text/xml; charset=utf-8" }; // POST Logger.log(names[i]); UrlFetchApp.fetch('(EC2のIPアドレス)', options); }; return; }
配列の要素数だけPOSTしていきます。
定期実行する
あとは、上の関数を定期実行していくよう設定します。
時計のマークをクリックして、
画面右下の「トリガーを追加」をクリックして、
実行する関数、デプロイ(バージョン)、
イベントのソース(トリガーのソース)、
時間主導型の場合実行の時間間隔、
エラーの通知頻度を設定したらOKです。
今度こそ、終わり!!!
まとめ
・Webエンジニアって大変だね!
・どんな分野のエンジニアだろうが、大変だね!
・GAS、無限の可能性を秘めている☆
次回こそ!次回こそ!
最☆終☆回!