2017-07-01 02:07:04 -04:00
|
|
|
package com.moparisthebest.jdbc;
|
|
|
|
|
|
|
|
import com.moparisthebest.jdbc.codegen.JdbcMapper;
|
|
|
|
|
|
|
|
import java.sql.SQLException;
|
2017-07-02 01:35:59 -04:00
|
|
|
import java.util.concurrent.*;
|
2017-07-01 02:07:04 -04:00
|
|
|
|
|
|
|
import static com.moparisthebest.jdbc.TryClose.tryClose;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Created by mopar on 6/30/17.
|
|
|
|
*/
|
|
|
|
public class QueryRunner<T extends JdbcMapper> {
|
|
|
|
|
2017-07-02 01:35:59 -04:00
|
|
|
private static final int defaultRetryCount = 10;
|
2017-07-03 00:00:03 -04:00
|
|
|
private static final DelayStrategy defaultDelayStrategy = exponentialBackoff(1000, 30000, 2000, 10);
|
2017-07-02 01:35:59 -04:00
|
|
|
private static final ExecutorService defaultExecutorService = Executors.newCachedThreadPool(); // todo: good or bad default?
|
|
|
|
|
2017-07-01 02:07:04 -04:00
|
|
|
private final Factory<T> factory;
|
2017-07-02 01:35:59 -04:00
|
|
|
private final DelayStrategy delayStrategy;
|
|
|
|
private final int retryCount;
|
|
|
|
private final ExecutorService executorService;
|
2017-07-01 02:07:04 -04:00
|
|
|
|
2017-07-02 01:35:59 -04:00
|
|
|
public QueryRunner(final Factory<T> factory, final DelayStrategy delayStrategy, final ExecutorService executorService, final int retryCount) {
|
|
|
|
if (factory == null)
|
2017-07-01 02:07:04 -04:00
|
|
|
throw new NullPointerException("factory must be non-null");
|
2017-07-02 01:35:59 -04:00
|
|
|
if (delayStrategy == null)
|
|
|
|
throw new NullPointerException("delayStrategy must be non-null");
|
|
|
|
if (executorService == null)
|
|
|
|
throw new NullPointerException("executorService must be non-null");
|
|
|
|
if (retryCount < 1)
|
|
|
|
throw new IllegalArgumentException("retryCount must be > 0");
|
2017-07-01 02:07:04 -04:00
|
|
|
this.factory = factory;
|
2017-07-02 01:35:59 -04:00
|
|
|
this.delayStrategy = delayStrategy;
|
|
|
|
this.retryCount = retryCount;
|
|
|
|
this.executorService = Executors.newSingleThreadExecutor();
|
|
|
|
}
|
|
|
|
|
|
|
|
public QueryRunner(final Factory<T> factory) {
|
|
|
|
this(factory, defaultDelayStrategy, defaultExecutorService, defaultRetryCount);
|
|
|
|
}
|
|
|
|
|
|
|
|
public QueryRunner(final Factory<T> factory, final DelayStrategy delayStrategy) {
|
|
|
|
this(factory, delayStrategy, defaultExecutorService, defaultRetryCount);
|
|
|
|
}
|
|
|
|
|
|
|
|
public QueryRunner(final Factory<T> factory, final int retryCount) {
|
|
|
|
this(factory, defaultDelayStrategy, defaultExecutorService, retryCount);
|
|
|
|
}
|
|
|
|
|
|
|
|
public QueryRunner(final Factory<T> factory, final ExecutorService executorService) {
|
|
|
|
this(factory, defaultDelayStrategy, executorService, defaultRetryCount);
|
|
|
|
}
|
|
|
|
|
|
|
|
public QueryRunner(final Factory<T> factory, final DelayStrategy delayStrategy, final ExecutorService executorService) {
|
|
|
|
this(factory, delayStrategy, executorService, defaultRetryCount);
|
|
|
|
}
|
|
|
|
|
|
|
|
public QueryRunner(final Factory<T> factory, final ExecutorService executorService, final int retryCount) {
|
|
|
|
this(factory, defaultDelayStrategy, executorService, retryCount);
|
2017-07-01 02:07:04 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
public <E> E run(final Runner<T, E> query) throws SQLException {
|
2017-07-02 01:35:59 -04:00
|
|
|
if (query == null)
|
2017-07-01 02:07:04 -04:00
|
|
|
throw new NullPointerException("query must be non-null");
|
|
|
|
T dao = null;
|
|
|
|
try {
|
|
|
|
dao = factory.create();
|
|
|
|
return query.run(dao);
|
|
|
|
} finally {
|
|
|
|
tryClose(dao);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public <E> E runInTransaction(final Runner<T, E> query) throws SQLException {
|
2017-07-02 01:35:59 -04:00
|
|
|
if (query == null)
|
2017-07-01 02:07:04 -04:00
|
|
|
throw new NullPointerException("query must be non-null");
|
|
|
|
T dao = null;
|
|
|
|
try {
|
|
|
|
dao = factory.create();
|
|
|
|
dao.getConnection().setAutoCommit(false);
|
|
|
|
final E ret = query.run(dao);
|
|
|
|
dao.getConnection().commit();
|
|
|
|
return ret;
|
|
|
|
} catch (final Throwable e) {
|
|
|
|
if (dao != null) {
|
|
|
|
try {
|
|
|
|
dao.getConnection().rollback();
|
2017-07-02 01:35:59 -04:00
|
|
|
} catch (SQLException excep) {
|
2017-07-01 02:07:04 -04:00
|
|
|
// ignore to throw original
|
|
|
|
}
|
|
|
|
}
|
2017-07-02 01:35:59 -04:00
|
|
|
if (e instanceof SQLException)
|
2017-07-01 02:07:04 -04:00
|
|
|
throw (SQLException) e;
|
2017-07-02 01:35:59 -04:00
|
|
|
if (e instanceof RuntimeException)
|
2017-07-01 02:07:04 -04:00
|
|
|
throw (RuntimeException) e;
|
|
|
|
throw new RuntimeException("odd error should never happen", e);
|
|
|
|
} finally {
|
|
|
|
if (dao != null) {
|
|
|
|
try {
|
|
|
|
dao.getConnection().setAutoCommit(true);
|
2017-07-02 01:35:59 -04:00
|
|
|
} catch (SQLException excep) {
|
2017-07-01 02:07:04 -04:00
|
|
|
// ignore
|
|
|
|
}
|
|
|
|
tryClose(dao);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-02 01:35:59 -04:00
|
|
|
public <E> E runRetry(final Runner<T, E> query) throws SQLException {
|
|
|
|
SQLException lastException = null;
|
|
|
|
int x = 0;
|
|
|
|
do {
|
|
|
|
try {
|
|
|
|
return runInTransaction(query);
|
|
|
|
} catch (SQLException e) {
|
|
|
|
lastException = e;
|
|
|
|
try {
|
2017-07-03 00:00:03 -04:00
|
|
|
Thread.sleep(delayStrategy.getDelay(++x));
|
2017-07-02 01:35:59 -04:00
|
|
|
} catch (InterruptedException e2) {
|
|
|
|
Thread.interrupted();
|
|
|
|
}
|
|
|
|
}
|
2017-07-03 00:00:03 -04:00
|
|
|
} while (x <= retryCount);
|
2017-07-02 01:35:59 -04:00
|
|
|
throw lastException;
|
|
|
|
}
|
|
|
|
|
|
|
|
public <E> Future<E> runRetryFuture(final Runner<T, E> query) {
|
|
|
|
// todo: sleeps in thread, could use ScheduledExecutorService maybe?
|
2017-07-03 00:00:03 -04:00
|
|
|
return executorService.submit(
|
|
|
|
//IFJAVA8_START
|
|
|
|
() -> runRetry(query)
|
|
|
|
//IFJAVA8_END
|
|
|
|
/*IFJAVA6_START
|
|
|
|
new Callable<E>() {
|
|
|
|
@Override
|
|
|
|
public E call() throws Exception {
|
|
|
|
return runRetry(query);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
IFJAVA6_END*/
|
|
|
|
);
|
2017-07-02 01:35:59 -04:00
|
|
|
}
|
|
|
|
|
2017-07-02 23:02:16 -04:00
|
|
|
//IFJAVA8_START
|
|
|
|
|
|
|
|
public <E> CompletableFuture<E> runRetryCompletableFuture(final Runner<T, E> query) {
|
|
|
|
// todo: sleeps in thread, could use ScheduledExecutorService maybe?
|
|
|
|
return CompletableFuture.supplyAsync(() -> {
|
|
|
|
try {
|
|
|
|
return runRetry(query);
|
|
|
|
} catch (SQLException e) {
|
|
|
|
throw new RuntimeException(e);
|
|
|
|
}
|
|
|
|
}, executorService);
|
|
|
|
}
|
|
|
|
|
|
|
|
//IFJAVA8_END
|
|
|
|
|
2017-07-01 02:07:04 -04:00
|
|
|
public static interface Runner<T, E> {
|
|
|
|
E run(T dao) throws SQLException;
|
|
|
|
}
|
2017-07-02 01:35:59 -04:00
|
|
|
|
|
|
|
public static interface DelayStrategy {
|
|
|
|
long getDelay(int attempt);
|
2017-07-03 01:20:06 -04:00
|
|
|
|
|
|
|
//IFJAVA8_START
|
|
|
|
default DelayStrategy withJitter(final int maxJitterMs) {
|
|
|
|
return QueryRunner.withJitter(this, maxJitterMs);
|
|
|
|
}
|
|
|
|
//IFJAVA8_END
|
2017-07-02 01:35:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
public static DelayStrategy exponentialBackoff() {
|
|
|
|
return defaultDelayStrategy;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Implements truncated binary exponential backoff to calculate retry delay per
|
|
|
|
* IEEE 802.3-2008 Section 1. There will be at most ten (10) contention periods
|
|
|
|
* of backoff, each contention period whose amount is equal to the delta backoff.
|
|
|
|
*
|
|
|
|
* @author Robert Buck (buck.robert.j@gmail.com)
|
|
|
|
*/
|
2017-07-03 00:00:03 -04:00
|
|
|
public static DelayStrategy exponentialBackoff(final long minBackoff, final long maxBackoff, final long slotTime, final long maxContentionPeriods) {
|
|
|
|
return
|
|
|
|
/*IFJAVA6_START
|
|
|
|
new DelayStrategy() {
|
2017-07-02 23:02:16 -04:00
|
|
|
@Override
|
|
|
|
public long getDelay(final int attempt) {
|
2017-07-03 00:00:03 -04:00
|
|
|
return
|
|
|
|
IFJAVA6_END*/
|
|
|
|
//IFJAVA8_START
|
|
|
|
(attempt) ->
|
|
|
|
//IFJAVA8_END
|
|
|
|
attempt == 0 ? 0 : Math.min(minBackoff + ThreadLocalRandom.current().nextInt(2 << Math.min(attempt, maxContentionPeriods - 1)) * slotTime, maxBackoff);
|
|
|
|
/*IFJAVA6_START
|
2017-07-02 23:02:16 -04:00
|
|
|
}
|
|
|
|
};
|
2017-07-03 00:00:03 -04:00
|
|
|
IFJAVA6_END*/
|
2017-07-02 01:35:59 -04:00
|
|
|
}
|
|
|
|
|
2017-07-02 23:02:16 -04:00
|
|
|
public static DelayStrategy fixedDelay(final long delay) {
|
2017-07-03 00:00:03 -04:00
|
|
|
return
|
|
|
|
/*IFJAVA6_START
|
|
|
|
new DelayStrategy() {
|
2017-07-02 23:02:16 -04:00
|
|
|
@Override
|
|
|
|
public long getDelay(final int attempt) {
|
2017-07-03 00:00:03 -04:00
|
|
|
return
|
|
|
|
IFJAVA6_END*/
|
|
|
|
//IFJAVA8_START
|
|
|
|
(attempt) ->
|
|
|
|
//IFJAVA8_END
|
|
|
|
delay;
|
|
|
|
/*IFJAVA6_START
|
2017-07-02 23:02:16 -04:00
|
|
|
}
|
|
|
|
};
|
2017-07-03 00:00:03 -04:00
|
|
|
IFJAVA6_END*/
|
2017-07-02 01:35:59 -04:00
|
|
|
}
|
|
|
|
|
2017-07-02 23:02:16 -04:00
|
|
|
public static DelayStrategy incrementalDelay(final long initialInterval, final long incrementalInterval) {
|
2017-07-03 00:00:03 -04:00
|
|
|
return
|
|
|
|
/*IFJAVA6_START
|
|
|
|
new DelayStrategy() {
|
2017-07-02 23:02:16 -04:00
|
|
|
@Override
|
|
|
|
public long getDelay(final int attempt) {
|
2017-07-03 00:00:03 -04:00
|
|
|
return
|
|
|
|
IFJAVA6_END*/
|
|
|
|
//IFJAVA8_START
|
|
|
|
(attempt) ->
|
|
|
|
//IFJAVA8_END
|
|
|
|
initialInterval + incrementalInterval * attempt;
|
|
|
|
/*IFJAVA6_START
|
|
|
|
}
|
|
|
|
};
|
|
|
|
IFJAVA6_END*/
|
|
|
|
}
|
|
|
|
|
2017-07-03 01:20:06 -04:00
|
|
|
public static DelayStrategy withJitter(final DelayStrategy toWrap, final int maxJitterMs) {
|
|
|
|
return
|
|
|
|
/*IFJAVA6_START
|
|
|
|
new DelayStrategy() {
|
|
|
|
@Override
|
|
|
|
public long getDelay(final int attempt) {
|
|
|
|
return
|
|
|
|
IFJAVA6_END*/
|
|
|
|
//IFJAVA8_START
|
|
|
|
(attempt) ->
|
|
|
|
//IFJAVA8_END
|
|
|
|
toWrap.getDelay(attempt) + ThreadLocalRandom.current().nextInt(maxJitterMs);
|
|
|
|
/*IFJAVA6_START
|
|
|
|
}
|
|
|
|
};
|
|
|
|
IFJAVA6_END*/
|
|
|
|
}
|
|
|
|
|
2017-07-03 00:00:03 -04:00
|
|
|
/*IFJAVA6_START
|
|
|
|
// terrible, I know, use java8
|
|
|
|
private static class ThreadLocalRandom {
|
|
|
|
private static final ThreadLocal<java.util.Random> randomThreadLocal = new ThreadLocal<java.util.Random>() {
|
|
|
|
@Override
|
|
|
|
protected java.util.Random initialValue() {
|
|
|
|
return new java.util.Random();
|
2017-07-02 23:02:16 -04:00
|
|
|
}
|
|
|
|
};
|
2017-07-03 00:00:03 -04:00
|
|
|
|
|
|
|
private static java.util.Random current() {
|
|
|
|
return randomThreadLocal.get();
|
|
|
|
}
|
2017-07-02 01:35:59 -04:00
|
|
|
}
|
2017-07-03 00:00:03 -04:00
|
|
|
IFJAVA6_END*/
|
|
|
|
|
2017-07-01 02:07:04 -04:00
|
|
|
}
|