All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.ebeaninternal.server.transaction.TransactionManager Maven / Gradle / Ivy

There is a newer version: 15.8.0
Show newest version
package io.ebeaninternal.server.transaction;

import io.avaje.applog.AppLog;
import io.ebean.BackgroundExecutor;
import io.ebean.ProfileLocation;
import io.ebean.TxScope;
import io.ebean.annotation.PersistBatch;
import io.ebean.annotation.TxType;
import io.ebean.cache.ServerCacheNotification;
import io.ebean.cache.ServerCacheNotify;
import io.ebean.config.CurrentTenantProvider;
import io.ebean.config.dbplatform.DatabasePlatform;
import io.ebean.event.changelog.ChangeLogListener;
import io.ebean.event.changelog.ChangeLogPrepare;
import io.ebean.event.changelog.ChangeSet;
import io.ebean.meta.MetricVisitor;
import io.ebean.metric.MetricFactory;
import io.ebean.metric.TimedMetric;
import io.ebean.metric.TimedMetricMap;
import io.ebean.plugin.SpiServer;
import io.ebeaninternal.api.*;
import io.ebeaninternal.api.TransactionEventTable.TableIUD;
import io.ebeaninternal.server.cache.CacheChangeSet;
import io.ebeaninternal.server.cluster.ClusterManager;
import io.ebeaninternal.server.deploy.BeanDescriptorManager;
import io.ebeaninternal.server.profile.TimedProfileLocation;
import io.ebeaninternal.server.profile.TimedProfileLocationRegistry;

import jakarta.persistence.PersistenceException;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.ERROR;

