ラベル rq の投稿を表示しています。 すべての投稿を表示
ラベル rq の投稿を表示しています。 すべての投稿を表示

2024年6月18日火曜日

rq-dashboard を nginx 配下で動作させる設定のサンプル

rq-dashboard を nginx 配下で動作させる設定のサンプル

概要

compose.yaml と nginx.conf の設定ファイルを紹介します
ポイントは RQ_DASHBOARD_URL_PREFIX を設定する点です

環境

  • Ubuntu 22.04.4
  • docker 26.1.1
  • rq-dashboard 0.7.0

nginx.conf

upstream rq_dashboard {
    server 192.168.0.100:9181;
}

server {
    listen 80;
    server_name host01;

    location = /rq_dashboard {
        return 302 /rq_dashboard/;
    }

    location /rq_dashboard/ {
        proxy_set_header Host $host;
        proxy_pass http://rq_dashboard;
    }
}

compose.yaml

services:
  rq_dashboard:
    image: eoranged/rq-dashboard
    environment:
      RQ_DASHBOARD_REDIS_URL: redis://192.168.0.101:6379
      RQ_DASHBOARD_USERNAME: user01
      RQ_DASHBOARD_PASSWORD: xxx
      RQ_DASHBOARD_URL_PREFIX: /rq_dashboard
    ports:
      - 9181:9181

最後に

スラッシュなしをリダイレクトしているのはなぜかうまく Chrome がリダイレクトしてくれないので明示的にリダイレクトルールを記載しています

参考サイト

2024年5月22日水曜日

rq dashboard を docker compose で動かしてみる

rq dashboard を docker compose で動かしてみる

概要

過去に rq-dashboard を docker で動作させました
今回は docker compose で動作させます

環境

  • Ubuntu 22.04.4
  • docker 26.1.1

compose.yml

services:
  dashboard:
    image: eoranged/rq-dashboard
    environment:
      RQ_DASHBOARD_REDIS_URL: redis://172.17.0.1:6379
      RQ_DASHBOARD_USERNAME: user01
      RQ_DASHBOARD_PASSWORD: xxxxxxx
    ports:
      - 9181:9181

動作確認

  • docker compose up -d

で localhost:9181 で起動します
ベーシック認証ありで起動します

最後に

環境変数は RQ_DASHBOARD_ + 各種オプション名で指定可能です

2024年5月7日火曜日

RQ でジョブがエラーになった際の挙動を確認する

RQ でジョブがエラーになった際の挙動を確認する

概要

ジョブ内で raise した際の挙動を確認してみました

環境

  • macOS 11.7.10
  • Python 3.11.6
  • rq 1.16.1

サンプルコード

エンキューする側のコードは以下です

from datetime import timedelta

from redis import Redis
from rq import Queue
from rq_scheduler import Scheduler

from lib.util import Message

queue = Queue("default", connection=Redis(host="192.168.1.2"))
scheduler = Scheduler(queue=queue, connection=queue.connection)


scheduler.enqueue_in(timedelta(seconds=5), Message().say, msg="hello")

ジョブは単純に raise するようにしています

import time


class Message:
    def say(self, **kwargs):
        print("Start say")
        print(kwargs)
        time.sleep(10)
        raise ValueError()
        # print("End say")

起動

  • pipenv run rq worker
  • pipenv run rqscheduler
  • pipenv run app.py

動作確認

failed のジョブを確認

redis 内のキーの情報を確認すると failed したジョブの情報が格納されています

1) "rq:job:2c380d9c-c65b-41b8-8d92-3b85135782c0"
2) "rq:results:2c380d9c-c65b-41b8-8d92-3b85135782c0"
3) "rq:failed:default"
4) "rq:scheduler_instance:e276ed31a4434691b91c426e979a9e4d"
5) "rq:worker:86f028648da249c38de683f8ce10f865"
6) "rq:queues"

失敗したジョブは zset としてジョブIDが格納されています

127.0.0.1:6379> zrange rq:failed:default 0 -1
1) "2c380d9c-c65b-41b8-8d92-3b85135782c0"

結果には何も格納されていませんでした

ワーカーは自動でリトライすることはなさそう

オプションを付与していない限りリトライはしないようです

なおワーカー、スケジューラを再起動しても特にリトライはしませんでした

失敗したジョブの情報を確認するスクリプト

FailedJobRegistry として管理されています
以下のスクリプトを実行すると失敗したジョブの情報を一覧で表示してくれます

from redis import Redis
from rq import Queue
from rq.job import Job
from rq.registry import FailedJobRegistry

redis = Redis()
queue = Queue(connection=redis)
registry = FailedJobRegistry(queue=queue)

# Show all failed job IDs and the exceptions they caused during runtime
for job_id in registry.get_job_ids():
    job = Job.fetch(job_id, connection=redis)
    print(job_id, job.exc_info)

最後に

rq では基本はリトライせず失敗したジョブは別途管理されるようです

参考サイト

2024年4月26日金曜日

RQ のジョブのタイムアウトについて調査した

RQ のジョブのタイムアウトについて調査した

概要

