トランザクションの中で dispatch したジョブが「レコードが見つからない」で失敗しました。なぜですか。
コミット前にワーカーがジョブを拾うからです。ジョブから見ると対象レコードはまだ存在しません。after_commit を有効にするか、コミット後に投げる形へ変えれば解消します。
面接官が見ているのは定番の罠。実務で踏んだことがあるかが出る。
これは「失敗したジョブはどうなりますか。」への追撃質問です。
よくある答えと、面談でどう見られるか
- コミット前にワーカーがジョブを拾ってしまうから。after_commit / afterCommit() で回避する
正解です。ワーカーが別プロセスで走っている以上、必ず起こりうる競合です。 - トランザクション内では dispatch 自体が失敗するから
dispatch は成功します。成功して先に実行されてしまうのが問題です。 - キューのドライバが database だと DB がロックされるから
ロックが絡む場面もありますが、原因はタイミングです。redis でも同じことが起きます。 - ジョブのシリアライズでモデルの内容が失われるから
惜しい着眼点です。ジョブはモデルの ID だけを持って復元しますが、問題はその ID の行がまだコミットされていないことです。
面接官は何を見ているか
- コミット前にワーカーがジョブを拾うから、と原因を説明できた
- 競合状態(レースコンディション)だと認識できた
- after_commit / afterCommit() による対処を挙げた
模範解答
トランザクションがコミットされる前に、ワーカーが先にジョブを拾ってしまうからです。ジョブから見ると対象レコードはまだ存在しません。ワーカーが別プロセスで走っている以上、必ず起こりうる競合です。
対処は2つ。config/queue.php の該当コネクションに after_commit => true を入れるか、その場だけなら dispatch にチェーンして ProcessPodcast::dispatch($x)->afterCommit()と書きます。どちらも「開いているトランザクションが全てコミットされるまで投入を待つ」挙動になります(トランザクションが開いていなければ即座に投入されます)。
面談で効くのは、この設定の副作用まで言えるかどうかです。after_commit => true はジョブだけでなく、キュー投入されたイベントリスナー・メール・通知・ブロードキャストにも一律で効きます。逆に特定のジョブだけ即座に投入したいときは beforeCommit() で個別に打ち消せます。また、トランザクションが例外でロールバックされた場合、その中で dispatch されたジョブは投入されずに破棄されます。