/**
 * Manages transactions.
 * 

* Keeps the Cache and Cluster in sync when transactions are committed. */ public class TransactionManager implements SpiTransactionManager { private static final System.Logger log = CoreLog.log; private static final System.Logger clusterLogger = AppLog.getLogger("io.ebean.Cluster"); private final SpiServer server; private final BeanDescriptorManager beanDescriptorManager; /** * Ebean defaults this to true but for EJB compatible behaviour set this to false; */ private final boolean rollbackOnChecked; /** * Prefix for transaction id's (logging). */ final String prefix; /** * The dataSource of connections. */ private final DataSourceSupplier dataSourceSupplier; /** * Flag to indicate the default Isolation is READ COMMITTED. This enables us * to close queryOnly transactions rather than commit or rollback them. */ private final BackgroundExecutor backgroundExecutor; private final ClusterManager clusterManager; private final String serverName; /** * The elastic search index update processor. */ private final boolean autoPersistUpdates; private final boolean persistBatch; private final boolean persistBatchOnCascade; private final BulkEventListenerMap bulkEventListenerMap; private final ChangeLogPrepare changeLogPrepare; private final ChangeLogListener changeLogListener; private final boolean changeLogAsync; final boolean notifyL2CacheInForeground; private final boolean viewInvalidation; private final boolean skipCacheAfterWrite; private final TransactionFactory transactionFactory; private final SpiLogManager logManager; private final DatabasePlatform databasePlatform; private final SpiProfileHandler profileHandler; private final TimedMetric txnMain; private final TimedMetric txnReadOnly; private final TimedMetricMap txnNamed; private final TransactionScopeManager scopeManager; private final TableModState tableModState; private final ServerCacheNotify cacheNotify; private final boolean supportsSavepointId; private final ConcurrentHashMap profileLocations = new ConcurrentHashMap<>(); /** * Create the TransactionManager */ public TransactionManager(TransactionManagerOptions options) { this.server = options.server; this.logManager = options.logManager; this.databasePlatform = options.config.getDatabasePlatform(); this.supportsSavepointId = databasePlatform.supportsSavepointId(); this.skipCacheAfterWrite = options.config.isSkipCacheAfterWrite(); this.notifyL2CacheInForeground = options.notifyL2CacheInForeground; this.autoPersistUpdates = options.config.isAutoPersistUpdates(); this.persistBatch = PersistBatch.ALL == options.config.getPersistBatch(); this.persistBatchOnCascade = PersistBatch.ALL == options.config.appliedPersistBatchOnCascade(); this.rollbackOnChecked = options.config.isTransactionRollbackOnChecked(); this.beanDescriptorManager = options.descMgr; this.viewInvalidation = options.descMgr.requiresViewEntityCacheInvalidation(); this.changeLogPrepare = options.descMgr.changeLogPrepare(); this.changeLogListener = options.descMgr.changeLogListener(); this.changeLogAsync = options.config.isChangeLogAsync(); this.clusterManager = options.clusterManager; this.serverName = options.config.getName(); this.scopeManager = options.scopeManager; this.tableModState = options.tableModState; this.cacheNotify = options.cacheNotify; this.backgroundExecutor = options.backgroundExecutor; this.dataSourceSupplier = options.dataSourceSupplier; this.profileHandler = options.profileHandler; this.bulkEventListenerMap = new BulkEventListenerMap(options.config.getBulkTableEventListeners()); this.prefix = ""; CurrentTenantProvider tenantProvider = options.config.getCurrentTenantProvider(); this.transactionFactory = TransactionFactoryBuilder.build(this, dataSourceSupplier, tenantProvider); MetricFactory metricFactory = MetricFactory.get(); this.txnMain = metricFactory.createTimedMetric("txn.main"); this.txnReadOnly = metricFactory.createTimedMetric("txn.readonly"); this.txnNamed = metricFactory.createTimedMetricMap("txn.named."); scopeManager.register(this); } /** * Create a new scoped transaction. */ private ScopedTransaction createScopedTransaction() { return new ScopedTransaction(scopeManager); } /** * Return the scope manager. */ @Override public final TransactionScopeManager scope() { return scopeManager; } /** * Set the transaction onto the scope. */ public final void set(SpiTransaction txn) { scopeManager.set(txn); } /** * Return the current active transaction. */ @Override public final SpiTransaction active() { return scopeManager.active(); } /** * Return the current active transaction as a scoped transaction. */ private ScopedTransaction activeScoped() { return (ScopedTransaction) scopeManager.active(); } /** * Return the current transaction from thread local scope. Note that it may be inactive. */ public final SpiTransaction inScope() { return scopeManager.inScope(); } /** * Translate the SQLException into a specific exception if possible based on the DB platform. */ public final PersistenceException translate(String message, SQLException cause) { return databasePlatform.translate(message, cause); } public final void shutdown(boolean shutdownDataSource, boolean deregisterDriver) { if (shutdownDataSource) { dataSourceSupplier.shutdown(deregisterDriver); } } /** * Return true if the DB platform supports SavepointId(). */ final boolean isSupportsSavepointId() { return supportsSavepointId; } final boolean isSkipCacheAfterWrite() { return skipCacheAfterWrite; } public final BeanDescriptorManager descriptorManager() { return beanDescriptorManager; } final BulkEventListenerMap bulkEventListenerMap() { return bulkEventListenerMap; } final boolean isAutoPersistUpdates() { return autoPersistUpdates; } final boolean isPersistBatch() { return persistBatch; } final boolean isPersistBatchOnCascade() { return persistBatchOnCascade; } public final String name() { return serverName; } @Override public final Connection queryPlanConnection() throws SQLException { return dataSourceSupplier.connection(null); } @Override public final DataSource dataSource() { return dataSourceSupplier.dataSource(); } @Override public final DataSource readOnlyDataSource() { return dataSourceSupplier.readOnlyDataSource(); } /** * Wrap the externally supplied Connection. */ public SpiTransaction wrapExternalConnection(Connection c) { ExternalJdbcTransaction t = new ExternalJdbcTransaction(true, c, this); // set the default batch mode t.setBatchMode(persistBatch); t.setBatchOnCascade(persistBatchOnCascade); return t; } private SpiTransaction createTransaction(TxScope txScope) { if (txScope.isReadonly()) { return createReadOnlyTransaction(null, false); } else { return createTransaction(true, txScope.getIsolationLevel()); } } /** * Create a new Transaction. */ public SpiTransaction createTransaction(boolean explicit, int isolationLevel) { return transactionFactory.createTransaction(explicit, isolationLevel); } /** * Create a new Transaction for query only purposes (can use read only datasource). */ public SpiTransaction createReadOnlyTransaction(Object tenantId, boolean useMaster) { return transactionFactory.createReadOnlyTransaction(tenantId, useMaster); } /** * Create a new transaction. */ SpiTransaction createTransaction(boolean explicit, Connection c) { return new JdbcTransaction(explicit, c, this); } /** * Process a local rolled back transaction. */ @Override public final void notifyOfRollback(SpiTransaction transaction, Throwable cause) { // Do nothing now } /** * Query only transaction in read committed isolation. */ @Override public final void notifyOfQueryOnly(SpiTransaction transaction) { // Nothing that interesting here } /** * Process a local committed transaction. */ @Override public final void notifyOfCommit(SpiTransaction transaction) { try { PostCommitProcessing postCommit = new PostCommitProcessing(clusterManager, this, transaction); postCommit.notifyLocalCache(); backgroundExecutor.execute(postCommit.backgroundNotify()); } catch (Exception ex) { log.log(ERROR, "NotifyOfCommit failed. L2 Cache potentially not notified.", ex); } } public final void externalModification(TransactionEventTable tableEvent) { SpiTransaction t = active(); if (t != null) { t.event().add(tableEvent); } else { externalModificationEvent(tableEvent); } } private void externalModificationEvent(TransactionEventTable tableEvents) { TransactionEvent event = new TransactionEvent(); event.add(tableEvents); PostCommitProcessing postCommit = new PostCommitProcessing(clusterManager, this, event); postCommit.notifyLocalCache(); backgroundExecutor.execute(postCommit.backgroundNotify()); } /** * Notify local BeanPersistListeners etc of events from another server in the cluster. */ public final void remoteTransactionEvent(RemoteTransactionEvent remoteEvent) { if (clusterLogger.isLoggable(DEBUG)) { clusterLogger.log(DEBUG, "processing {0}", remoteEvent); } CacheChangeSet changeSet = new CacheChangeSet(); RemoteTableMod tableMod = remoteEvent.getRemoteTableMod(); if (tableMod != null) { changeSet.addInvalidate(tableMod.getTables()); } List tableIUDList = remoteEvent.getTableIUDList(); if (tableIUDList != null) { for (TableIUD tableIUD : tableIUDList) { beanDescriptorManager.cacheNotify(tableIUD, changeSet); } } // note DeleteById is written as BeanPersistIds and getBeanPersistList() // processes both Bean IUD and DeleteById List beanPersistList = remoteEvent.getBeanPersistList(); if (beanPersistList != null) { for (BeanPersistIds persistIds : beanPersistList) { persistIds.notifyCache(changeSet); } } changeSet.apply(); } /** * Prepare and then send/log the changeSet. */ final void sendChangeLog(final ChangeSet changeSet) { // can set userId, userIpAddress & userContext if desired if (changeLogPrepare.prepare(changeSet)) { if (changeLogAsync) { // call the log method in background backgroundExecutor.execute(() -> changeLogListener.log(changeSet)); } else { changeLogListener.log(changeSet); } } } /** * Invalidate the query caches for entities based on views. */ final void processTouchedTables(Set touchedTables) { tableModState.touch(touchedTables); if (viewInvalidation) { beanDescriptorManager.processViewInvalidation(touchedTables); } cacheNotify.notify(new ServerCacheNotification(touchedTables)); } /** * Process the collected transaction profiling information. */ final void profileCollect(TransactionProfile transactionProfile) { profileHandler.collectTransactionProfile(transactionProfile); } /** * Collect execution time for an explicit transaction. */ final void collectMetric(long exeMicros) { txnMain.add(exeMicros); } /** * Collect execution time for implicit read only transaction. */ final void collectMetricReadOnly(long exeMicros) { txnReadOnly.add(exeMicros); } /** * Collect execution time for a named transaction. */ final void collectMetricNamed(long exeMicros, String label) { txnNamed.add(label, exeMicros); } public final void visitMetrics(MetricVisitor visitor) { txnMain.visit(visitor); txnReadOnly.visit(visitor); txnNamed.visit(visitor); for (TimedProfileLocation timedLocation : TimedProfileLocationRegistry.registered()) { timedLocation.visit(visitor); } } /** * Clear an implicit transaction from thread local scope. */ public final void clearServerTransaction() { scopeManager.clear(); } /** * Begin an implicit transaction. */ public final SpiTransaction beginServerTransaction() { SpiTransaction t = createTransaction(false, -1); scopeManager.set(t); return t; } /** * Exit a scoped transaction (that can be inactive - already committed etc). */ public final void exitScopedTransaction(Object returnOrThrowable, int opCode) { SpiTransaction st = inScope(); if (st instanceof ScopedTransaction) { // can be null for Supports as that can start as a 'No Transaction' and then // effectively be replaced by transactions inside the scope ((ScopedTransaction) st).complete(returnOrThrowable, opCode); } } @Override public final void externalRemoveTransaction() { scopeManager.clearExternal(); } /** * Push an externally created transaction into scope. This transaction is usually managed externally * (e.g. Spring managed transaction). */ @Override public final ScopedTransaction externalBeginTransaction(SpiTransaction transaction, TxScope txScope) { ScopedTransaction scopedTxn = new ScopedTransaction(scopeManager); scopedTxn.push(new ScopeTrans(rollbackOnChecked, false, transaction, txScope)); scopeManager.replace(scopedTxn); return scopedTxn; } /** * Begin a scoped transaction. */ public final ScopedTransaction beginScopedTransaction(TxScope txScope) { txScope = initTxScope(txScope); ScopedTransaction txnContainer = activeScoped(); boolean setToScope; boolean nestedSavepoint; SpiTransaction transaction; if (txnContainer == null) { txnContainer = createScopedTransaction(); setToScope = true; transaction = null; nestedSavepoint = false; } else { setToScope = false; transaction = txnContainer.current(); nestedSavepoint = txnContainer.isNestedUseSavepoint(); } TxType type = txScope.getType(); boolean createTransaction; if (nestedSavepoint && (type == TxType.REQUIRED || type == TxType.REQUIRES_NEW)) { createTransaction = true; transaction = createSavepoint(transaction, this); } else { createTransaction = isCreateNewTransaction(transaction, type); if (createTransaction) { switch (type) { case SUPPORTS: case NOT_SUPPORTED: case NEVER: transaction = NoTransaction.INSTANCE; break; default: transaction = createTransaction(txScope); initNewTransaction(transaction, txScope); } } } txnContainer.push(new ScopeTrans(rollbackOnChecked, createTransaction, transaction, txScope)); if (setToScope) { set(txnContainer); } return txnContainer; } private SpiTransaction createSavepoint(SpiTransaction transaction, TransactionManager manager) { try { return new SavepointTransaction(transaction, manager); } catch (SQLException e) { throw new PersistenceException("Error creating nested Savepoint Transaction", e); } } private void initNewTransaction(SpiTransaction transaction, TxScope txScope) { if (txScope.isSkipCache()) { transaction.setSkipCache(true); } String label = txScope.getLabel(); if (label != null) { transaction.setLabel(label); } ProfileLocation profileLocation = txScope.getProfileLocation(); if (profileLocation != null) { if (profileLocation.obtain()) { registerProfileLocation(profileLocation); } transaction.setProfileLocation(profileLocation); if (profileLocation.trace()) { transaction.setProfileStream(profileHandler.createProfileStream(profileLocation)); } } } private void registerProfileLocation(ProfileLocation profileLocation) { profileLocations.put(profileLocation.fullLocation(), profileLocation); } private TxScope initTxScope(TxScope txScope) { if (txScope == null) { return new TxScope(); } else { // check for implied batch mode via setting batchSize txScope.checkBatchMode(); return txScope; } } /** * Determine whether to create a new transaction or not. *

* This will also potentially throw exceptions for MANDATORY and NEVER types. */ private boolean isCreateNewTransaction(SpiTransaction current, TxType type) { switch (type) { case REQUIRED: case SUPPORTS: return current == null; case REQUIRES_NEW: return true; case MANDATORY: if (current == null) { throw new PersistenceException("Transaction missing when MANDATORY"); } return false; case NEVER: if (current != null) { throw new PersistenceException("Transaction exists for Transactional NEVER"); } return true; // always use NoTransaction instance case NOT_SUPPORTED: return true; // always use NoTransaction instance default: throw new RuntimeException("Should never get here?"); } } public SpiTxnLogger logger() { return logManager.logger(); } public SpiTxnLogger loggerReadOnly() { return logManager.readOnlyLogger(); } public final SpiLogManager log() { return logManager; } /** * Experimental - find dirty beans in the persistence context and persist them. */ public final void flushTransparent(SpiPersistenceContext persistenceContext, SpiTransaction transaction) { List dirtyBeans = persistenceContext.dirtyBeans(beanDescriptorManager); if (!dirtyBeans.isEmpty()) { server.updateAll(dirtyBeans, transaction); } } }