001/* 002 * The contents of this file are subject to the license and copyright 003 * detailed in the LICENSE and NOTICE files at the root of the source 004 * tree. 005 */ 006package org.fcrepo.kernel.impl; 007 008import org.fcrepo.common.db.DbTransactionExecutor; 009import org.fcrepo.config.FedoraPropsConfig; 010import org.fcrepo.kernel.api.ContainmentIndex; 011import org.fcrepo.kernel.api.Transaction; 012import org.fcrepo.kernel.api.TransactionManager; 013import org.fcrepo.kernel.api.cache.UserTypesCache; 014import org.fcrepo.kernel.api.exception.TransactionClosedException; 015import org.fcrepo.kernel.api.exception.TransactionNotFoundException; 016import org.fcrepo.kernel.api.lock.ResourceLockManager; 017import org.fcrepo.kernel.api.observer.EventAccumulator; 018import org.fcrepo.kernel.api.services.MembershipService; 019import org.fcrepo.kernel.api.services.ReferenceService; 020import org.fcrepo.persistence.api.PersistentStorageSessionManager; 021import org.fcrepo.search.api.SearchIndex; 022import org.slf4j.Logger; 023import org.slf4j.LoggerFactory; 024import org.springframework.beans.factory.annotation.Autowired; 025import org.springframework.beans.factory.annotation.Qualifier; 026import org.springframework.scheduling.annotation.Scheduled; 027import org.springframework.stereotype.Component; 028 029import javax.annotation.PostConstruct; 030import javax.annotation.PreDestroy; 031import javax.inject.Inject; 032import java.util.Map; 033import java.util.concurrent.ConcurrentHashMap; 034 035import static java.util.UUID.randomUUID; 036 037/** 038 * The Fedora Transaction Manager implementation 039 * 040 * @author mohideen 041 */ 042@Component 043public class TransactionManagerImpl implements TransactionManager { 044 045 private static final Logger LOGGER = LoggerFactory.getLogger(TransactionManagerImpl.class); 046 047 private final Map<String, Transaction> transactions; 048 049 @Autowired 050 @Qualifier("containmentIndex") 051 private ContainmentIndex containmentIndex; 052 053 @Inject 054 private PersistentStorageSessionManager pSessionManager; 055 056 @Inject 057 private EventAccumulator eventAccumulator; 058 059 @Autowired 060 @Qualifier("referenceService") 061 private ReferenceService referenceService; 062 063 @Inject 064 private MembershipService membershipService; 065 066 @Inject 067 @Qualifier("searchIndex") 068 private SearchIndex searchIndex; 069 070 @Inject 071 private ResourceLockManager resourceLockManager; 072 073 @Inject 074 private FedoraPropsConfig fedoraPropsConfig; 075 076 @Inject 077 private DbTransactionExecutor dbTransactionExecutor; 078 079 @Inject 080 private UserTypesCache userTypesCache; 081 082 TransactionManagerImpl() { 083 transactions = new ConcurrentHashMap<>(); 084 } 085 086 /** 087 * Periodically scan for closed transactions for cleanup 088 */ 089 @Scheduled(fixedDelayString = "#{fedoraPropsConfig.sessionTimeout}") 090 public void cleanupClosedTransactions() { 091 LOGGER.debug("Cleaning up expired transactions"); 092 093 final var txIt = transactions.entrySet().iterator(); 094 while (txIt.hasNext()) { 095 final var txEntry = txIt.next(); 096 final var tx = txEntry.getValue(); 097 098 // Cleanup if transaction is closed and past its expiration time 099 if (tx.isCommitted() || tx.isRolledBack()) { 100 if (tx.hasExpired()) { 101 txIt.remove(); 102 } 103 } else if (tx.hasExpired()) { 104 LOGGER.debug("Rolling back expired transaction {}", tx.getId()); 105 try { 106 // If the tx has expired but is not already closed, then rollback 107 // but don't immediately remove it from the list of transactions 108 // so that the rolled back status can be checked 109 tx.rollback(); 110 } catch (final RuntimeException e) { 111 LOGGER.error("Failed to rollback expired transaction {}", tx.getId(), e); 112 } 113 } 114 115 if (tx.hasExpired()) { 116 // By this point the session as already been committed or rolledback by the transaction 117 pSessionManager.removeSession(tx.getId()); 118 } 119 } 120 } 121 122 @Override 123 public synchronized Transaction create() { 124 String txId = randomUUID().toString(); 125 while (transactions.containsKey(txId)) { 126 txId = randomUUID().toString(); 127 } 128 final Transaction tx = new TransactionImpl(txId, this, fedoraPropsConfig.getSessionTimeout()); 129 transactions.put(txId, tx); 130 return tx; 131 } 132 133 @Override 134 public Transaction get(final String transactionId) { 135 if (transactions.containsKey(transactionId)) { 136 final Transaction transaction = transactions.get(transactionId); 137 if (transaction.hasExpired()) { 138 transaction.rollback(); 139 throw new TransactionClosedException("Transaction with transactionId: " + transactionId + 140 " expired at " + transaction.getExpires() + "!"); 141 } 142 if (transaction.isCommitted()) { 143 throw new TransactionClosedException("Transaction with transactionId: " + transactionId + 144 " has already been committed."); 145 } 146 if (transaction.isRolledBack()) { 147 throw new TransactionClosedException("Transaction with transactionId: " + transactionId + 148 " has already been rolled back."); 149 } 150 return transaction; 151 } else { 152 throw new TransactionNotFoundException("No Transaction found with transactionId: " + transactionId); 153 } 154 } 155 156 @PreDestroy 157 public void cleanupAllTransactions() { 158 LOGGER.debug("Shutting down transaction manager, attempt to rollback any incomplete transactions"); 159 final var txIt = transactions.entrySet().iterator(); 160 while (txIt.hasNext()) { 161 final var txEntry = txIt.next(); 162 final var tx = txEntry.getValue(); 163 164 if ((tx.isOpen() || tx.hasExpired()) && !tx.isRolledBack()) { 165 LOGGER.debug("Rolling back transaction as part of shutdown {}", tx.getId()); 166 try { 167 tx.rollback(); 168 pSessionManager.removeSession(tx.getId()); 169 } catch (final RuntimeException e) { 170 LOGGER.error("Failed to rollback transaction {}", tx.getId(), e); 171 } 172 } 173 } 174 LOGGER.debug("Finished rollback of all incomplete transactions as part of shut down"); 175 } 176 177 @PostConstruct 178 public void preCleanTransactions() { 179 LOGGER.debug("TransactionManagerImpl initialized, cleaning up leftover transaction entries"); 180 181 // Clean up any leftover transaction database entries immediately after startup 182 synchronized (transactions) { 183 containmentIndex.clearAllTransactions(); 184 membershipService.clearAllTransactions(); 185 referenceService.clearAllTransactions(); 186 searchIndex.clearAllTransactions(); 187 // Also clear any leftover ocfl sessions and staged files 188 pSessionManager.clearAllSessions(); 189 } 190 } 191 192 protected PersistentStorageSessionManager getPersistentStorageSessionManager() { 193 return pSessionManager; 194 } 195 196 protected ContainmentIndex getContainmentIndex() { 197 return containmentIndex; 198 } 199 200 protected SearchIndex getSearchIndex() { 201 return searchIndex; 202 } 203 204 205 protected EventAccumulator getEventAccumulator() { 206 return eventAccumulator; 207 } 208 209 protected ReferenceService getReferenceService() { 210 return referenceService; 211 } 212 213 protected MembershipService getMembershipService() { 214 return membershipService; 215 } 216 217 protected ResourceLockManager getResourceLockManager() { 218 return resourceLockManager; 219 } 220 221 protected UserTypesCache getUserTypesCache() { 222 return userTypesCache; 223 } 224 225 public DbTransactionExecutor getDbTransactionExecutor() { 226 return dbTransactionExecutor; 227 } 228}