com.rapleaf.jack.transaction.TransactorImpl Maven / Gradle / Ivy
package com.rapleaf.jack.transaction;
import java.time.Duration;
import java.util.concurrent.Callable;
import com.google.common.base.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.rapleaf.jack.IDb;
import com.rapleaf.jack.exception.SqlExecutionFailureException;
/**
* If there is any exception while executing the query, throws
* {@link com.rapleaf.jack.exception.SqlExecutionFailureException}.
*
* If there is no available connections, throws {@link com.rapleaf.jack.exception.NoAvailableConnectionException}.
* Users can either increase the max total connections or max wait time.
*
* If the DB manager has already been closed, throws {@link IllegalStateException}.
*
* If new DB connections cannot be created, throws
* {@link com.rapleaf.jack.exception.ConnectionCreationFailureException}.
*/
public class TransactorImpl implements ITransactor {
private static final Logger LOG = LoggerFactory.getLogger(TransactorImpl.class);
private final IDbManager dbManager;
private static final int QUERY_LOG_SIZE = 30;
private TransactorMetricsImpl queryMetrics = new TransactorMetricsImpl(QUERY_LOG_SIZE);
private boolean metricsTrackingEnabled = DbPoolManager.DEFAULT_METRICS_TRACKING_ENABLED;
TransactorImpl(IDbManager dbManager, boolean metricsTrackingEnabled) {
this.dbManager = dbManager;
this.metricsTrackingEnabled = metricsTrackingEnabled;
}
public static Builder create(Callable dbConstructor) {
return new Builder<>(dbConstructor);
}
ITransactor newContext() {
return new ExecutionContext();
}
@Override
public ITransactor asTransaction() {
return newContext().asTransaction();
}
@Override
public ITransactor allowRetries(RetryPolicy retryPolicy) {
return newContext().allowRetries(retryPolicy);
}
@Override
public T query(IQuery query) {
return newContext().query(query);
}
@Deprecated
@Override
public T queryAsTransaction(IQuery query) {
return asTransaction().query(query);
}
@Override
public void execute(IExecution execution) {
newContext().execute(execution);
}
@Deprecated
@Override
public void executeAsTransaction(IExecution execution) {
asTransaction().execute(execution);
}
TransactorMetrics getQueryMetrics() {
if (!metricsTrackingEnabled) {
return new MockTransactorMetrics();
} else {
return queryMetrics;
}
}
DbMetrics getDbMetrics() {
return dbManager.getMetrics();
}
private T query(IQuery query, ExecutionContext context) {
while (true) {
DB connection = dbManager.getConnection();
boolean connectionSafeToReturn = true;
try {
connection.setAutoCommit(!context.asTransaction);
long startTime = System.currentTimeMillis();
T value = query.query(connection);
if (context.asTransaction) {
connection.commit();
}
long executionTime = System.currentTimeMillis() - startTime;
context.retryPolicy.onSuccess();
if (metricsTrackingEnabled) {
queryMetrics.update(executionTime, Thread.currentThread().getStackTrace()[3]);
}
return value;
} catch (Exception e) {
LOG.error("SQL execution failure", e);
if (context.asTransaction) {
connectionSafeToReturn = tryToSafelyRollback(connection);
}
context.retryPolicy.onFailure(e);
} catch (Throwable t) {
// We still try to explicitly rollback the transaction if a throwable is thrown.
if (context.asTransaction) {
connectionSafeToReturn = tryToSafelyRollback(connection);
}
throw t;
} finally {
if (connectionSafeToReturn) {
dbManager.returnConnection(connection);
} else {
dbManager.invalidateConnection(connection);
}
/*
* {@link RetryPolicy.execute} is very likely to block (sleep or perform a long running task);
* We make sure it is called only after the connection has been invalidated/returned to the pool.
*/
context.retryPolicy.execute();
}
}
}
/**
* @param connection The connection to rollback
* @return True if the connection was successfully rolled back. False if it failed to rollback.
*/
private boolean tryToSafelyRollback(DB connection) {
try {
connection.rollback();
} catch (Exception e) {
LOG.warn("Failed to rollback an active transaction", e);
return false;
}
return true;
}
@Override
public void close() {
if (metricsTrackingEnabled) {
LOG.info("{}\n\n{}", dbManager.getMetrics().getSummary(), getQueryMetrics().getSummary());
}
dbManager.close();
}
@Override
public DbPoolStatus getDbPoolStatus() {
return this.dbManager.getDbPoolStatus();
}
private class ExecutionContext implements ITransactor {
boolean asTransaction = false;
RetryPolicy retryPolicy = new NoRetryPolicy();
@Override
public ITransactor asTransaction() {
asTransaction = true;
return this;
}
@Override
public ITransactor allowRetries(RetryPolicy retryPolicy) {
Preconditions.checkArgument(retryPolicy != null);
this.retryPolicy = retryPolicy;
return this;
}
@Override
public T query(IQuery query) {
return TransactorImpl.this.query(query, this);
}
@Override
public void execute(IExecution execution) {
query(db -> {
execution.execute(db);
return null;
});
}
@Deprecated
@Override
public T queryAsTransaction(IQuery query) {
return asTransaction().query(query);
}
@Deprecated
@Override
public void executeAsTransaction(IExecution execution) {
asTransaction().execute(execution);
}
@Override
public void close() {
TransactorImpl.this.close();
}
@Override
public DbPoolStatus getDbPoolStatus() {
return TransactorImpl.this.getDbPoolStatus();
}
}
public static class NoRetryPolicy implements ITransactor.RetryPolicy {
@Override
public void onFailure(Exception cause) {
throw new SqlExecutionFailureException(cause);
}
@Override
public void onSuccess() {
}
@Override
public boolean execute() {
return false;
}
}
public static class Builder implements ITransactor.Builder> {
final Callable dbConstructor;
int maxTotalConnections = DbPoolManager.DEFAULT_MAX_TOTAL_CONNECTIONS;
int minIdleConnections = DbPoolManager.DEFAULT_MIN_IDLE_CONNECTIONS;
long maxWaitMillis = DbPoolManager.DEFAULT_MAX_WAIT_TIME;
long keepAliveMillis = DbPoolManager.DEFAULT_KEEP_ALIVE_TIME;
boolean metricsTrackingEnabled = DbPoolManager.DEFAULT_METRICS_TRACKING_ENABLED;
Builder(Callable dbConstructor) {
this.dbConstructor = dbConstructor;
}
/**
* @param maxTotalConnections The maximum number of connections that can be created in the pool. Negative values
* mean infinite number of connections are allowed.
* @throws IllegalArgumentException if the value is zero.
*/
public Builder setMaxTotalConnections(int maxTotalConnections) {
Preconditions.checkArgument(maxTotalConnections != 0, "Max total connections cannot be zero");
this.maxTotalConnections = maxTotalConnections;
return this;
}
/**
* Allow infinite number of connections.
*/
public Builder enableInfiniteConnections() {
this.maxTotalConnections = -1;
return this;
}
/**
* @param minIdleConnections The minimum number of idle connections to keep in the pool. Zero or negative values
* will be ignored.
*/
public Builder setMinIdleConnections(int minIdleConnections) {
this.minIdleConnections = minIdleConnections;
return this;
}
/**
* @param maxWaitTime The maximum amount of time that the {@link DbPoolManager#getConnection} method
* should block before throwing an exception when the pool is exhausted. Negative values
* mean that the block can be infinite.
*/
public Builder setMaxWaitTime(Duration maxWaitTime) {
this.maxWaitMillis = maxWaitTime.toMillis();
return this;
}
/**
* Allow infinite block for the {@link DbPoolManager#getConnection} method.
*/
public Builder enableInfiniteWait() {
this.maxWaitMillis = -1L;
return this;
}
/***
* @param keepAliveTime The minimum amount of time the connection may sit idle in the pool before it is
* eligible for eviction (with the extra condition that at least {@code minIdleConnections}
* connections remain in the pool).
*/
public Builder setKeepAliveTime(Duration keepAliveTime) {
this.keepAliveMillis = keepAliveTime.toMillis();
return this;
}
public Builder setMetricsTracking(boolean metricsTrackingEnabled) {
this.metricsTrackingEnabled = metricsTrackingEnabled;
return this;
}
@Override
public TransactorImpl get() {
return Builder.build(this);
}
private static TransactorImpl build(Builder builder) {
DbPoolManager dbPoolManager = new DbPoolManager<>(builder.dbConstructor, builder.maxTotalConnections,
builder.minIdleConnections, builder.maxWaitMillis, builder.keepAliveMillis, builder.metricsTrackingEnabled);
return new TransactorImpl<>(dbPoolManager, builder.metricsTrackingEnabled);
}
}
}