RQ のジョブにはタイムアウトが設定されておりデフォルトでは 180 秒のようです
このタイムアウトは単純にワーカーが実行しているジョブが 180 秒経過しても終了しない場合には強制的にジョブを終了させる設定になります

環境

  • macOS 11.7.10
  • Python 3.11.6
  • rq 1.16.1

挙動を確認するためのスクリプト

ジョブは 190 秒待つようにします
初回はエンキュー時にタイムアウトなしでエンキューして本当にタイムアウトが発生するか確認します

  • vim lib/utils.py
import time


class Message:
    def say(self, **kwargs):
        print("Start say")
        print(kwargs)
        time.sleep(190)
        print("End say")
  • vim app.py
from datetime import timedelta

from redis import Redis
from rq import Queue
from rq_scheduler import Scheduler

from lib.util import Message

queue = Queue("default", connection=Redis(host="192.168.1.2"))
scheduler = Scheduler(queue=queue, connection=queue.connection)


scheduler.enqueue_in(timedelta(seconds=5), Message().say, msg="hello")
# scheduler.enqueue_in(timedelta(seconds=5), Message().say, msg="hello", timeout=3000)

実行

  • pipenv run rq worker
  • pipenv run rqscheduler
  • pipenv run app.py

ログ

実際に実行するとジョブの終了時に表示されるメッセージが表示されずに 180 経過してジョブが矯正終了していることがわかります
なお JobTimeoutException で終了したジョブはリトライなどはされませんでした

