#1232 - Performance: Add support for a second read-only DataSource (for implicit query-only transactions)

This commit is contained in:
Rob Bygrave committed 2018-01-09 16:35:22 +13:00
1 parent 01539091ef
commit 932986a6b5
27 files changed
+1128 -100

No files matched your search

@@ -1,7 +1,6 @@
package io.ebeaninternal.server.transaction;
import java.sql.Connection;
import java.sql.SQLException;
/**
* AutoCommit friendly Transaction.
@@ -15,17 +14,17 @@ public class AutoCommitJdbcTransaction extends JdbcTransaction {
}
@Override
protected void checkAutoCommit(Connection connection) throws SQLException {
protected void checkAutoCommit(Connection connection) {
// do nothing as autoCommit
}
@Override
protected void performRollback() throws SQLException {
protected void performRollback() {
// do nothing as autoCommit
}
@Override
protected void performCommit() throws SQLException {
protected void performCommit() {
// do nothing as autoCommit
}
@@ -20,6 +20,15 @@ public interface DataSourceSupplier {
*/
DataSource getDataSource();
/**
* Return the read only DataSource to use for the current request.
* <p>
* This can return null meaning that no read only DataSource (with autoCommit)
* is available for use so normal transactions with explicit commit should be used.
* </p>
*/
DataSource getReadOnlyDataSource();
/**
* Return a connection from the DataSource taking into account a tenantId for multi-tenant lazy loading.
*
@@ -28,6 +37,14 @@ public interface DataSourceSupplier {
*/
Connection getConnection(Object tenantId) throws SQLException;
/**
* Return a connection from the read only DataSource taking into account a tenantId for multi-tenant lazy loading.
*
* @param tenantId Most often null but well supplied indicates a multi-tenant lazy loading query
* @return the connection to use
*/
Connection getReadOnlyConnection(Object tenantId) throws SQLException;
/**
* Shutdown the datasource de-registering the JDBC driver if requested.
*/
@@ -0,0 +1,591 @@
package io.ebeaninternal.server.transaction;
import io.ebean.TransactionCallback;
import io.ebean.annotation.DocStoreMode;
import io.ebean.annotation.PersistBatch;
import io.ebean.bean.PersistenceContext;
import io.ebean.event.changelog.BeanChange;
import io.ebean.event.changelog.ChangeSet;
import io.ebeaninternal.api.SpiProfileTransactionEvent;
import io.ebeaninternal.api.SpiTransaction;
import io.ebeaninternal.api.TransactionEvent;
import io.ebeaninternal.api.TxnProfileEventCodes;
import io.ebeaninternal.server.core.PersistDeferredRelationship;
import io.ebeaninternal.server.core.PersistRequest;
import io.ebeaninternal.server.core.PersistRequestBean;
import io.ebeaninternal.server.persist.BatchControl;
import io.ebeanservice.docstore.api.DocStoreTransaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.PersistenceException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
/**
* Read only transaction expected to use autoCommit connection and for implicit use only.
* <p>
* This transaction is created implicitly and not expected to be exposed to application code and has
* none of the features for supporting inserts, updates and deletes etc (and throws errors if those
* persisting features are attempted to be used - which is not expected).
* </p>
*/
class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEventCodes {
private static final Logger logger = LoggerFactory.getLogger(ImplicitReadOnlyTransaction.class);
private static final String illegalStateMessage = "Transaction is Inactive";
private static final String notExpectedMessage = "Not expected on read only transaction";
/**
* The status of the transaction.
*/
protected boolean active;
/**
* The underlying Connection which is expected to use autoCommit such that we avoid the
* explicit commit call at the end of the 'transaction' (for performance).
*/
protected Connection connection;
/**
* Holder of the objects fetched to ensure unique objects are used.
*/
protected PersistenceContext persistenceContext;
private Object tenantId;
private Map<String, Object> userObjects;
/**
* Create without a tenantId.
*/
ImplicitReadOnlyTransaction(Connection connection) {
this.active = true;
this.connection = connection;
this.persistenceContext = new DefaultPersistenceContext();
}
/**
* Create with a tenantId.
*/
ImplicitReadOnlyTransaction(Connection connection, Object tenantId) {
this(connection);
this.tenantId = tenantId;
}
@Override
public long profileOffset() {
return 0;
}
@Override
public void profileEvent(SpiProfileTransactionEvent event) {
// do nothing
}
@Override
public ProfileStream profileStream() {
return null;
}
@Override
public boolean isSkipCache() {
return false;
}
@Override
public void setSkipCache(boolean skipCache) {
}
@Override
public String getLogPrefix() {
return null;
}
@Override
public void addBeanChange(BeanChange beanChange) {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public void sendChangeLog(ChangeSet changesRequest) {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public void register(TransactionCallback callback) {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public int getDocStoreBatchSize() {
return 0;
}
@Override
public void setDocStoreBatchSize(int docStoreBatchSize) {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public DocStoreMode getDocStoreMode() {
return null;
}
@Override
public void setDocStoreMode(DocStoreMode docStoreMode) {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public void registerDeferred(PersistDeferredRelationship derived) {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public void registerDeleteBean(Integer persistingBean) {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public void unregisterDeleteBean(Integer persistedBean) {
throw new IllegalStateException(notExpectedMessage);
}
/**
* Return true if this is a bean that has already been saved/deleted.
*/
@Override
public boolean isRegisteredDeleteBean(Integer persistingBean) {
return false;
}
@Override
public void unregisterBean(Object bean) {
throw new IllegalStateException(notExpectedMessage);
}
/**
* Return true if this is a bean that has already been saved. This will
* register the bean if it is not already.
*/
@Override
public boolean isRegisteredBean(Object bean) {
return false;
}
@Override
public boolean isSaveAssocManyIntersection(String intersectionTable, String beanName) {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public void depth(int diff) {
}
/**
* Return the current depth.
*/
@Override
public int depth() {
return 0;
}
@Override
public void markNotQueryOnly() {
}
@Override
public boolean isReadOnly() {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
try {
return connection.isReadOnly();
} catch (SQLException e) {
throw new PersistenceException(e);
}
}
@Override
public void setReadOnly(boolean readOnly) {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
try {
connection.setReadOnly(readOnly);
} catch (SQLException e) {
throw new PersistenceException(e);
}
}
@Override
public void setUpdateAllLoadedProperties(boolean updateAllLoadedProperties) {
}
@Override
public Boolean isUpdateAllLoadedProperties() {
return null;
}
@Override
public void setBatchMode(boolean batchMode) {
}
@Override
public void setBatch(PersistBatch batchMode) {
}
@Override
public PersistBatch getBatch() {
return null;
}
@Override
public void setBatchOnCascade(PersistBatch batchOnCascadeMode) {
}
@Override
public PersistBatch getBatchOnCascade() {
return null;
}
@Override
public Boolean getBatchGetGeneratedKeys() {
return null;
}
@Override
public void setBatchGetGeneratedKeys(boolean getGeneratedKeys) {
}
@Override
public void setBatchFlushOnMixed(boolean batchFlushOnMixed) {
}
/**
* Return the batchSize specifically set for this transaction or 0.
* <p>
* Returning 0 implies to use the system wide default batch size.
* </p>
*/
@Override
public int getBatchSize() {
return 0;
}
@Override
public void setBatchSize(int batchSize) {
}
@Override
public boolean isBatchFlushOnQuery() {
return false;
}
@Override
public void setBatchFlushOnQuery(boolean batchFlushOnQuery) {
}
/**
* Return true if this request should be batched. Returning false means that
* this request should be executed immediately.
*/
@Override
public boolean isBatchThisRequest(PersistRequest.Type type) {
return false;
}
@Override
public void checkBatchEscalationOnCollection() {
}
@Override
public void flushBatchOnCollection() {
}
@Override
public PersistenceException translate(String message, SQLException cause) {
return new PersistenceException(message, cause);
}
/**
* Flush after completing persist cascade.
*/
@Override
public void flushBatchOnCascade() {
}
@Override
public void flushBatchOnRollback() {
}
@Override
public boolean checkBatchEscalationOnCascade(PersistRequestBean<?> request) {
return false;
}
@Override
public BatchControl getBatchControl() {
return null;
}
/**
* Set the BatchControl to the transaction. This is done once per transaction
* on the first persist request.
*/
@Override
public void setBatchControl(BatchControl batchControl) {
}
/**
* Flush any queued persist requests.
* <p>
* This is general will result in a number of batched PreparedStatements
* executing.
* </p>
*/
@Override
public void flush() {
}
@Override
public void flushBatch() {
flush();
}
/**
* Return the persistence context associated with this transaction.
*/
@Override
public PersistenceContext getPersistenceContext() {
return persistenceContext;
}
/**
* Set the persistence context to this transaction.
* <p>
* This could be considered similar to EJB3 Extended PersistanceContext. In
* that you get the PersistanceContext from a transaction, hold onto it, and
* then set it back later to a second transaction.
* </p>
*/
@Override
public void setPersistenceContext(PersistenceContext context) {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
this.persistenceContext = context;
}
@Override
public TransactionEvent getEvent() {
throw new IllegalStateException(notExpectedMessage);
}
/**
* Return true if this was an explicitly created transaction.
*/
@Override
public boolean isExplicit() {
return false;
}
@Override
public boolean isLogSql() {
return TransactionManager.SQL_LOGGER.isDebugEnabled();
}
@Override
public boolean isLogSummary() {
return TransactionManager.SUM_LOGGER.isDebugEnabled();
}
@Override
public void logSql(String msg) {
TransactionManager.SQL_LOGGER.debug(msg);
}
@Override
public void logSummary(String msg) {
TransactionManager.SUM_LOGGER.debug(msg);
}
/**
* Return the transaction id.
*/
@Override
public String getId() {
return null;
}
@Override
public void setTenantId(Object tenantId) {
this.tenantId = tenantId;
}
@Override
public Object getTenantId() {
return tenantId;
}
/**
* Return the underlying connection for internal use.
*/
@Override
public Connection getInternalConnection() {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
return connection;
}
/**
* Return the underlying connection for public use.
*/
@Override
public Connection getConnection() {
return getInternalConnection();
}
private void deactivate() {
try {
connection.close();
} catch (Exception ex) {
// the connection pool will automatically remove the
// connection if it does not pass the test
logger.error("Error closing connection", ex);
}
connection = null;
active = false;
}
/**
* Perform a commit, fire callbacks and notify l2 cache etc.
* <p>
* This leaves the transaction active and expects another commit
* to occur later (which closes the underlying connection etc).
* </p>
*/
@Override
public void commitAndContinue() {
// do nothing, expect AutoCommit
}
/**
* Commit the transaction.
*/
@Override
public void commit() {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
// expect AutoCommit so just deactivate / put back into pool
deactivate();
}
/**
* Return true if the transaction is marked as rollback only.
*/
@Override
public boolean isRollbackOnly() {
return false;
}
/**
* Mark the transaction as rollback only.
*/
@Override
public void setRollbackOnly() {
// expect AutoCommit so we can't really support rollbackOnly
throw new IllegalStateException(notExpectedMessage);
}
/**
* Rollback the transaction.
*/
@Override
public void rollback() throws PersistenceException {
rollback(null);
}
/**
* Rollback the transaction. If there is a throwable it is logged as the cause
* in the transaction log.
*/
@Override
public void rollback(Throwable cause) throws PersistenceException {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
// expect AutoCommit so it really has already committed
deactivate();
}
/**
* If the transaction is active then perform rollback.
*/
@Override
public void end() throws PersistenceException {
if (isActive()) {
rollback();
}
}
/**
* Return true if the transaction is active.
*/
@Override
public boolean isActive() {
return active;
}
@Override
public boolean isPersistCascade() {
return false;
}
@Override
public void setPersistCascade(boolean persistCascade) {
}
@Override
public void addModification(String tableName, boolean inserts, boolean updates, boolean deletes) {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public DocStoreTransaction getDocStoreTransaction() {
throw new IllegalStateException(notExpectedMessage);
}
@Override
public void putUserObject(String name, Object value) {
if (userObjects == null) {
userObjects = new HashMap<>();
}
userObjects.put(name, value);
}
@Override
public Object getUserObject(String name) {
if (userObjects == null) {
return null;
}
return userObjects.get(name);
}
/**
* Alias for end(), which enables this class to be used in try-with-resources.
*/
@Override
public void close() {
end();
}
}
@@ -13,37 +13,49 @@ import java.sql.SQLException;
*/
class TransactionFactoryBasic extends TransactionFactory {
final DataSourceSupplier dataSourceSupplier;
private final DataSource dataSource;
TransactionFactoryBasic(TransactionManager manager, DataSourceSupplier dataSourceSupplier) {
super(manager);
this.dataSourceSupplier = dataSourceSupplier;
this.dataSource = dataSourceSupplier.getDataSource();
}
@Override
public SpiTransaction createQueryTransaction(Object tenantId) {
return create(0,false);
}
@Override
public SpiTransaction createTransaction(int profileId, boolean explicit, int isolationLevel) {
SpiTransaction t = create(profileId, explicit);
return setIsolationLevel(t, explicit, isolationLevel);
}
private SpiTransaction create(int profileId, boolean explicit) {
Connection c = null;
Connection connection = null;
try {
c = dataSource.getConnection();
return manager.createTransaction(profileId, explicit, c, counter.incrementAndGet());
connection = dataSource.getConnection();
return create(0, false, connection);
} catch (PersistenceException ex) {
JdbcClose.close(c);
JdbcClose.close(connection);
throw ex;
} catch (SQLException ex) {
throw new PersistenceException(ex);
}
}
@Override
public SpiTransaction createTransaction(int profileId, boolean explicit, int isolationLevel) {
Connection connection = null;
try {
connection = dataSource.getConnection();
SpiTransaction t = create(profileId, explicit, connection);
return setIsolationLevel(t, explicit, isolationLevel);
} catch (PersistenceException ex) {
JdbcClose.close(connection);
throw ex;
} catch (SQLException ex) {
throw new PersistenceException(ex);
}
}
private SpiTransaction create(int profileId, boolean explicit, Connection c) {
return manager.createTransaction(profileId, explicit, c, counter.incrementAndGet());
}
}
@@ -0,0 +1,45 @@
package io.ebeaninternal.server.transaction;
import io.ebeaninternal.api.SpiTransaction;
import io.ebeaninternal.util.JdbcClose;
import javax.persistence.PersistenceException;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* Aware of read only autoCommit based DataSource.
* <p>
* This means for implicit query only transactions we can:
* - Use the read only DataSource
* - Skip explicit commit (as we use AutoCommit instead)
* </p>
*/
class TransactionFactoryBasicWithRead extends TransactionFactoryBasic {
private final DataSource readOnlyDataSource;
TransactionFactoryBasicWithRead(TransactionManager manager, DataSourceSupplier dataSourceSupplier) {
super(manager, dataSourceSupplier);
this.readOnlyDataSource = dataSourceSupplier.getReadOnlyDataSource();
}
@Override
public SpiTransaction createQueryTransaction(Object tenantId) {
Connection connection = null;
try {
connection = readOnlyDataSource.getConnection();
return new ImplicitReadOnlyTransaction(connection);
} catch (PersistenceException ex) {
JdbcClose.close(connection);
throw ex;
} catch (SQLException ex) {
throw new PersistenceException(ex);
}
}
}
@@ -0,0 +1,30 @@
package io.ebeaninternal.server.transaction;
import io.ebean.config.CurrentTenantProvider;
/**
* Helper to build and return the appropriate TransactionFactory.
*/
class TransactionFactoryBuilder {
/**
* Build and return based on multi-tenancy and read only DataSource.
*/
static TransactionFactory build(TransactionManager manager, DataSourceSupplier dataSourceSupplier, CurrentTenantProvider tenantProvider) {
boolean hasReadOnlyDataSource = dataSourceSupplier.getReadOnlyDataSource() != null;
if (tenantProvider == null) {
if (hasReadOnlyDataSource) {
return new TransactionFactoryBasicWithRead(manager, dataSourceSupplier);
} else {
return new TransactionFactoryBasic(manager, dataSourceSupplier);
}
} else {
if (hasReadOnlyDataSource) {
return new TransactionFactoryTenantWithRead(manager, dataSourceSupplier, tenantProvider);
} else {
return new TransactionFactoryTenant(manager, dataSourceSupplier, tenantProvider);
}
}
}
}
@@ -13,9 +13,9 @@ import java.sql.SQLException;
*/
class TransactionFactoryTenant extends TransactionFactory {
private final DataSourceSupplier dataSourceSupplier;
final DataSourceSupplier dataSourceSupplier;
private final CurrentTenantProvider tenantProvider;
final CurrentTenantProvider tenantProvider;
TransactionFactoryTenant(TransactionManager manager, DataSourceSupplier dataSourceSupplier, CurrentTenantProvider tenantProvider) {
super(manager);
@@ -36,19 +36,19 @@ class TransactionFactoryTenant extends TransactionFactory {
}
private SpiTransaction create(int profileId, boolean explicit, Object tenantId) {
Connection c = null;
Connection connection = null;
try {
if (tenantId == null) {
// tenantId not set (by lazy loading) so get current tenantId
tenantId = tenantProvider.currentId();
}
c = dataSourceSupplier.getConnection(tenantId);
SpiTransaction transaction = manager.createTransaction(profileId, explicit, c, counter.incrementAndGet());
connection = dataSourceSupplier.getConnection(tenantId);
SpiTransaction transaction = manager.createTransaction(profileId, explicit, connection, counter.incrementAndGet());
transaction.setTenantId(tenantId);
return transaction;
} catch (PersistenceException ex) {
JdbcClose.close(c);
JdbcClose.close(connection);
throw ex;
} catch (SQLException ex) {
@@ -0,0 +1,45 @@
package io.ebeaninternal.server.transaction;
import io.ebean.config.CurrentTenantProvider;
import io.ebeaninternal.api.SpiTransaction;
import io.ebeaninternal.util.JdbcClose;
import javax.persistence.PersistenceException;
import java.sql.Connection;
import java.sql.SQLException;
/**
* Aware of read only autoCommit based DataSource.
* <p>
* This means for implicit query only transactions we can:
* - Use the read only DataSource
* - Skip explicit commit (as we use AutoCommit instead)
* </p>
*/
class TransactionFactoryTenantWithRead extends TransactionFactoryTenant {
TransactionFactoryTenantWithRead(TransactionManager manager, DataSourceSupplier dataSourceSupplier, CurrentTenantProvider tenantProvider) {
super(manager, dataSourceSupplier, tenantProvider);
}
@Override
public SpiTransaction createQueryTransaction(Object tenantId) {
Connection connection = null;
try {
if (tenantId == null) {
// tenantId not set (by lazy loading) so get current tenantId
tenantId = tenantProvider.currentId();
}
connection = dataSourceSupplier.getReadOnlyConnection(tenantId);
return new ImplicitReadOnlyTransaction(connection, tenantId);
} catch (PersistenceException ex) {
JdbcClose.close(connection);
throw ex;
} catch (SQLException ex) {
throw new PersistenceException(ex);
}
}
}
@@ -1,8 +1,8 @@
package io.ebeaninternal.server.transaction;
import io.ebean.BackgroundExecutor;
import io.ebean.config.CurrentTenantProvider;
import io.ebean.annotation.PersistBatch;
import io.ebean.config.CurrentTenantProvider;
import io.ebean.config.dbplatform.DatabasePlatform;
import io.ebean.config.dbplatform.DatabasePlatform.OnQueryOnly;
import io.ebean.event.changelog.ChangeLogListener;
@@ -141,11 +141,7 @@ public class TransactionManager {
this.onQueryOnly = initOnQueryOnly(options.config.getDatabasePlatform().getOnQueryOnly());
CurrentTenantProvider tenantProvider = options.config.getCurrentTenantProvider();
if (tenantProvider == null) {
transactionFactory = new TransactionFactoryBasic(this, dataSourceSupplier);
} else {
transactionFactory = new TransactionFactoryTenant(this, dataSourceSupplier, tenantProvider);
}
this.transactionFactory = TransactionFactoryBuilder.build(this, dataSourceSupplier, tenantProvider);
}
/**
@@ -221,6 +217,10 @@ public class TransactionManager {
return dataSourceSupplier.getDataSource();
}
public DataSource getReadOnlyDataSource() {
return dataSourceSupplier.getReadOnlyDataSource();
}
/**
* Defines the type of behavior to use when closing a transaction that was used to query data only.
*/