動的スケジューリング対応:実行時にRecurringTaskを追加・削除可能に
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_task は dynamic スコープを通じて削除するため、static: true のタスクを誤って削除しようとすると ActiveRecord::RecordNotFound が発生します。静的タスクと動的タスクを明確に分離した安全な設計です。
パブリックAPIの追加
SolidQueue モジュールに schedule_recurring_task と unschedule_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_enabled と polling_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_schedule は recurring_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::Configuration に SCHEDULER_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_task が dynamic スコープを経由することで、静的タスクの誤削除を防ぐ安全策が設けられています。プログラムによる削除APIが静的設定に干渉しないよう、操作対象を明確に分離した判断といえます。
まとめ
本PRは、static フラグとオプトインの dynamic_tasks_enabled 設定を軸に、既存の静的スケジュール機能との後方互換性を完全に維持しながら、動的スケジューリングを実現した変更です。設定ファイルなしでもスケジューラーを起動できるようになったことで、純粋に動的タスクのみで運用するアーキテクチャも選択肢に入ります。