08:39:14 default: say(msg='hello') (c97f39f8-c197-43ac-828f-245d78ef78f0)
Start say
{'msg': 'hello'}
08:42:14 [Job c97f39f8-c197-43ac-828f-245d78ef78f0]: exception raised while executing (say)
Traceback (most recent call last):
  File "/Users/user/.local/share/virtualenvs/python-try-aR_k1rUJ/lib/python3.11/site-packages/rq/worker.py", line 1431, in perform_job
    rv = job.perform()
         ^^^^^^^^^^^^^
  File "/Users/user/.local/share/virtualenvs/python-try-aR_k1rUJ/lib/python3.11/site-packages/rq/job.py", line 1280, in perform
    self._result = self._execute()
                   ^^^^^^^^^^^^^^^
  File "/Users/user/.local/share/virtualenvs/python-try-aR_k1rUJ/lib/python3.11/site-packages/rq/job.py", line 1317, in _execute
    result = self.func(*self.args, **self.kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/user/data/repo/python-try/lib/util.py", line 8, in say
    time.sleep(190)
  File "/Users/user/.local/share/virtualenvs/python-try-aR_k1rUJ/lib/python3.11/site-packages/rq/timeouts.py", line 63, in handle_death_penalty
    raise self._exception('Task exceeded maximum timeout value ({0} seconds)'.format(self._timeout))
rq.timeouts.JobTimeoutException: Task exceeded maximum timeout value (180 seconds)

timeout=3000 にして再度試す

コメントにしていた部分を変更して再度エンキューしてみます
今度はジョブのタイムアウトを 3000 秒にしてエンキューします

すると先ほどタイムアウトしたジョブがタイムアウトせずにちゃんと最後まで実行されることが確認できました

08:52:14 default: say(msg='hello') (91aab157-25a5-44b9-898b-4da6dcb3baff)
Start say
{'msg': 'hello'}
End say
08:55:24 default: Job OK (91aab157-25a5-44b9-898b-4da6dcb3baff)
08:55:24 Result is kept for 500 seconds

最後に

ジョブの実行時間が長くなりそうな場合にはエンキュー時に timeout を設定してあげましょう

参考サイト

2024年4月25日木曜日

RQ のシグナルハンドリングについて調べてみる

RQ のシグナルハンドリングについて調べてみる

概要

RQ でプロセスハンドリングの挙動を確認してみました

環境

  • macOS 11.7.10
  • Python 3.11.6
  • rq 1.16.1

サンプルコード

  • vim app.py
from datetime import timedelta

from redis import Redis
from rq import Queue
from rq_scheduler import Scheduler

from lib.util import Message

queue = Queue("default", connection=Redis(host="192.168.1.2"))
scheduler = Scheduler(queue=queue, connection=queue.connection)


scheduler.enqueue_in(timedelta(seconds=5), Message().say, msg="hello")
  • vim lib/util.py
import time


class Message:
    def say(self, **kwargs):
        print("Start say")
        time.sleep(30)
        print(kwargs)
  • pipenv run rq worker
  • pipenv run rqscheduler
  • pipenv run python app.py

10秒以内にワーカーを Ctrl+c で停止した場合

ワーカーがジョブを掴んでからワーカーのプロセスを停止しようとするとちゃんと最後まで処理が終了してからワーカーが停止しました

11:21:53 default: say(msg='hello') (4d5a4db9-e8c9-4ee3-b64c-1dd266e66068)
Start say
11:21:55 Worker 98f9aac6f73445f685d4f5588abd514f [PID 32100]: warm shut down requested
{'msg': 'hello'}
11:22:03 default: Job OK (4d5a4db9-e8c9-4ee3-b64c-1dd266e66068)
11:22:03 Result is kept for 500 seconds
11:22:03 Worker rq:worker:98f9aac6f73445f685d4f5588abd514f: stopping on request
11:22:03 Unsubscribing from channel rq:pubsub:98f9aac6f73445f685d4f5588abd514f

kill -9 した場合

ワーカーがジョブを実行中に kill -9 を送信してみました
その場合ワーカーは Warm shut down せずにすぐに終了してしまいます

12:05:54 default: say(msg='hello') (b4eac96d-9591-482c-9753-e3a9c150d83b)
Start say
zsh: killed     pipenv run rq worker

redis 外部からアクセスできない場合

Protection mode が有効になっているので一旦無効にしましょう

  • CONFIG SET protected-mode no

docker の場合

docker stop はデフォルトでは SIGTERM -> SIGKILL の順番で信号を送ります
SIGKILL までの時間は time というオプションで変更可能でデフォルトで10秒になります

ワーカーを docker 上で動かし docker stop した際のワーカーの挙動を確認します

  • vim Dockerfile
FROM python:3.11.9-bullseye

COPY . /home
WORKDIR /home

RUN pip install pipenv
RUN pipenv install

CMD ["pipenv", "run", "rq", "worker", "--url", "redis://192.168.1.2:6379"]
  • docker build -t worker .
  • docker create --name worker worker
  • docker start worker
  • docker logs -f worker

でワーカーを起動します
スケジューラとエンキューは python から行います

  • pipenv run rqscheduler
  • pipenv run python app.py

これでワーカーがジョブを実行中にワーカーコンテナを停止します

  • docker stop worker
03:28:54 default: say(msg='hello') (76dbdc1b-5af2-4fa1-b4d2-49793454b235)
03:28:57 Worker 15c934ce3084457da63ade0e89ea14b6 [PID 1]: warm shut down requested

SIGTERM を受取り Warm shut down がスタートしますが10秒後に docker が強制的に SIGKILL を送信するためワーカーはジョブの完了を待たずに終了してしました

また強制終了したジョブは FailedJobRegistry に移動するようです

StartedJobRegistry cleanup: Moving job to FailedJobRegistry (due to AbandonedJobError)

time=60 にしてみる

SITTERM -> SIGKILL の送信にかかる時間を 60 秒に伸ばしてみます

  • docker stop --time=60 worker

すると今度はちゃんとワーカーがジョブの終了を待ってから停止していることが確認できました

03:32:54 default: say(msg='hello') (8e04f278-c5b1-4c4a-9069-3105960129f4)
03:33:14 Worker faa70ceee374410ba9416f55d74f4927 [PID 1]: warm shut down requested
Start say
{'msg': 'hello'}
03:33:25 default: Job OK (8e04f278-c5b1-4c4a-9069-3105960129f4)
03:33:25 Result is kept for 500 seconds
03:33:25 Worker rq:worker:faa70ceee374410ba9416f55d74f4927: stopping on request
03:33:25 Unsubscribing from channel rq:pubsub:faa70ceee374410ba9416f55d74f4927

最後に

RQ のシグナルハンドリングについて調べてみました
基本は RQ 側で実装されているので気にすることはないですが docker などと組み合わせる場合には SIGKILL の送信タイミングについて考慮する必要がありそうです

2023年11月17日金曜日

rqscheduler でエンキューする際にキーワード引数を指定するサンプル

rqscheduler でエンキューする際にキーワード引数を指定するサンプル

概要

kwargs を使ってワーカー側は受け取ることができるので特定の enqueue_in を判定するときなどに使えそうです

環境

  • Python 3.11.3
  • rq-scheduler 0.13.1

サンプルコード

from datetime import timedelta

from redis import Redis
from rq import Queue
from rq_scheduler import Scheduler

from my_lib.util import Message

queue = Queue("default", connection=Redis())
scheduler = Scheduler(queue=queue, connection=queue.connection)


scheduler.enqueue_in(timedelta(seconds=5), Message().say, msg="hello")
  • vim my_lib/util.py
class Message:
    def say(self, **kwargs):
        print(kwargs)

最後に

AttributeError: Can't get attribute 'Message' on <module '__main__' from '/home/user01/.local/share/virtualenvs/python-T6UYzsfV/bin/rqscheduler'>

という感じのエラーになる場合は enqueue_in する際のメソッドを素直にモジュール化しましょう
今回であれば my_lib/__init__.py の設置も忘れずに行いましょう

2023年9月5日火曜日

rq-scheduler の enqueue_in を使ってみた

rq-scheduler の enqueue_in を使ってみた

概要

enqueue_in はスケジューラの起点の時間から指定時間後にジョブをスケジューリングする機能です

具体的にどんな動きになるのか試してみました

環境

  • Python 3.11.3
  • flask-rq2 18.3
  • rq-scheduler 0.13.1

サンプルコード

from datetime import timedelta

from flask import Flask
from flask_rq2 import RQ

app = Flask(__name__)
app.config["RQ_REDIS_URL"] = "redis://localhost:6379/0"

rq = RQ(app)


class User:
    def __init__(self, name) -> None:
        self.name = name

    def say(self, msg):
        print(msg)


@app.route("/register")
def register_job():
    scheduler = rq.get_scheduler(queue="bar")
    job = scheduler.enqueue_in(timedelta(seconds=5), User("hawksnowlog").say, "HELLO")
    return job.get_id()

動作確認

まずはスケジューラを起動します

  • pipenv run rqscheduler
13:40:50 Registering birth

ここの時間が起点時間になります
次にワーカーを起動します

  • pipenv run rq worker bar

あとはアプリを起動しジョブを登録します

  • pipenv run flask run
  • curl localhost:5000/register

するとワーカーのジョブに起点時間から次の 1 分後にログが流れることが確認できると思います

13:41:50 bar: say('HELLO') (0378cd1a-274f-46f7-8484-00d513b98413)
HELLO
13:41:50 bar: Job OK (0378cd1a-274f-46f7-8484-00d513b98413)
13:41:50 Result is kept for 500 seconds

つまり timedelta の最小値は 1min になることがわかります
1min 以下を指定しても結局次の 1 分後にジョブが実行されることになります

最後に

もし秒単位で実行したい場合は少しトリッキーな使い方をする必要があるようです https://github.com/rq/rq-scheduler/issues/74

もしくは素直に rq の機能の enqueue を使う感じかなと思います

また enqueue_in を使った場合は redis 側への結果の保存は 500sec がデフォルトになるのでそのうちキーごと削除されます

参考サイト

2023年9月4日月曜日

rq のジョブに独自のメタ情報を付与する方法

rq のジョブに独自のメタ情報を付与する方法

概要

ジョブ登録時に meta 引数を指定することができます
辞書で好きな値を指定できます
pickle するので pickle 可能なオブジェクトにしましょう

環境

  • Python 3.11.3
  • flask-rq2 18.3
  • rq-scheduler 0.13.1

サンプルコード

from flask import Flask, request
from flask_rq2 import RQ

app = Flask(__name__)
app.config["RQ_REDIS_URL"] = "redis://localhost:6379/0"

rq = RQ(app)


def hello():
    raise Exception()


@app.route("/")
def get_jobs():
    # 検索する id をクエリストリングで渡せる
    id = request.args.get("id")
    scheduler = rq.get_scheduler(queue="bar")
    jobs_ = scheduler.get_jobs()
    # id が設定されていない場合は全件取得、そうでない場合は meta 情報を使って id 検索
    if id is None:
        jobs = jobs_
    else:
        jobs = [
            j for j in jobs_ if j.meta.get("id") is not None and j.meta.get("id") == id
        ]
    return [
        {
            "id": job.id,
            "meta": job.meta.get("id"),
        }
        for job in jobs
    ]


@app.route("/register")
def register_job():
    # id を meta 情報として設定する
    id = request.args.get("id")
    scheduler = rq.get_scheduler(queue="bar")
    job = scheduler.cron(
        "*/1 * * * *",
        func=hello,
        args=[],
        meta={"id": id},  # meta 属性に辞書を設定可能
    )
    return job.get_id()

最後に

ジョブを特定する際に使えると思います
ただ meta 情報で直接検索する機能がないので全件取得してから検索するしかないので redis のデータが膨大になる場合にはパフォーマンスなどの注意が必要です

参考サイト

2023年8月11日金曜日

rqscheduler でジョブの実行結果を保存する期間を指定する

rqscheduler でジョブの実行結果を保存する期間を指定する

概要

result_ttl をジョブ登録時に指定します
ただ最初に注意事項として記載しますが result_ttl を設定するとジョブのキー自体が削除されて再スケジュールされなくなるので注意してください

例えば1分おきのジョブを登録した場合に result_ttl=10 (10秒) に設定すると一度ジョブが実行されて10秒後にジョブ自体が消えてしまうので次の1分後にジョブが実行されなくなります

環境

  • Python 3.11.3
  • flask-rq2 18.3
  • rq-scheduler 0.13.1

サンプルコード

前回からのを流用しているのでインタフェースは Flask になります
ポイントは scheduler.cron 時に result_ttl を設定するところです

  • vim ./app.py
from flask import Flask, request
from flask_rq2 import RQ
from rq.job import Job

app = Flask(__name__)
app.config["RQ_REDIS_URL"] = "redis://localhost:6379/0"

rq = RQ(app)


def hello():
    return "hello"


@app.route("/update")
def update_job():
    job_id = request.args["job_id"]
    cron_string = request.args.get("cron_string", "*/2 * * * *")
    scheduler = rq.get_scheduler(queue="bar")
    job = Job.fetch(job_id, connection=scheduler.connection)
    job.meta["cron_string"] = cron_string
    job.save_meta()
    return {"id": job.id, "cron_string": job.meta.get("cron_string")}


@app.route("/")
def get_jobs():
    scheduler = rq.get_scheduler(queue="bar")
    jobs = scheduler.get_jobs()
    return [
        {
            "id": job.id,
            "cron_string": job.meta.get("cron_string"),
            "results": [
                f"{r.type}:{r.return_value}:{r.created_at}" for r in job.results()
            ],
        }
        for job in jobs
    ]


@app.route("/register")
def register_job():
    scheduler = rq.get_scheduler(queue="bar")
    job = scheduler.cron(
        "*/1 * * * *",
        func=hello,
        args=[],
        result_ttl=10,  # ここを追加、単位は秒
    )
    return job.get_id()

ちなみに redis 上では rq:results:xxx というキーに stream 型で保存されます
return_value は base64 エンコードされています

127.0.0.1:6379> type rq:results:2064a66f-b2cf-4e42-962d-e71d9313a4bd
stream
127.0.0.1:6379> xrange rq:results:2064a66f-b2cf-4e42-962d-e71d9313a4bd - +
1) 1) "1691107781067-0"
   2) 1) "type"
      2) "1"
      3) "return_value"
      4) "gAWVCQAAAAAAAACMBWhlbGxvlC4="
