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.config.FedoraPropsConfig;
021import org.fcrepo.kernel.api.ContainmentIndex;
022import org.fcrepo.kernel.api.Transaction;
023import org.fcrepo.kernel.api.TransactionManager;
024import org.fcrepo.kernel.api.exception.TransactionClosedException;
025import org.fcrepo.kernel.api.exception.TransactionNotFoundException;
026import org.fcrepo.kernel.api.lock.ResourceLockManager;
027import org.fcrepo.kernel.api.observer.EventAccumulator;
028import org.fcrepo.kernel.api.services.MembershipService;
029import org.fcrepo.kernel.api.services.ReferenceService;
030import org.fcrepo.persistence.api.PersistentStorageSessionManager;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033import org.springframework.beans.factory.annotation.Autowired;
034import org.springframework.beans.factory.annotation.Qualifier;
035import org.springframework.scheduling.annotation.Scheduled;
036import org.springframework.stereotype.Component;
037import org.springframework.transaction.PlatformTransactionManager;
038import org.springframework.transaction.support.TransactionTemplate;
039
040import javax.annotation.PostConstruct;
041import javax.inject.Inject;
042import java.util.Map;
043import java.util.concurrent.ConcurrentHashMap;
044
045import static java.util.UUID.randomUUID;
046
047/**
048 * The Fedora Transaction Manager implementation
049 *
050 * @author mohideen
051 */
052@Component
053public class TransactionManagerImpl implements TransactionManager {
054
055    private static final Logger LOGGER = LoggerFactory.getLogger(TransactionManagerImpl.class);
056
057    private final Map<String, Transaction> transactions;
058
059    @Inject
060    private PlatformTransactionManager platformTransactionManager;
061
062    @Autowired
063    @Qualifier("containmentIndex")
064    private ContainmentIndex containmentIndex;
065
066    @Inject
067    private PersistentStorageSessionManager pSessionManager;
068
069    @Inject
070    private EventAccumulator eventAccumulator;
071
072    @Autowired
073    @Qualifier("referenceService")
074    private ReferenceService referenceService;
075
076    @Inject
077    private MembershipService membershipService;
078
079    @Inject
080    private ResourceLockManager resourceLockManager;
081
082    @Inject
083    private FedoraPropsConfig fedoraPropsConfig;
084
085    private TransactionTemplate transactionTemplate;
086
087    @PostConstruct
088    public void postConstruct() {
089        transactionTemplate = new TransactionTemplate(platformTransactionManager);
090    }
091
092    TransactionManagerImpl() {
093        transactions = new ConcurrentHashMap<>();
094    }
095
096    /**
097     * Periodically scan for closed transactions for cleanup
098     */
099    @Scheduled(fixedDelayString = "#{fedoraPropsConfig.sessionTimeout}")
100    public void cleanupClosedTransactions() {
101        LOGGER.trace("Cleaning up expired transactions");
102
103        final var txIt = transactions.entrySet().iterator();
104        while (txIt.hasNext()) {
105            final var txEntry = txIt.next();
106            final var tx = txEntry.getValue();
107
108            // Cleanup if transaction is closed and past its expiration time
109            if (tx.isCommitted() || tx.isRolledBack()) {
110                if (tx.hasExpired()) {
111                    txIt.remove();
112                }
113            } else if (tx.hasExpired()) {
114                LOGGER.debug("Rolling back expired transaction {}", tx.getId());
115                try {
116                    // If the tx has expired but is not already closed, then rollback
117                    // but don't immediately remove it from the list of transactions
118                    // so that the rolled back status can be checked
119                    tx.rollback();
120                } catch (final RuntimeException e) {
121                    LOGGER.error("Failed to rollback expired transaction {}", tx.getId(), e);
122                }
123            }
124
125            if (tx.hasExpired()) {
126                // By this point the session as already been committed or rolledback by the transaction
127                pSessionManager.removeSession(tx.getId());
128            }
129        }
130    }
131
132    @Override
133    public synchronized Transaction create() {
134        String txId = randomUUID().toString();
135        while (transactions.containsKey(txId)) {
136            txId = randomUUID().toString();
137        }
138        final Transaction tx = new TransactionImpl(txId, this, fedoraPropsConfig.getSessionTimeout());
139        transactions.put(txId, tx);
140        return tx;
141    }
142
143    @Override
144    public Transaction get(final String transactionId) {
145        if (transactions.containsKey(transactionId)) {
146            final Transaction transaction = transactions.get(transactionId);
147            if (transaction.hasExpired()) {
148                transaction.rollback();
149                throw new TransactionClosedException("Transaction with transactionId: " + transactionId +
150                    " expired at " + transaction.getExpires() + "!");
151            }
152            if (transaction.isCommitted()) {
153                throw new TransactionClosedException("Transaction with transactionId: " + transactionId +
154                        " has already been committed.");
155            }
156            if (transaction.isRolledBack()) {
157                throw new TransactionClosedException("Transaction with transactionId: " + transactionId +
158                        " has already been rolled back.");
159            }
160            return transaction;
161        } else {
162            throw new TransactionNotFoundException("No Transaction found with transactionId: " + transactionId);
163        }
164    }
165
166    protected PersistentStorageSessionManager getPersistentStorageSessionManager() {
167        return pSessionManager;
168    }
169
170    protected ContainmentIndex getContainmentIndex() {
171        return containmentIndex;
172    }
173
174    protected EventAccumulator getEventAccumulator() {
175        return eventAccumulator;
176    }
177
178    protected ReferenceService getReferenceService() {
179        return referenceService;
180    }
181
182    protected MembershipService getMembershipService() {
183        return membershipService;
184    }
185
186    protected TransactionTemplate getTransactionTemplate() {
187        return transactionTemplate;
188    }
189
190    protected ResourceLockManager getResourceLockManager() {
191        return resourceLockManager;
192    }
193
194}