Active Job: `stopping?` にジョブを渡してきめ細かい割り込み制御を実現
Active Jobのチェックポイント機構が拡張され、キューアダプターが割り込み判断時にジョブ自身を参照できるようになりました。これにより、Solid QueueやGood Jobといったバックエンドがキュー名などのジョブ属性に基づいた一時停止機能を、長時間実行ジョブのコンティニュエーションと連携させることが可能になります。
背景
Solid Queue、Good Job、Sidekiq Proなどのバックエンドは、キュー名などのジョブ属性に基づいてジョブ処理を一時停止する機能を持っています。しかし、コンティニュエーション(ActiveJob::Continuable)を使った長時間実行ジョブでは、チェックポイントで呼ばれる stopping? がジョブの情報を受け取れなかったため、この一時停止機能と連携できませんでした。
これまでの stopping? はジョブに依らない単純な真偽値を返すだけでした。キューアダプター側では「どのジョブが止まろうとしているか」を知る手段がなく、ジョブ属性(キュー名、引数など)に基づいた細粒度の割り込み制御は不可能でした。グレースフルシャットダウンが開始されるまで、一時停止中のキューのジョブも実行し続けてしまうという問題がありました。
この制約を解消するため、stopping? にジョブインスタンスを渡す設計変更が#57472で導入されました。
技術的な変更
チェックポイント時にジョブインスタンスを stopping? へ渡すよう変更され、戻り値によって割り込み理由を動的に指定できるようになりました。
activejob/lib/active_job/continuable.rb の checkpoint! メソッドが拡張されました。
変更前:
def checkpoint! # :nodoc:
interrupt!(reason: :stopping) if queue_adapter.stopping?
end
変更後:
def checkpoint! # :nodoc:
if (reason = queue_adapter.stopping?(self))
reason = :stopping if reason == true
interrupt!(reason: reason)
end
end
戻り値が true の場合はこれまで通り :stopping を理由として使用し、それ以外の truthy な値はそのまま割り込み理由として利用されます。これにより :queue_paused のようなカスタム理由を返すことが可能になりました。
アダプター側のインターフェースも更新されています。abstract_adapter.rb と test_adapter.rb の stopping? がいずれも job = nil のオプション引数を受け取るシグネチャに変更されました。
# 変更前
def stopping?
!!@stopping
end
# 変更後
def stopping?(job = nil)
!!@stopping
end
# 変更前
def stopping?
@stopping.is_a?(Proc) ? @stopping.call : @stopping
end
# 変更後
def stopping?(job = nil)
@stopping.is_a?(Proc) ? @stopping.call(job) : @stopping
end
テストヘルパーの interrupt_job_during_step と interrupt_job_after_step も更新されました。第一引数の名前が job から job_class に変更され、reason: キーワード引数が追加されています。また、ラムダの引数もジョブインスタンスを受け取るよう変更されています。
# 変更前
def interrupt_job_during_step(job, step, cursor: nil, &block)
queue_adapter.with(stopping: ->() { during_step?(job, step, cursor: cursor) }, &block)
end
# 変更後
def interrupt_job_during_step(job_class, step, cursor: nil, reason: true, &block)
stopping = ->(job) { reason if job.is_a?(job_class) && during_step?(job, step, cursor: cursor) }
queue_adapter.with(stopping: stopping, &block)
end
テストヘルパーの変更では、引数名の変更(job → job_class)と共に、ラムダがジョブインスタンスを受け取って is_a? で型チェックを行うようになっています。これにより、複数のジョブクラスが同時に実行されている場合でも、特定クラスのジョブだけを選択的に割り込めます。
設計判断
後方互換性を最優先にしたオプション引数の採用が、この変更の核心的な設計判断です。
stopping?(job = nil) のシグネチャは、既存のキューアダプターが引数なしの stopping? を実装していても、Rails側から引数を渡してもエラーにならないことを意図しています。CHANGELOGのエントリにも「def stopping?(job = nil) として実装することで、このバージョンと旧バージョンのRailsの両方で動作する」と明記されており、サードパーティアダプターの移行を段階的に行えます。
また、戻り値のセマンティクスも慎重に設計されています。true を :stopping にマッピングすることで既存の動作を保ちつつ、任意の truthy 値をカスタム理由として利用可能にしています。これにより、:queue_paused、:rate_limited などのバックエンド固有の割り込み理由をログやモニタリングに活用できます。
テストヘルパーにおける引数名の変更(job → job_class)は、「クラスを渡す」という意図をAPIレベルで明確化したもので、型の不一致によるバグを防ぐ効果があります。
まとめ
本変更は、チェックポイントAPIにジョブインスタンスを渡すという最小限の変更で、キューアダプターに細粒度の割り込み制御という大きな表現力を与えています。オプション引数による後方互換性の確保と、戻り値によるカスタム割り込み理由の伝播という設計により、Solid Queue・Good Job等の既存アダプターが段階的にこの機能へ対応できる経路が整備されました。