2) 1) "1691107841069-0"
   2) 1) "type"
      2) "1"
      3) "return_value"
      4) "gAWVCQAAAAAAAACMBWhlbGxvlC4="
3) 1) "1691107901019-0"
   2) 1) "type"
      2) "1"
      3) "return_value"
      4) "gAWVCQAAAAAAAACMBWhlbGxvlC4="

対策方法

幸いにも結果を保存するキーは別なので単純にそれだけ定期的に削除するようにすればいいかなと思います

127.0.0.1:6379> del rq:results:2064a66f-b2cf-4e42-962d-e71d9313a4bd

もしかするとそれをやってくれる API が rq にあるかもしれません

最後に

冒頭も記載しましたがジョブ (redis のキー自体) がなくなるので注意しましょう
公式にもありますが cron ジョブでは基本的には result_ttl=-1 で無期限かかなり長めの値を設定するのがいいようです

参考サイト

2023年8月7日月曜日

rqscheduler で登録したジョブの cron 情報を変更する方法

rqscheduler で登録したジョブの cron 情報を変更する方法

概要

ジョブの meta 情報を書き換えます
ジョブの操作は Flask-RQ2 を使っています

環境

  • Python 3.11.3
  • flask-rq2 18.3
  • rq-scheduler 0.13.1

