Azure SQL Database のリトライ処理を Doma で書いてみる
Microsoft Azure SQL Database*1 では、リトライ処理が必須と言われています。
[SQL Database] アプリケーション作成における推奨事項について (Microsoft Azure SQL Database) - Microsoft SQL Server Japan Support Team Blog - Site Home - MSDN Blogs
.NET の場合は、
Retry Logic for Transient Failures in Windows Azure SQL Database - TechNet Articles - United States (English) - TechNet Wiki
The Design of the Transient Fault Handling Application Block
Entity Framework 6 と SQL Azure をうまく使う、たったひとつの冴えたやりかた - しばやん雑記
等の情報がありますが、Java の場合はあまり見かけません。
JDBC to SQL Azure Best Practices?
今回は Doma を使って試してみました。
リトライの方法として2個試してみました。
- DataSource#getConnection をリトライする
- 各 Dao のメソッド呼び出しでリトライする
DataSource#getConnection をリトライする
このパターンはお手軽な感じ。
DataSource を wrap して getConnection だけカスタムする感じ。
package sqldatabase_doma_retry.retryconnection; import java.io.PrintWriter; import java.sql.Connection; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import javax.sql.DataSource; public class RetryConnectionDataSource implements DataSource { private final DataSource datasource; public static final int DEFAULT_MAX = 3; public static final int DEFAULT_DELAY = 10; private final int max; private final int delay; private int count; public int getRetryCount() { return count; } public RetryConnectionDataSource(DataSource dataSource) { this(dataSource, DEFAULT_MAX, DEFAULT_DELAY); } public RetryConnectionDataSource(DataSource datasource, int max, int delay) { this.datasource = datasource; this.max = max; this.delay = delay; } /** * @return * @throws SQLException * @see javax.sql.DataSource#getConnection() */ public Connection getConnection() throws SQLException { count = 0; while (true) { try { System.out.println(count + "回目-"); return datasource.getConnection(); } catch (SQLException e) { count++; if (count > max) { System.out.println("リトライ回数超えた-"); throw e; } try { TimeUnit.SECONDS.sleep(delay); } catch (InterruptedException ex) { ex.printStackTrace(); } } } } // 他のメソッドはそのまま datasource に委譲... }
これを Config クラスで利用するだけです。
package sqldatabase_doma_retry.retryconnection; import javax.sql.DataSource; import org.seasar.doma.jdbc.Config; import org.seasar.doma.jdbc.dialect.Dialect; import org.seasar.doma.jdbc.dialect.MssqlDialect; import org.seasar.doma.jdbc.tx.LocalTransactionDataSource; import org.seasar.doma.jdbc.tx.LocalTransactionManager; import org.seasar.doma.jdbc.tx.TransactionManager; import com.microsoft.sqlserver.jdbc.SQLServerDataSource; public class SqlConfig implements Config { private final RetryConnectionDataSource retryDS; private final LocalTransactionDataSource datasource; private final TransactionManager manager; private static SQLServerDataSource DEFAULT_DATASOURCE; static { SQLServerDataSource tmp = new SQLServerDataSource(); tmp.setUser("user"); tmp.setPassword("pass"); tmp.setURL("jdbc:sqlserver://<your server>.database.windows.net:1433;database=<your database>;ssl=require"); tmp.setLoginTimeout(30); DEFAULT_DATASOURCE = tmp; } public SqlConfig() { this(DEFAULT_DATASOURCE); } public SqlConfig(SQLServerDataSource ds) { retryDS = new RetryConnectionDataSource(ds); datasource = new LocalTransactionDataSource(retryDS); manager = new LocalTransactionManager( datasource.getLocalTransaction(getJdbcLogger())); } @Override public DataSource getDataSource() { return datasource; } @Override public TransactionManager getTransactionManager() { return manager; } @Override public Dialect getDialect() { return new MssqlDialect(); } public int getRetryCount() { return retryDS.getRetryCount(); } }
これだけで他は何も変わりません。
各 Dao のメソッド呼び出しで リトライする
こっちのパターンは、それぞれの呼び出し元でリトライ処理を経由しないといけません。
こんなリトライ用のヘルパーを用意して
package sqldatabase_doma_retry.retrydao; import java.sql.SQLException; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import org.seasar.doma.jdbc.JdbcException; import org.seasar.doma.message.Message; public class RetryHelper { private static final int DEFAULT_MAX = 3; private static final int DEFAULT_DELAY = 10; // SQL Server デッドロックのエラーコード private static final int ERROR_CODE_DEADLOCK = 1205; public static <T> T retry(Supplier<T> supplier) { return retry(supplier, DEFAULT_MAX, DEFAULT_DELAY); } public static <T> T retry(Supplier<T> supplier, int maxCount, int delay) { int count = 0; JdbcException lastEx = null; while (count <= maxCount) { try { System.out.println(count + "回目ー"); return supplier.get(); } catch (JdbcException ex) { if (isRetryable(ex)) { try { TimeUnit.SECONDS.sleep(delay); } catch (InterruptedException e) { e.printStackTrace(); } count++; lastEx = ex; continue; } throw ex; } } throw new RuntimeException(String.valueOf(count), lastEx); } private static boolean isRetryable(JdbcException ex) { if (ex.getMessageResource() != null && Message.DOMA2015.getCode().equals(ex.getMessageResource().getCode())) { return true; } if (ex.getCause() instanceof SQLException && ((SQLException) ex.getCause()).getErrorCode() == ERROR_CODE_DEADLOCK) { return true; } return false; } }
各 Dao のメソッド呼び出しではこんな感じになります。
SqlConfig config = new SqlConfig(); Test1Dao dao = new Test1DaoImpl(config); config.getTransactionManager().required(() -> { RetryHelper.retry(() -> dao.insert(new Test1(1, "aa"))); });
ちょっと面倒な感じですが、リトライするエラーの種類をカスタム出来ます。
また Interceptor 等が使える環境なら、そっちでリトライ処理を書いてしまえば、呼び出し側のコードは特に何も変わらずで良さそうです。
SQL Database + JDBC Driver でのリトライサンプルがあれば教えて下さい~。
コード全部はこちら
sample/sqldatabase_doma_retry at master · OdaShinsuke/sample · GitHub