はじめに
PlayFramework(Java)でEbeanを使って開発をしていると、1回くらいはこの例外を見る羽目になるのではないかと思います。
javax.persistence.PersistenceException: Query threw SQLException:ERROR: デッドロックを検出しました
そう、デッドロックです。
完全に回避は難しいので、デッドロックが発生したら一定回数のリトライをしたいところです。
今回は、既にそれなりに開発が進んでしまった後だったこともあり、
リトライを行うヘルパークラスを作って、そこにトランザクション管理を行っている層の関数を渡すという形で対応しました。
リトライを行うヘルパークラスを作る
DeadLockRetryHelper.java
import java.sql.SQLException;
import java.util.concurrent.Callable;
import javax.persistence.PersistenceException;
/**
* デッドロックを検知してリトライします。
*/
public class DeadLockRetryHelper {
private static final int DEFAULT_RETRT_MAX = 3;
private static final int DEFAULT_DELAY = 1;
private static Exception lastException = null;
public static <T> T retry(Callable<T> callable) throws Exception {
return retry(callable, DEFAULT_RETRT_MAX, DEFAULT_DELAY);
}
public static <T> T retry(Callable<T> callable, int maxRetryCount, int delaySeconds) throws Exception {
int count = 0;
while (count <= maxRetryCount)
{
try
{
return callable.call();
}
catch (Exception e)
{
if (isRetryable(e))
{
try
{
Thread.sleep(1000);
} catch (InterruptedException ie) {}
count++;
lastException = e;
}
else
{
throw e;
}
}
}
// リトライ限界を超えた場合
throw lastException;
}
/**
* リトライ対象の例外か判別して返します。
*/
private static boolean isRetryable(Exception e) {
Throwable cause = e;
final int CASE_LOOP_MAX = 3;
int loopCount = 0;
do
{
if(CASE_LOOP_MAX < loopCount)
{
break;
}
cause = cause.getCause();
if (cause instanceof SQLException && (
"40001".equals(((SQLException)cause).getSQLState())
|| "40P01".equals(((SQLException)cause).getSQLState())))
{
return true;
}
loopCount ++;
} while(cause != null);
return false;
}
}
ヘルパークラスにトランザクション管理をしている層の関数を渡して実行する
SampleController.java
DeadLockRetryHelper.retry( () -> XXXXController.updateUser(param1, param2);