サンプルコード

from flask import Flask, request
from flask_rq2 import RQ
from rq.job import Job

app = Flask(__name__)
app.config["RQ_REDIS_URL"] = "redis://localhost:6379/0"

rq = RQ(app)


def hello():
    return "hello"


@app.route("/update")
def update_job():
    job_id = request.args["job_id"]
    cron_string = request.args.get("cron_string", "*/2 * * * *")
    scheduler = rq.get_scheduler(queue="bar")
    job = Job.fetch(job_id, connection=scheduler.connection)
    job.meta["cron_string"] = cron_string
    job.save_meta()
    return {"id": job.id, "cron_string": job.meta.get("cron_string")}


@app.route("/")
def get_jobs():
    scheduler = rq.get_scheduler(queue="bar")
    jobs = scheduler.get_jobs()
    return [{"id": job.id, "cron_string": job.meta.get("cron_string")} for job in jobs]


@app.route("/register")
def register_job():
    scheduler = rq.get_scheduler(queue="bar")
    job = scheduler.cron(
        "*/1 * * * *",
        func=hello,
        args=[],
    )
    return job.get_id()

update でジョブのメタ情報を更新しています
Job.fetch で job_id を指定して取得しその後 job.meta を更新することで cron 情報を変更することができます

動作確認

  • pipenv run rq worker bar
  • pipenv run rqscheduler
  • pipenv run flask run flask

で各種プロセスを起動しておきます

  • curl localhost:5000/register

で一度ジョブを登録し

  • curl "localhost:5000/update?job_id=b6498c8c-f78b-4be6-a3e4-857c663f9d18&cron_string=%2A%2F3+%2A+%2A+%2A+%2A"

で cron 情報を書き換えます
そしてワーカーのログで更新した時間間隔で実行されていることを確認しましょう

念のため redis 側の情報も書き換わっているか確認しましょう
meta のフィールドが cron 情報になります

127.0.0.1:6379> hgetall rq:job:b6498c8c-f78b-4be6-a3e4-857c663f9d18
 1) "meta"
 2) "\x80\x05\x957\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x0bcron_string\x94\x8c\x0b*/3 * * * *\x94\x8c\x12use_local_timezone\x94\x89u."
 3) "timeout"
 4) "180"
 5) "result_ttl"
 6) "-1"
 7) "created_at"
 8) "2023-08-03T06:20:34.186347Z"
 9) "started_at"
10) "2023-08-03T06:54:52.683199Z"
11) "status"
12) "finished"
13) "stopped_callback_name"
14) ""
15) "failure_callback_name"
16) ""
17) "success_callback_name"
18) ""
19) "ended_at"
20) "2023-08-03T06:54:52.801018Z"
21) "data"
22) "x\x9ck`\x9d*\xca\x00\x01\x1a=\x9c\x89\x05\x05z\x19\xa999\xf9S\xfcb\xa7\xd4N)\x99\xa2\a\x00y^\t\x8a"
23) "enqueued_at"
24) "2023-08-03T06:54:52.671411Z"
25) "last_heartbeat"
26) "2023-08-03T06:54:52.801019Z"
27) "description"
28) "app.hello()"
29) "worker_name"
30) "e3bb9dd7b1f544ba9042ea2b1675cc50"
31) "origin"
32) "bar"

