RedashでScheduler更新時に発生するエラー「There’s already an active RQ scheduler」について原因を調べて解消してみた

ECS(Fargate)上で動かしていたRedashを更新しようとすると、毎回Schedulerのタスクが更新完了しない状態でした。GitHub Actions上でのCDとした場合には稼働時間超過に繋がってきます。止む無く管理コンソール上からSchedulerの古いタスクを叩きおとしていました。エラーメッセージを元に原因を辿りつつRedashのFeatureを参照した結果、恒久対処することができました。
2021.07.09

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

RedashのDeploy用WorkflowをGitHub Actions上に設置したものの、毎回Scheduler更新だけが何故か完了せず、Actionsの稼働時間を浪費しがちな状態になっていました。

管理コンソール上でFargateのタスク一覧から古いSchedulerを手動で叩き落としてあげると完了します。ただ、本番環境でそれを踏襲すると

  1. Actions から Workflow を実行
  2. Scheduler更新のタイミングで管理コンソールを開く
  3. 古いSchedulerタスクを叩き落とす
  4. Actions の Workflow が完了するまで見守る

という作業が入り、Redashの更新にはそれなりの手間が発生します。正直イケてない。

Redash本体であれこれできる問題でもなさそうで頭を抱え気味だったのですが、RQ Scheduler自体のプロセス終了方法について辿ってみたところシンプルな解決方法に至りました。関連すること含めてまとめてみました。

該当するRedashのバージョン

この記事記述時点でのRedash安定版(8.0.0.b32245)で発生します。beta版でも依存するRQ Schedulerのバージョンは変わらないようで、同じく発生するでしょう。

原因

CloudWatchに上がるログを見る限り RQ Scheduler が悪さをしているらしいとは認識していました。

ValueError: There's already an active RQ scheduler

該当のエラーが発生するのは以下の部分です。接続判定に用いるkeyが存在しつつステータスが death ではない(=維持)のため、新規接続プロセスは落とされたと読み取れます。

    def register_birth(self):
        self.log.info('Registering birth')
        if self.connection.exists(self.scheduler_key) and \
                not self.connection.hexists(self.scheduler_key, 'death'):
            raise ValueError("There's already an active RQ scheduler")

rq-scheduler/scheduler.py at 81c23cb3d1e72c7ff21323fdde71095fb056b51d · rq/rq-scheduler

このkey構成はどうなっているかというと、常に固定です。

class Scheduler(object):
    scheduler_key = 'rq:scheduler'

rq-scheduler/scheduler.py at 81c23cb3d1e72c7ff21323fdde71095fb056b51d · rq/rq-scheduler - https://github.com/

また、このkeyの期限はループ処理中のjobをqueueに追加するタイミングで更新されており、エラーが発生しない限りは永続です。

    def enqueue_jobs(self):
        """
        Move scheduled jobs into queues.
        """
        self.log.debug('Checking for scheduled jobs')


        jobs = self.get_jobs_to_queue()
        for job in jobs:
            self.enqueue_job(job)


        # Refresh scheduler key's expiry
        self.connection.expire(self.scheduler_key, int(self._interval) + 10)
        return jobs

rq-scheduler/scheduler.py at 81c23cb3d1e72c7ff21323fdde71095fb056b51d · rq/rq-scheduler - https://github.com/

そして、ステータスが終了となるのはrq-schedulerにエラーが発生したときのみです。

    def register_death(self):
        """Registers its own death."""
        self.log.info('Registering death')
        with self.connection.pipeline() as p:
            p.hset(self.scheduler_key, 'death', time.time())
            p.expire(self.scheduler_key, 60)
            p.execute()

rq-scheduler/scheduler.py at 81c23cb3d1e72c7ff21323fdde71095fb056b51d · rq/rq-scheduler - https://github.com/

    def _install_signal_handlers(self):
        """
        Installs signal handlers for handling SIGINT and SIGTERM
        gracefully.
        """


        def stop(signum, frame):
            """
            Register scheduler's death and exit
            and remove previously acquired lock and exit.
            """
            self.log.info('Shutting down RQ scheduler...')
            self.register_death()
            self.remove_lock()
            raise SystemExit()


        signal.signal(signal.SIGINT, stop)
        signal.signal(signal.SIGTERM, stop)

rq-scheduler/scheduler.py at 81c23cb3d1e72c7ff21323fdde71095fb056b51d · rq/rq-scheduler - https://github.com/

つまり、何らかのエラーが発生しない限りは維持され続けます。新しいSchedulerが実行されても既存keyでの接続があるため、実行直後の self.register_birth() でエラーを発生して終了します。つまりDeploy操作のみでは永遠に切り替わりません。

通常利用中にエラーが発生しとして、タイミングが一致すれば切り替わるかもしれませんが、所謂動作検証中で利用者がほぼいない場合にはそれも見込めなくなります。

対処方法

rq-schedulerのバージョンを0.10.0以降に更新します。

pip install rq-scheduler==0.10.0

0.10.0以降では以下のFeatureが取り込まれたことでステータス維持に利用しているkey構成が 固定文字+(ユーザ指定文字|uuid4().hex) となっており、複数のScheduler生成が可能になっています。

タスク数を1としていた場合は、新しいSchedulerが生成された後に以前のSchedulerが自動で終了するわけです。

あとがき

Redashを利用する上で遭遇するRQ-Scheduler関係のトラブルについて、相談系スレッドを幾つかは検索すると発見可能です。ただ、いずれもRQ-Schedulerのバージョン更新には触れられていないようです。

とりあえずRedashの更新でRQ-Schedulerが原因でSchedulerが切り替わらないというトラブルに見舞われたら、今回紹介したrq-scedulerの更新を試してみると解消されるかもしれません。