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}