RPAエンジニアの雑記

RPA(Blue Prism)について色々記載してます。

(BluePrism)ポケモン図鑑を作る ~Part17 めっちゃ詰まったところ編~

はいどうも、おむおむです。

もう前置きすら面倒だ!
ひたすらに記事を書く、書く、書く!

※注意:めちゃ長いです

なぜか4回結果が返ってくる

前回までで、実行する環境の構築が完了しました。
また、コントロールルームからも問題なく実行できることも確認済みです。
newgraduate19-rpa.hatenablog.com

ポケモン図鑑の起動方法は
SlackのBotポケモンの名前をメンションすることであり、
そのためにSlackのEvent APIGASを使用していました。

newgraduate19-rpa.hatenablog.com
newgraduate19-rpa.hatenablog.com

ただ、この状態で起動すると、、、

f:id:newgraduate19:20200725152630p:plain

なぜか、4件結果が返ってくるんです。
しかも、最初に2件、ほぼ同時に来て、
約1分後に1件、その3分ぐらいあとにもう1件。。。

f:id:newgraduate19:20200704141849p:plain

問題の切り分け

冷静に問題を切り分けてみましょう。

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.



犯人は、お前だ!!!

f:id:newgraduate19:20200729091153p:plain

解決方法を考えよう

どうやら、Event APIからPOSTを受け取ったら
3秒以内に200番を返す必要があるようです。

ところが、諸々検証してみたところ、
以下のような結果となりました。

1. UrlFetchApp.fetchメソッドは、タイムアウト時間を指定できない
2. Blue Prismは、プロセスが終了した段階でレスポンスを返す
3. 当たり前だが、冒頭にreturnを入れると後続の処理が実行されない
4. GASは現時点で非同期処理ができない


困った。。。

今回、無料枠のスペックの低いサーバ上にデプロイしているので、
どうしてもプロセスが完了するまで時間がかかってしまいます。

というか、どんなハイスペックなものだろうと
外部API叩く処理が入っているので
3秒以内にレスポンスを返すのは不可能。。。

f:id:newgraduate19:20200725161826p:plain





いや、あきらめちゃだめだ!
ここまで来たんだから!

GASで非同期処理っぽいことをする

そんな中調べていたら、おあつらえ向きな記事が!

qiita.com

f:id:newgraduate19:20200725162431p:plain

ざっくりとやるべきことを並べるとこんな感じ?

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していきます。

定期実行する

あとは、上の関数を定期実行していくよう設定します。

時計のマークをクリックして、
f:id:newgraduate19:20200729200713p:plain

画面右下の「トリガーを追加」をクリックして、
f:id:newgraduate19:20200729200844p:plain

実行する関数、デプロイ(バージョン)、
イベントのソース(トリガーのソース)、
時間主導型の場合実行の時間間隔、
エラーの通知頻度を設定したらOKです。
f:id:newgraduate19:20200729201336p:plain

今度こそ、終わり!!!

まとめ

・Webエンジニアって大変だね!
・どんな分野のエンジニアだろうが、大変だね!
・GAS、無限の可能性を秘めている☆

次回こそ!次回こそ!
最☆終☆回!