最後に

一度登録したジョブでもメタ情報を更新することで実行時間を変更することができました

参考サイト

2023年7月25日火曜日

rqschedulerでワーカーとスケジューラをスクリプトとして定義する方法

rqschedulerでワーカーとスケジューラをスクリプトとして定義する方法

概要

rq worker コマンドと rqscheduler コマンドを Python スクリプトとして定義する方法を紹介します

環境

  • Python 3.11.3
    • rq 1.15.0
    • rq-scheduler 0.13.1

ワーカーサンプルコード

rq worker コマンドの代用スクリプトです

  • vim ./worker.py
from redis import Redis
from rq.connections import Connection
from rq_scheduler import Scheduler

redis_conn = Redis(host="localhost", port=6379)
scheduler = Scheduler(connection=redis_conn)


if __name__ == "__main__":
    with Connection(connection=scheduler.connection):
        scheduler.run()

スケジューラサンプルコード

rqscheduler コマンドの代用スクリプトです

  • vim ./scheduler.py
from redis import Redis
from rq.queue import Queue
from rq.worker import Worker

redis_conn = Redis(host="localhost", port=6379)
queue = Queue("default", connection=redis_conn)


if __name__ == "__main__":
    worker = Worker(queues=[queue], connection=redis)
    worker.work(with_scheduler=True)

注意事項

実行する際はワーカー、スケジューラともに PYTHONPATH を設定してあげましょう

  • PYTHONPATH=./ pipenv run python ./worker.py
  • PYTHONPATH=./ pipenv run python ./scheduler.py

そうしないとジョブを登録するときとスケジューリングするときに実行する関数が見つからず NoModuleNameError になってしまいます

最後に

これで更に Worker や Scheduler クラスを継承することで独自のワーカークラスやスケジューラ実行クラスが作成できます

例えば複数のキューを扱うスケジューラを作成したり接続先の redis を動的に変更したりすることができます

参考サイト

2023年6月20日火曜日

rq のダッシュボードを動かす方法

rq のダッシュボードを動かす方法

概要

rq-dashboard を使います

環境

  • macOS 11.7.6
  • Python 3.10.2
    • rq 1.15.0
    • rq-scheduler 0.13.1

起動

  • docker run --rm -p 9181:9181 -e RQ_DASHBOARD_REDIS_URL=redis://172.17.0.1:6379 eoranged/rq-dashboard

172.17.0.1 は docker ホストの IP になります

動作確認

localhost:9181 にアクセスするとジョブやキュー、ワーカーの情報が UI で確認できるようになります

参考サイト

2023年6月11日日曜日

rqではワーカーに渡す関数はクラスのインスタンスメソッドでもよい

rqではワーカーに渡す関数はクラスのインスタンスメソッドでもよい

概要

エンキューする際に func 引数にインスタンスメソッドが指定可能か試してみました
結論としては指定可能です

環境

  • macOS 11.7.6
  • Python 3.10.2
    • rq 1.15.0
    • rq-scheduler 0.13.1

エンキュー

  • vim app.py
from redis import Redis
from rq import Queue
from rq_scheduler import Scheduler
from lib.util import Message

queue = Queue('bar', connection=Redis())
scheduler = Scheduler(queue=queue, connection=queue.connection)

scheduler.cron(
    "*/1 * * * *",
    func=Message("Hello!").say,
    args=[],
)

scheduler.run()

上記の func=Message("Hello!").say のポイントです
コーラブルであればいいのでインスタンスメソッドでも渡すことができます

ワーカー

  • vim lib/util.py
from datetime import datetime

class Message():
    def __init__(self, message: str):
        self.message = message

    def say(self):
        msg = f"{self.message}: {datetime.now()}"
        print(msg)
        return msg
  • vim worker.py
from rq import Worker, Queue
from redis import Redis

redis = Redis()
queue = Queue('bar', connection=redis)


if __name__ == "__main__":
    worker = Worker(queues=[queue], connection=redis)
    worker.work(with_scheduler=True)

動作確認

  • pipenv run python app.py
  • pipenv run python worker.py

最後に

これも pickle を使ってシリアライズしているため実現可能になっています
コンストラクタも呼ぶことができるので便利です
あとは引数と組み合わせればいろいろなケースに応じた処理ができるようになるかなと思います

rq の記事はいくつか紹介していますがワーカー側のコードはほぼ変わっていないのでコマンド実行でもいいかもしれません

2023年6月10日土曜日

rqではワーカーに渡す変数情報はクラスのオブジェクトでも良い

rqではワーカーに渡す変数情報はクラスのオブジェクトでも良い

概要

エンキューする際にシリアライザを内部的にコールしておりデフォルトのシリアライザが pikle を使っているためオブジェクトでもワーカーに渡すことができます

環境

  • macOS 11.7.6
  • Python 3.10.2
    • rq 1.15.0
    • rq-scheduler 0.13.1

