LINE BOTをつくったので紹介します。
ソースコードはgithubにあります。
つくったもの
入力された時間(秒)が経過したら通知してくれるLINE BOTです。
面白くないネタですが、非同期処理をやるには丁度よい題材でした。
以下がつくったもののスクリーンショットですが、"180"と入力すると180秒後にメッセージが通知されます。
当然その180秒の間、他のメッセージも処理する必要があります。
しかしLINEのAPIの仕様上、ユーザーからのリクエストは180秒も待たずにタイムアウトしてしまい、返信できなくなります。
そもそもタイムアウトする前に返信できるならそれが良いですが、
大量の処理が来た場合や、画像処理などどうしても間に合わない重い処理、
今回のタイマーのようなものは一旦リクエストを返した後に処理するしかありません。
基本方針
とりあえずLINE BOTサーバーは他の多くの人がやっているようにheroku上で動かします。
LINE BOTはSSL通信が必須となっていて自分で準備するのはやや手間ですが、
herokuならデフォルトで可能だからです。PaaSの利点ですね。
LINE BOTのアーキテクチャについては以下の記事を参考にしました。
-
LINE BOTに関する記事
1.1. 大量メッセージが来ても安心なLINE BOTサーバーのアーキテクチャ -
herokuに関する記事
2.1 Worker Dynos, Background Jobs and Queueing
2.2 Background Tasks in Python with RQ
非同期処理のためのバックグラウンドワーカーは2.2に従ってRQを使います。
RQを使うためにはRedisサーバーが必要ですが、herokuならRedisToGoというアドオンを追加するだけで良いので簡単です。
$ heroku addons:create redistogo
LINE BOTをRQでやる場合、ユーザーからメッセージを受信したときのMessageEventをざっくり描くと下図のようなシーケンスになります。
- LINEサーバーからBOTサーバーにリクエスト(送信者ID、メッセージ、返信用トークン)が届く
- 返信用トークンを用いて、LINE APIサーバーにメッセージを返信する(必須ではない)
- RQ(ジョブキュー)にメッセージに対するジョブを登録する
- LINEサーバーに200を返す
- ジョブをジョブキューから取り出す
- ジョブを処理する
- ジョブの結果をLINE APIサーバーに通知する
ここで、LINE APIサーバーに送信したメッセージがユーザーのLINE画面に表示されます。
重要なのは1~4までの時間がLINE的にもheroku的にも制限されていることです。
ちなみに返信用トークンにも有効期限があります。使うならさっさと使いましょう。
そしてすぐにLINEサーバーに200を返しましょう。
送信者IDが分かっていれば、5~7はいつでも実行できます。(結果の通知が遅すぎるのは問題ですが)
今回のLINE BOTでは以下の3つのプロセスが存在します。
リクエストごとにタイマースレッドを作るわけにもいかないので、タイマーはRedisで管理することにしました。
- LINE BOTサーバー:リクエストを受け付けてRQにジョブを登録する
- RQワーカー:ジョブを取り出してRedisのソート済みセットにタイマーを登録する(設定時間でソート)
- Timerワーカー:Redisを定期的に監視し、設定時間になったタイマーがあれば送信者に通知し、タイマーを削除する
Timerワーカーは上のシーケンスには出てこないですが、
RQワーカーはあくまでジョブの登録を契機に実行されるので、タイマーのタイミングは分かりません。
なのでタイマーが設定時間になったかどうかはTimerワーカーが検知してLINE APIサーバーに通知します。
heroku無料枠の制限対策
今回のソースコードですが、このままではherokuの無料枠で動きません。
Procfileにウェブサーバー1つとバックグラウンドワーカー2つを指定していますが、
無料枠では一度に2つのdynoしか設定できないからです。
web: gunicorn app:app
rq_worker: python rq_worker.py
timer_worker: python timer_worker.py
$ heroku ps:scale rq_worker=1 timer_worker=1
Scaling dynos... !
! Cannot run more than 2 Free size dynos.
このため、残念な対策ですが、同じソースコードをProcfileだけ変更して2つのアプリとしてデプロイする方法を取りました。
web: gunicorn app:app
rq_worker: python rq_worker.py
timer_worker: python timer_worker.py
この場合、2つのアプリは同じRedisサーバーに接続しなければならないので、どちらかのアプリで作ったRedisToGoアドオンの環境変数REDISTOGO_URLをもう片方のアプリの環境変数にも設定してあげる必要があります。
おまけ
今回作ったLINE BOT、入力に大して何も処理をしてないので数字以外はエラーなります。
ちなみに入力を形態素解析してゴニョゴニョするLINE BOTも作ってみました( ソースコード:github)。