Heroku上のPythonからRedisを使う 開発環境Mac

Redis to Goがadd onから削除されたことによってしばらく混乱していたのですが、やっとすっきり解決したので備忘録として公開。

## 何が書いてあるか
- Heroku上のPythonからHeroku Data for Redisを使うための方法が書いてある
- Macのローカルで同じ環境を実現して開発する方法が書いてある

## Let's go

HerokuのPythonからバックグラウンドタスクを処理しようとする場合のオフィシャルドキュメントはこちら

https://devcenter.heroku.com/ja/articles/python-rq

ただし、これはRedis to Goがadd_onに存在していたときの情報で、若干変わっているので英語版を見る必要がある。

https://devcenter.heroku.com/articles/python-rq

そして、worker.py にはこう書けと書いてあるのだが、それだと動かない

```python worker.py
redis_url = os.getenv('REDIS_URL', 'redis://localhost:6379')

conn = redis.from_url(redis_url)
```

Heroku Data for Redisのプランがhobbyの場合は問題ないのだが、課金すると突然動かなくなる。
なぜかというとTLSが必要になるから。
これについてはここに書いてある

https://devcenter.heroku.com/articles/connecting-heroku-redis#connecting-in-python

```python: worker.py
import os
from urllib.parse import urlparse
import redis

url = urlparse(os.environ.get("REDIS_URL"))
r = redis.Redis(host=url.hostname, port=url.port, username=url.username, password=url.password, ssl=True, ssl_cert_reqs=None)
```

~~そして、何故かこのままだと動かない。urlparseでportがうまく認識されないっぽい。謎。
ということで、最終的にはhost,port,passを環境変数に入れることになった。~~
上記の原因はusernameが含まれていたからだった。
Heroku Data for Redisを追加してそのURLを見ると分かるのだがusernameは存在しないのだ。
ということで、以下の様にするのが正解であった。元々はhost,pass,portをそれぞれ環境変数に入れてそれを使う形にしていたのだが、Heroku Data for Redisは定期的にURLが変更される。そのタイミングで使えなくなってしまうので、設定は以下のようにしましょう。

2023.1.12追記 ドキュメントページに修正を反映してもらいました
https://devcenter.heroku.com/articles/connecting-heroku-redis#connecting-in-python

```python:worker.py
import os
import redis
from urllib.parse import urlparse

from rq import Worker, Queue, Connection
import logging
logger = logging.getLogger(__name__)

listen = ["events", 'cursor', 'high', 'default', 'low']

if os.environ.get("ENV") != 'production':
    print("development")
    conn = redis.Redis(host='localhost', port=6379, db=0)
else:
    url = urlparse(os.environ.get("REDIS_URL"))
    conn = redis.Redis(
        host=url.hostname,
        port=url.port,
        password=url.password,
        ssl=True,
        ssl_cert_reqs=None
        )


if __name__ == '__main__':
    with Connection(conn):
        print("worker start")
        worker = Worker(map(Queue, listen))
        worker.work()
```

これらをHerokuの環境変数に入れておく
```.env
export REDIS_HOST=hostのURL
export REDIS_PORT=REDISのポート番号
export REDIS_PASS=REDISのパスワード
```

`if os.environ.get("ENV") != 'production':`
これは、ローカル開発環境で使うための分岐。
MacにRedisインストールして、ターミナルから
`$ redis-server`
するとRedisが立ち上がる。
ここに接続する場合は、環境変数ENVを
`export ENV=local`
としておけば
`conn = redis.Redis(host='localhost', port=6379, db=0)`
を使ってローカルのRedisに接続される

## workerについて
作ったworker.pyをどこに置くかというと、ドキュメントルートである。
ProcFileにはこんなふうに書く

```ProcFile
worker: python ./worker.py
```

実は、かねてよりここに書いてある形でSlack用のアプリケーションを書いている

https://qiita.com/geeorgey/items/fb99637d4024cbaf0d37

ここの構成では、Pythonで動く部分は
/backend/
以下にファイルを配置しており、worker.pyももれなくそれに従う形で配置していた
/backend/worker.py
そして、ProcFileには
```ProcFile
worker: python ./backend/worker.py
```
と書いていたのだけど、これだとパスが通らない。workerはドキュメントルートに配置しましょう。そうしないと、workerから呼び出す関数へのパスがおかしくなるので実行できないのです。

## 2種類のworkerを使いたい
処理が異なる2つのバックグラウンドタスクが存在しており、片方が割と重たいけど常に使われているものではない、且つもう一方は常時稼働が必要なタスクなのでそちらに影響を与えたくないということで2つのworkerを設置することにしました。
最初一つのworkerでやろうとしたら、重たい方を回したら、常時稼働が必要な方が止まりました。という訳で分離しましょうということにしました。
- workerのファイルをコピーして若干変更
- Redisももう一つadd on追加して２つ起動するように変更

```python:workerc.py
import os
import redis
from urllib.parse import urlparse

from rq import Worker, Queue, Connection
import logging
logger = logging.getLogger(__name__)

listen = ["events", 'cursor', 'high', 'default', 'low']

if os.environ.get("ENV") != 'production':
    print("development")
    conn_c = redis.Redis(host='localhost', port=6379, db=0)
else:
    url = urlparse(os.environ.get(os.environ.get("REDIS_COLLECTOR_KEY")))
    conn_c = redis.Redis(
        host=url.hostname,
        port=url.port,
        password=url.password,
        ssl=True,
        ssl_cert_reqs=None
        )

if __name__ == '__main__':
    with Connection(conn_c):
        worker = Worker(map(Queue, listen))
        worker.work()
```
ほぼ同じですが、Redisの接続先をもう一つの方に変更しています。
環境変数を追加しておきます
- REDIS_C_HOST
- REDIS_C_PORT
- REDIS_C_PASS

