動的スケジューリング対応:実行時にRecurringTaskを追加・削除可能に

rails/solid_queue

Solid Queueのスケジューラーがresque-schedulerスタイルの動的スケジュール機能に対応しました。これにより、静的な設定ファイルを変更することなく、実行時にRecurringTaskの追加・削除が可能になります。

背景

これまでSolid QueueのRecurringTask(繰り返しタスク)は、config/recurring.ymlなどの静的設定ファイルでのみ定義できました。#186では、resque-schedulerやsidekiq-schedulerが提供するような動的スケジュール機能の要望が挙がっていました。Solid Queueはすべてのジョブ情報をデータベースで管理しているため、動的スケジュールとの親和性は高く、今回のPR #553でその機能が実装されました。

静的設定ファイルの変更にはデプロイが必要ですが、動的スケジュールを使えばAPIやコンソール経由でタスクをその場で追加・削除できます。マルチテナント構成やユーザー定義のスケジュールジョブなど、設定ファイルでは対応しにくいユースケースをカバーします。

技術的な変更

RecurringTaskモデルの拡張

SolidQueue::RecurringTask モデルに dynamic スコープと、動的タスクの作成・削除メソッドが追加されました。既存の static スコープ(where(static: true))に対し、dynamic スコープは where(static: false) として定義されます。

scope :static, -> { where(static: true) }
scope :dynamic, -> { where(static: false) }

def create_dynamic_task(key, **options)
  from_configuration(key, **options.merge(static: false)).save!
end

def delete_dynamic_task(key)
  RecurringTask.dynamic.find_by!(key: key).destroy
end

delete_dynamic_taskdynamic スコープを通じて削除するため、static: true のタスクを誤って削除しようとすると ActiveRecord::RecordNotFound が発生します。静的タスクと動的タスクを明確に分離した安全な設計です。

パブリックAPIの追加

SolidQueue モジュールに schedule_recurring_taskunschedule_recurring_task の2メソッドが追加され、アプリケーションコードから直接呼び出せるようになりました。

def schedule_recurring_task(key, **options)
  RecurringTask.create_dynamic_task(key, **options)
end

def unschedule_recurring_task(key)
  RecurringTask.delete_dynamic_task(key)
end

これにより、Railsアプリケーションのコントローラーやサービスオブジェクトから SolidQueue.schedule_recurring_task("my_task", class: "MyJob", schedule: "every hour") のように呼び出せます。

Schedulerプロセスの動的タスク監視

SolidQueue::Scheduler が起動時オプションとして dynamic_tasks_enabledpolling_interval を受け取るようになり、ループ内で定期的に動的タスクの変更をDBから取得します。

# 変更前
SLEEP_INTERVAL = 60 # Right now it doesn't matter, can be set to 1 in the future for dynamic tasks

def run
  loop do
    break if shutting_down?
    interruptible_sleep(SLEEP_INTERVAL)
  end
end

# 変更後
STATIC_SLEEP_INTERVAL = 60

def run
  loop do
    break if shutting_down?
    reload_dynamic_schedule if dynamic_tasks_enabled?
    interruptible_sleep(sleep_interval)
  end
end

reload_dynamic_schedulerecurring_schedule.reschedule_dynamic_tasks を呼び出してDBの現在状態を取得し、新たに追加されたタスクをスケジュールし、削除されたタスクをアンスケジュールします。その後 reload_metadata でプロセスのメタデータを更新し、稼働中のSchedulerが常に最新のタスク一覧を反映するようにしています。

RecurringScheduleの静的・動的分離

SolidQueue::Scheduler::RecurringSchedule では、@configured_tasks が廃止され、静的タスクと動的タスクを個別に管理する構造に変更されました。

# 変更前
def initialize(tasks)
  @configured_tasks = Array(tasks).map { |task| SolidQueue::RecurringTask.wrap(task) }.select(&:valid?)
  @scheduled_tasks = Concurrent::Hash.new
end

# 変更後
def initialize(static_tasks, dynamic_tasks_enabled: false)
  @static_tasks = Array(static_tasks).map { |task| RecurringTask.wrap(task) }.select(&:valid?)
  @dynamic_tasks_enabled = dynamic_tasks_enabled
  @scheduled_tasks = Concurrent::Hash.new
end

def configured_tasks
  static_tasks + dynamic_tasks
end

