Step#advance! がカーソルの `succ` 実装エラーを隠す問題を解消
ActiveJob の継続処理で、カーソルの succ が内部エラーを起こした際に本来の例外が失われ、誤った UnadvanceableCursorError が報告されていた問題を、実装有無の判定へ置き換えることで解決しました。
背景
Step#advance! は継続処理の次ステップへ進める際、現在のカーソルオブジェクトの succ を呼び出すメソッドです。従来は succ 呼び出し全体を rescue NoMethodError で捕捉し、succ が未実装の場合に UnadvanceableCursorError を送出していました。しかし、succ の実装内部で別の NoMethodError が発生した場合でも同様に捕捉され、元々の例外情報とバックトレースが失われていました。この挙動は、デバッグ時に実際の原因が隠蔽され、誤解を招く結果となっていました。
技術的な変更
実装は from.respond_to?(:succ) による事前判定へ置き換えられ、succ が未実装のケースのみ UnadvanceableCursorError が発生します。コードの差分は以下の通りです。
変更前:
def advance!(from: nil)
from = cursor if from.nil?
begin
to = from.succ
rescue NoMethodError
raise UnadvanceableCursorError, "Cursor class '#{from.class}' does not implement 'succ'"
end
set! to
end
変更後:
def advance!(from: nil)
from = cursor if from.nil?
unless from.respond_to?(:succ)
raise UnadvanceableCursorError, "Cursor class '#{from.class}' does not implement 'succ'"
end
to = from.succ
set! to
end
この修正により、succ 実装が存在していても内部で NoMethodError が発生した場合はそのまま伝搬し、開発者は正確な例外情報を得られます。また、nil や Float など succ を持たない標準オブジェクトは依然として respond_to? が false を返すため、従来通りのエラーハンドリングが維持されます。
設計判断
respond_to?(:succ) による事前チェックは、メソッドの存在確認という明示的な契約に基づく判断です。これにより、例外処理は本来の目的(未実装の検出)に限定され、予期しない例外のサイレント化を防止します。PR の議論では、method_defined? や public_methods などの代替案も検討されましたが、respond_to? がプライベートメソッドも含めた包括的判定を提供し、かつシンプルである点が採用理由となりました。
まとめ
この変更は Step#advance! のエラーハンドリングを、未実装チェックに特化させることで、カーソル実装内部の本来のエラーが隠蔽される問題を解消しました。結果として、デバッグ情報の可視性が向上し、既存の動作互換性も維持されています。