エンキュー

  • vim app.py
from redis import Redis
from rq import Queue
from rq_scheduler import Scheduler
from lib.util import say, Message

queue = Queue('bar', connection=Redis())
scheduler = Scheduler(queue=queue, connection=queue.connection)

scheduler.cron(
    "*/1 * * * *",
    func=say,
    args=[Message("Hello!")],
)


scheduler.run()

上記の args=[Message("Hello!")], のポイントです
string や integer の値ではなく Message クラスのオブジェクトを渡すことができます

ワーカー

  • vim lib/util.py
from datetime import datetime

class Message():
    def __init__(self, message: str):
        self.message = message


def say(msg: Message):
    print(msg.message)
    return f"{msg.message}: {datetime.now()}"
  • vim worker.py
from rq import Worker, Queue
from redis import Redis

redis = Redis()
queue = Queue('bar', connection=redis)


if __name__ == "__main__":
    worker = Worker(queues=[queue], connection=redis)
    worker.work(with_scheduler=True)

動作確認

  • pipenv run python app.py
  • pipenv run python worker.py

最後に

デフォルトのシリアライザを拡張して独自のシリアライザを指定することも可能です

2023年6月9日金曜日

Flask-RQ2を試す

Flask-RQ2を試す

概要

前回 rq-scheduler を使って見ました
今回は Flask の拡張である Flask-RQ2 を使って rq-scheduler と組み合わせてみました

環境

  • macOS 11.7.6
  • Python 3.10.2
    • rq 1.15.0
    • rq-scheduler 0.13.1
    • flask 2.3.2
    • flask-rq2 18.3

流れ

  1. Flask アプリでリクエストを受け取りジョブを登録
  2. スケジューラの実行
  3. ワーカーの実行

という流れになります
つまり起動するプロセスも 3 つ必要になります

Flask アプリ: ジョブを登録する

  • vim app.py
from flask import Flask, request
from flask_rq2 import RQ
from lib.util import Message

app = Flask(__name__)
app.config['RQ_REDIS_URL'] = 'redis://localhost:6379/0'

rq = RQ(app)


@app.route("/")
def register_job():
    username = request.args.get('username', "default_user")
    # get_scheduler は rq-scheduler のオブジェクトが変えるので cron が使える
    scheduler = rq.get_scheduler(queue='bar')
    job = scheduler.cron(
        "*/1 * * * *",
        func=Message(f"{username}").say,
        args=[],
    )
    print(job)
    return f"<p>{job.get_id()}</p>"
  • vim lib/util.py
from datetime import datetime

class Message():
    def __init__(self, message: str):
        self.message = message

    def say(self):
        msg = f"{self.message}: {datetime.now()}"
        print(msg)
        return msg

以下で起動しておきます

  • pipenv run flask run

スケジューラの起動

スケジューラは登録されたジョブの内容に応じて実際にキューにジョブを登録しワーカーにジョブを渡す役割になります

今回は特にスケジューラはカスタムしないのでコマンドラインで起動します (カスタムする場合は python スクリプトを作成してそれを起動します)

  • pipenv run flask rq scheduler

flask-rq2 をインストールすると flask 経由でスケジューラを起動できるようになります

ワーカーの起動

最後にワーカーを起動しましょう
これもコマンドで起動できますがせっかくなのでスクリプトにしました

  • vim worker.py
from rq import Worker, Queue
from redis import Redis

redis = Redis()
queue = Queue('bar', connection=redis)


if __name__ == "__main__":
    worker = Worker(queues=[queue], connection=redis)
    worker.work(with_scheduler=True)

起動は

  • pipenv run python worker.py

で OK です

動作確認

  • curl localhost:5000

と実行するとジョブが登録されます
そしてスケジューラが登録されたジョブのルールーに沿ってキューにジョブを登録します

あとはワーカーのログにジョブが実行されているログが流れれば OK です

最後に

Flask-RQ2 を使ってみました

組み合わせて使わなくても OK ですが組み合わせることで特殊なデコレータが使えたり Flask の設定ファイルの管理と同じにしたりすることができます
また Queue や Worker などのモジュールも Flask 経由で取得できるようになります

参考サイト

2023年6月8日木曜日

Pythonのrq-schedulerを試す

Pythonのrq-schedulerを試す

概要

rq-scheduler は Python で使えるスケジューラツールです
特定の時間や条件に応じて何か処理を実行させることができます
バックエンドには redis を使います
今回はインストールから簡単なサンプルコードを動かしてみます

環境

  • macOS 11.7.6
  • Python 3.10.2
    • rq 1.15.0
    • rq-scheduler 0.13.1

インストール

  • pipenv install rq-scheduler

redis 起動

  • brew services run redis

サンプルコード

流れとしては

  1. ジョブを redis に登録
  2. 登録したジョブをワーカーが受け取り実行

という感じになります

ジョブを登録する

rq-scheduler の cron 機能を使ってジョブを登録してみます

  • vim enqueue.py
from redis import Redis
from rq import Queue
from rq_scheduler import Scheduler
from datetime import timedelta
from lib.util import say