configured_tasks はメソッドとして再定義され、呼び出すたびに静的タスクと動的タスクを合算して返します。これにより reschedule_dynamic_tasks が動的タスクのみをDB再読み込みの対象にでき、静的タスクを毎回再評価する必要がなくなっています。

Configuration変更:静的設定ファイルなしで起動可能に

SolidQueue::ConfigurationSCHEDULER_DEFAULTS が追加され、dynamic_tasks_enabled: false がデフォルト値として定義されました。スケジューラーの起動条件も変更され、静的タスクがなくても dynamic_tasks_enabled: true であればスケジューラープロセスが起動します。

# 変更前
def schedulers
  if !skip_recurring_tasks? && recurring_tasks.any?
    [ Process.new(:scheduler, recurring_tasks: recurring_tasks) ]
  else
    []
  end
end

# 変更後
def schedulers
  return [] if skip_recurring_tasks?

  if recurring_tasks.any? || dynamic_recurring_tasks_enabled?
    [ Process.new(:scheduler, { recurring_tasks: recurring_tasks, **scheduler_options.with_defaults(SCHEDULER_DEFAULTS) }) ]
  else
    []
  end
end

設定ファイルでの指定方法は以下のとおりです:

production:
  scheduler:
    dynamic_tasks_enabled: true
    polling_interval: 5

polling_interval のデフォルト値は 5(秒)です。

設計判断

staticフラグによる静的・動的タスクの共存設計 が採用されています。recurring_tasks テーブルの既存 static カラムを活用し、同一テーブル内で静的・動的タスクを区別します。これにより既存のDB構造を変更せずに機能追加が実現されています。

dynamic_tasks_enabled フラグをオプトイン方式にした点も注目です。デフォルトは false であり、既存のユーザーはこのフラグを明示的に有効化しない限り、スケジューラーの動作に変化はありません。動的タスク監視のポーリングループは dynamic_tasks_enabled? の条件下でのみ動作するため、静的スケジュールのみの構成ではオーバーヘッドが発生しません。

また delete_dynamic_taskdynamic スコープを経由することで、静的タスクの誤削除を防ぐ安全策が設けられています。プログラムによる削除APIが静的設定に干渉しないよう、操作対象を明確に分離した判断といえます。

まとめ

本PRは、static フラグとオプトインの dynamic_tasks_enabled 設定を軸に、既存の静的スケジュール機能との後方互換性を完全に維持しながら、動的スケジューリングを実現した変更です。設定ファイルなしでもスケジューラーを起動できるようになったことで、純粋に動的タスクのみで運用するアーキテクチャも選択肢に入ります。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
4a73f03e

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→各論(背景、技術的な変更、設計判断)→まとめ(結論)の3部構成が明確に適用されており、各セクションの役割も適切です。必須要素はすべて満たされています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きのシンタックスハイライト(```言語:ファイルパス)やPR/Issue番号のリンク記法([#123](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Ruby on Railsの内部実装やバックグラウンドジョブシステムに関する専門的な内容であり、対象読者であるエンジニアに適した技術レベルと表現です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが要約から詳細へと展開され、各段落の1文目がトピックセンテンスとして機能しています。1段落1トピックの原則も守られており、構成が非常に明瞭です。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内のコードブロックは、提供されたDiffの内容を正確に反映しています。ファイルパス、変更前後のコード、追加されたメソッドなどがすべて一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

PR DescriptionやDiffで使われている`scope`, `static`, `dynamic`, `Scheduler`などの技術用語を正確に使用しており、文脈に適した用語選択がなされています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

技術的な説明はすべて正確であり、Diffの内容と論理的に整合しています。特に、動的タスクの有効化によるSchedulerの起動条件の変更や、`delete_dynamic_task`の安全性に関する説明は的確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、提供されたPRのDescriptionやDiffの内容に基づいています。設計判断のセクションもコードの変更から論理的に導出できる内容であり、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#553)、Issue番号(#186)、設定値(polling_interval: 5)など、記事内の数値や固有名詞はすべて正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRのタイトル「Dynamic scheduled tasks」の内容を的確に要約し、日本語の読者にとってより分かりやすい表現になっています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PR情報に記載のない、バージョンのサポート状況やリリース日程などの外部知識は含まれていません。言及されている他のライブラリも、背景説明として適切です。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

変更が完了したことを示す「〜しました」「〜になります」といった時制が正しく使われており、PRの内容と時間表現に矛盾はありません。