[Laravel] トランザクションの再試行について誤解していた
トランザクションの再試行についての誤解
DB::transaction()
の第二引数である $attempts
に値を渡すことで、指定回数を上限としてクエリの実行を再試行してくれる機能があります。
第一引数の $callback
内で例外が発生したときでも再試行してくれると思っていましたが、実際には違っていました。
サンプルコードで検証
以下のコードでは $i
は 3
になることを期待していましたが実際には 1
になりました。
$i = 0;
DB::transaction(function () use (&$i) {
$i += 1;
throw new Exception('Error');
}, 3);
// 期待: 3, 実際: 1
dd($i);
これは DB::transaction()
が再試行する条件がデッドロックに関する例外が発生した場合のみに限定されるためです。
実装から再試行の条件を見る
DB::transaction()
の実装を読んで再試行の条件を確認します。
以下は関連コードの抜粋です。
trait ManagesTransactions
{
public function transaction(Closure $callback, $attempts = 1)
{
for ($currentAttempt = 1; $currentAttempt <= $attempts; $currentAttempt++) {
$this->beginTransaction();
try {
// $callback を実行し結果を保持する
$callbackResult = $callback($this);
} catch (Throwable $e) {
// $callback で例外が発生した場合は handleTransactionException が呼ばれる
$this->handleTransactionException($e, $currentAttempt, $attempts);
continue;
}
// 以下省略
}
}
protected function handleTransactionException(Throwable $e, $currentAttempt, $maxAttempts)
{
// 省略
// ロールバックして
$this->rollBack();
// $e がデッドロックに関する例外かつ現在の試行回数が上限より小さい場合は何もせず抜ける
if ($this->causedByConcurrencyError($e) && $currentAttempt < $maxAttempts) {
return;
}
// 例外がデッドロックに関係ないものや試行回数が上限まで行った場合は例外を投げる
throw $e;
}
}
trait DetectsConcurrencyErrors
{
protected function causedByConcurrencyError(Throwable $e)
{
// デッドロックに関する例外である
if ($e instanceof PDOException && ($e->getCode() === 40001 || $e->getCode() === '40001')) {
return true;
}
$message = $e->getMessage();
// もしくは配列のいずれかの文字列が例外のメッセージに含まれる
return Str::contains($message, [
'Deadlock found when trying to get lock',
'deadlock detected',
'The database file is locked',
'database is locked',
'database table is locked',
'A table in the database is locked',
'has been chosen as the deadlock victim',
'Lock wait timeout exceeded; try restarting transaction',
'WSREP detected deadlock/conflict and aborted the transaction. Try restarting the transaction',
]);
}
}
handleTransactionException
では、デッドロック関連の例外かつ試行回数が上限に満たない場合のみ、再試行するようになっています。
例外がデッドロックとは関係のないもの、もしくは試行回数が上限に達した場合は発生した例外がそのまま投げられるようになっていました。
ドキュメントの確認
公式のドキュメントには以下のように記載があります。
原文:
If an exception is thrown within the transaction closure, the transaction will automatically be rolled back and the exception is re-thrown.
翻訳:
トランザクション クロージャ内で例外がスローされた場合、トランザクションは自動的にロールバックされ、例外が再スローされます。
原文:
The transaction method accepts an optional second argument which defines the number of times a transaction should be retried when a deadlock occurs. Once these attempts have been exhausted, an exception will be thrown:
翻訳:
トランザクション メソッドは、デッドロックが発生したときにトランザクションを再試行する回数を定義するオプションの 2 番目の引数を受け入れます。これらの試行がすべて完了すると、例外がスローされます。
まとめ
-
DB::transaction()
で再試行が行われるのは、デッドロックの発生時のみ - ドキュメントに目を通し、理解したうえで実装する
Discussion