ProcFileにも一行追加しています

```ProcFile
worker: python ./worker.py
collect: python ./workerc.py
```

pythonから呼び出す形はこんな感じ

```python:bolt.py
from rq import Queue
from worker import conn
from workerc import conn_c
from .collect import start_collect
q = Queue(connection=conn,default_timeout=100)
c = Queue(connection=conn_c,default_timeout=1000000000)
@bolt_app.view("collect_past_posts_submit")
def collect_past_posts_submit(ack, body, logger, client, view, context):
    ack()
    result = c.enqueue(start_collect, body['team']['id'], context['user_id'])

@bolt_app.event("message")
def handle_message_events(client, body, logger, context):
    user_id = context['user_id']
    try:
        userinfo = client.users_info(
            user=user_id,
            include_locale=True
        )
    except SlackApiError as e:
        print("Error fetching conversations: {}".format(e))
    post = q.enqueue(handleMessage,client,body,context,logger,userinfo,job_timeout=36000)

def handleMessage(client,body,context,logger,userinfo):
    print('handle_message_events ■□■□■□■□■□■□■□■□■□■□■□')
    #何かしらの処理----
```

ローカルでRedis２つ立ち上げる方法までは調べなかったので、ローカルでも同じ環境にしたいという場合はもう少し調べる必要があります。
一つのRedisに２つのworkerで接続しようとすると、処理が２つに別れないですが、検証にはそれほど困らないと思う。

## Herokuのworkerのログを見るには？

以下のコマンドで見ることができます

`heroku logs -t -d worker -a アプリ名`
２つ目の方は
`heroku logs -t -d collect -a アプリ名`
となります。ProcFileで指定した名前で呼び出す形式です。

`heroku logs -t -a アプリ名`
とすると、アプリ内の全dynoのログが確認できます。
処理がきちんと指定したworkerに伝わっているかどうかを確認するためには-d オプションを付けてworkerのログを見る必要があります。

## コンソールから接続するには
```heroku redis:cli -a アプリ名```
という形で接続します。
一つのアプリに複数のREDISインスタンスを紐付けている場合だとこれだと接続できませんので
```heroku redis:cli -a アプリ名 HEROKU_REDIS_xxxx```
というように、REDISを追加した際につけられた名前を指定してアクセスしましょう

## Redisにログインして作業する
### keyを確認するには
```>keys *```
これですべてのキーを確認できます。
### キーを削除する場合
```>flushdb```

### TTLが設定されていないキーの取得
```>info keyspace```

以上


## 開発してるSlackアプリの紹介
よろしければインストールして使ってみて下さい！こちらのTIPSを利用しているのはTimeLineです。

- TASUKARU-TaskALL- https://lne.st/3x1u
- TimeLine for Slack https://lne.st/wr21
- OYASUMI bot https://lne.st/oyasumilink
- TIPS https://lne.st/tips
- YOKOKU for Slack https://lne.st/4cdy

### TASUKARU 
Slackで色々な人からメンションをもらって混乱したことはありませんか？
何か頼まれていたはずだけど思い出せない…　そんな状態からあなたを救います。
TASUKARUはSlack専用のタスクマネージャーです。
デフォルトのスレッドやメンション画面では、処理が終わったpostをアーカイブすることが出来ませんが、TASUKARUならそれが出来ます。
今アクションが必要なものだけに集中することができる、それがTASUKARUです。　https://lne.st/3x1u

### TimeLine for Slack
全ての公開チャンネルのpostを一つのチャンネルにまとめるタイムラインチャンネルを生成するアプリです。
一部のチャンネルのみを集めたミックスチャンネルを作成すると、自分が必要なチャンネルだけのタイムラインを作り出すことが出来ます。
オプション機能として、メッセージ転送時にDeepL翻訳を挟むことが出来ます。　https://lne.st/wr21

### OYASUMI bot
Googleカレンダーのスケジュールに「休み」等の、休暇判定キーワードが入っていた場合に、自動的にSlackをスヌーズ状態に変更します。
メンションがあった場合に、メンションした相手にその日は休暇ですというレスを付けます。
スタッフの休暇を気持ちよく過ごしてもらう為のアプリです。　https://lne.st/oyasumilink

### TIPS
Slack用のリマインダーアプリです。
デフォルト機能のリマインダーは、一つのスケジュールに一つのメッセージの設定しか出来ません。
このアプリでは、一つのスケジュールに複数のメッセージを登録し、ランダムで指定された時間にpostします。
チャンネル特有のノウハウ等をスケジュール登録しておくことで、自然と情報が浸透する状態を作ります。　https://lne.st/tips

### YOKOKU for Slack
OYASUMI botの応用です。
例えば「営業」というキーワードが入った予定だけを抽出したいと思ったことはありませんか？
カレンダーをいちいち検索するのは手間がかかります。
このアプリを用いれば、特定のキーワードが入った予定を、特定のチャンネルに流すことが出来ます。　https://lne.st/4cdy