queue = Queue('bar', connection=Redis())
scheduler = Scheduler(queue=queue, connection=queue.connection)


# 指定の時間に実行するジョブを登録
# scheduler.enqueue_at(datetime(2023, 6, 6, 14, 36), say, "hello")

# 指定の間隔で実行するジョブを登録
# scheduler.enqueue_in(timedelta(seconds=5), say, "hello")

# cron 形式でジョブを登録
scheduler.cron(
    "*/1 * * * *",
    func=say,
    args=["hello"],
)

scheduler.run()  # なくてもいい
  • pipenv run app.py

まずこれを実行すると以下のようなキーが redis に登録されます

redis-cli keys '*'
1) "rq:job:53acc2fb-aa88-4627-9a4c-183eab6ee07a"
2) "rq:scheduler_instance:6f65031c38a2416bb1352d1fca46dfa9"
3) "rq:scheduler:scheduled_jobs"

rq:job から始まるキーが登録されたジョブの実態になります
さらにしばらくすると

redis-cli keys '*'
1) "rq:queue:bar"
2) "rq:queues"
3) "rq:job:53acc2fb-aa88-4627-9a4c-183eab6ee07a"
4) "rq:scheduler:scheduled_jobs"
5) "rq:scheduler_instance:6f65031c38a2416bb1352d1fca46dfa9"

となっておりジョブが実際に bar キューに登録された状態になります (rq:queue:bar )

ジョブを実行するワーカー

ワーカーは rq モジュールだけで OK です
実際にキューイングされたジョブを実行します

キューイングした際に指定したモジュールの関数はこのワーカーからも参照できる必要があるので注意しましょう (lib.util.say は自動でワーカーが参照してコールする)

  • vim worker.py
from rq import Worker, Queue
from redis import Redis

redis = Redis()
queue = Queue('bar', connection=redis)


if __name__ == "__main__":
    worker = Worker(queues=[queue], connection=redis)
    worker.work(with_scheduler=True)
  • pipenv run python worker.py

実行すると以下のようなログが流れます

15:50:00 Worker rq:worker:9f0f761294194e79a4bb9b87c0e35436 started with PID 90351, version 1.15.0
15:50:00 Subscribing to channel rq:pubsub:9f0f761294194e79a4bb9b87c0e35436
15:50:00 *** Listening on bar...
15:50:00 Cleaning registries for queue: bar
15:50:00 bar: lib.util.say('hello') (53acc2fb-aa88-4627-9a4c-183eab6ee07a)
hello
15:50:00 bar: Job OK (53acc2fb-aa88-4627-9a4c-183eab6ee07a)
15:50:00 Result will never expire, clean up result key manually

redis 内は以下のようになりました

keys *
 1) "rq:clean_registries:bar"
 2) "rq:job:53acc2fb-aa88-4627-9a4c-183eab6ee07a"
 3) "rq:finished:bar"
 4) "rq:scheduler_instance:6f65031c38a2416bb1352d1fca46dfa9"
 5) "rq:worker:9f0f761294194e79a4bb9b87c0e35436"
 6) "rq:scheduler-lock:bar"
 7) "rq:queues"
 8) "rq:workers"
 9) "rq:results:53acc2fb-aa88-4627-9a4c-183eab6ee07a"
10) "rq:workers:bar"
11) "rq:scheduler:scheduled_jobs"

いろいろ増えていますが完了したジョブは rq:finished:bar にあります
結果は rq:results:53acc2fb-aa88-4627-9a4c-183eab6ee07a に格納されており今回の場合はあれば永遠に保存されます

ワーカーは実装しなくても動かせる

今回のように簡単なワーカーであれば以下のコマンドだけでも OK です

  • pipenv run rq worker -q bar

エンキューの際に実行するべきメソッドが指定されているのでワーカーはどのメソッドを知る必要がないためコマンドだけでもワーカーが起動できるようになっています

再度エンキューを実行した場合に新たにジョブが登録される

もし app.py を終了して再度 app.pyを実行すると新たにジョブが登録されます
つまり前に登録した cron ジョブは残ったままになるので app.py を実行するたびに redis には新しいジョブが登録されます

ジョブの登録を解除する

もう前のジョブはいらないということであれば登録を解除してあげましょう

  • vim delete.py
from redis import Redis
from rq import Queue
from rq_scheduler import Scheduler

queue = Queue('bar', connection=Redis())
scheduler = Scheduler(queue=queue, connection=queue.connection)

jobs = scheduler.get_jobs()
for job in jobs:
    if job.get_id() == "53acc2fb-aa88-4627-9a4c-183eab6ee07a":
        print(scheduler.cancel(job))

これで再度 redis を確認すると rq:job:53acc2fb-aa88-4627-9a4c-183eab6ee07a というジョブが削除されているのが確認できると思います

最後に

rq-scheduler を試してみました
特定の時間に実行するジョブを登録しておけば定期的にワーカーが実行してくれるシステムを作りたいときに使えるかなと思います

実際はワーカーからのフィードバックやワーカーのタイムアウト、動的なジョブの登録に対応できるようにする必要がありそうです

参考サイト