001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.kernel.impl;
019
020import org.fcrepo.kernel.api.ContainmentIndex;
021import org.fcrepo.kernel.api.Transaction;
022import org.fcrepo.kernel.api.TransactionManager;
023import org.fcrepo.kernel.api.exception.TransactionClosedException;
024import org.fcrepo.kernel.api.exception.TransactionNotFoundException;
025import org.fcrepo.kernel.api.observer.EventAccumulator;
026import org.fcrepo.kernel.api.services.MembershipService;
027import org.fcrepo.kernel.api.services.ReferenceService;
028import org.fcrepo.persistence.api.PersistentStorageSessionManager;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031import org.springframework.beans.factory.annotation.Autowired;
032import org.springframework.beans.factory.annotation.Qualifier;
033import org.springframework.scheduling.annotation.Scheduled;
034import org.springframework.stereotype.Component;
035
036import javax.inject.Inject;
037import java.util.Map;
038import java.util.concurrent.ConcurrentHashMap;
039
040import static java.util.UUID.randomUUID;
041
042/**
043 * The Fedora Transaction Manager implementation
044 *
045 * @author mohideen
046 */
047@Component
048public class TransactionManagerImpl implements TransactionManager {
049
050    private static final Logger LOGGER = LoggerFactory.getLogger(TransactionManagerImpl.class);
051
052    private final Map<String, Transaction> transactions;
053
054    @Autowired
055    @Qualifier("containmentIndex")
056    private ContainmentIndex containmentIndex;
057
058    @Inject
059    private PersistentStorageSessionManager pSessionManager;
060
061    @Inject
062    private EventAccumulator eventAccumulator;
063
064    @Autowired
065    @Qualifier("referenceService")
066    private ReferenceService referenceService;
067
068    @Inject
069    private MembershipService membershipService;
070
071    TransactionManagerImpl() {
072        transactions = new ConcurrentHashMap<>();
073    }
074
075    /**
076     * Periodically scan for closed transactions for cleanup
077     */
078    @Scheduled(fixedDelayString = "#{fedoraPropsConfig.sessionTimeout}")
079    public void cleanupClosedTransactions() {
080        LOGGER.trace("Cleaning up expired transactions");
081
082        final var txIt = transactions.entrySet().iterator();
083        while (txIt.hasNext()) {
084            final var txEntry = txIt.next();
085            final var tx = txEntry.getValue();
086
087            // Cleanup if transaction is closed and past its expiration time
088            if (tx.isCommitted() || tx.isRolledBack()) {
089                if (tx.hasExpired()) {
090                    txIt.remove();
091                }
092            } else if (tx.hasExpired()) {
093                LOGGER.debug("Rolling back expired transaction {}", tx.getId());
094                try {
095                    // If the tx has expired but is not already closed, then rollback
096                    // but don't immediately remove it from the list of transactions
097                    // so that the rolled back status can be checked
098                    tx.rollback();
099                } catch (final RuntimeException e) {
100                    LOGGER.error("Failed to rollback expired transaction {}", tx.getId(), e);
101                }
102            }
103
104            if (tx.hasExpired()) {
105                // By this point the session as already been committed or rolledback by the transaction
106                pSessionManager.removeSession(tx.getId());
107            }
108        }
109    }
110
111    @Override
112    public synchronized Transaction create() {
113        String txId = randomUUID().toString();
114        while (transactions.containsKey(txId)) {
115            txId = randomUUID().toString();
116        }
117        final Transaction tx = new TransactionImpl(txId, this);
118        transactions.put(txId, tx);
119        return tx;
120    }
121
122    @Override
123    public Transaction get(final String transactionId) {
124        if (transactions.containsKey(transactionId)) {
125            final Transaction transaction = transactions.get(transactionId);
126            if (transaction.hasExpired()) {
127                transaction.rollback();
128                throw new TransactionClosedException("Transaction with transactionId: " + transactionId +
129                    " expired at " + transaction.getExpires() + "!");
130            }
131            if (transaction.isCommitted()) {
132                throw new TransactionClosedException("Transaction with transactionId: " + transactionId +
133                        " has already been committed.");
134            }
135            if (transaction.isRolledBack()) {
136                throw new TransactionClosedException("Transaction with transactionId: " + transactionId +
137                        " has already been rolled back.");
138            }
139            return transaction;
140        } else {
141            throw new TransactionNotFoundException("No Transaction found with transactionId: " + transactionId);
142        }
143    }
144
145    protected PersistentStorageSessionManager getPersistentStorageSessionManager() {
146        return pSessionManager;
147    }
148
149    protected ContainmentIndex getContainmentIndex() {
150        return containmentIndex;
151    }
152
153    protected EventAccumulator getEventAccumulator() {
154        return eventAccumulator;
155    }
156
157    protected ReferenceService getReferenceService() {
158        return referenceService;
159    }
160
161    protected MembershipService getMembershipService() {
162        return membershipService;
163    }
164}