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.lock;
007
008import com.github.benmanes.caffeine.cache.Caffeine;
009import com.google.common.collect.Sets;
010import org.fcrepo.kernel.api.exception.ConcurrentUpdateException;
011import org.fcrepo.kernel.api.identifiers.FedoraId;
012import org.fcrepo.kernel.api.lock.ResourceLockManager;
013import org.slf4j.Logger;
014import org.slf4j.LoggerFactory;
015import org.springframework.stereotype.Component;
016
017import java.util.Map;
018import java.util.Set;
019import java.util.concurrent.ConcurrentHashMap;
020import java.util.concurrent.TimeUnit;
021
022/**
023 * In memory resource lock manager
024 *
025 * @author pwinckles
026 */
027@Component
028public class InMemoryResourceLockManager implements ResourceLockManager {
029
030    private static final Logger LOG = LoggerFactory.getLogger(InMemoryResourceLockManager.class);
031
032    private final Map<String, Set<String>> transactionLocks;
033    private final Set<String> lockedResources;
034    private final Map<String, Object> internalResourceLocks;
035
036    public InMemoryResourceLockManager() {
037        transactionLocks = new ConcurrentHashMap<>();
038        lockedResources = Sets.newConcurrentHashSet();
039        internalResourceLocks = Caffeine.newBuilder()
040                .expireAfterAccess(10, TimeUnit.MINUTES)
041                .<String, Object>build()
042                .asMap();
043    }
044
045    @Override
046    public void acquire(final String txId, final FedoraId resourceId) {
047        final var resourceIdStr = resourceId.getResourceId();
048
049        if (transactionHoldsLock(txId, resourceIdStr)) {
050            return;
051        }
052
053        synchronized (acquireInternalLock(resourceIdStr)) {
054            if (transactionHoldsLock(txId, resourceIdStr)) {
055                return;
056            }
057
058            if (lockedResources.contains(resourceIdStr)) {
059                throw new ConcurrentUpdateException(
060                        String.format("Cannot update %s because it is being updated by another transaction.",
061                                resourceIdStr));
062            }
063
064            LOG.debug("Transaction {} acquiring lock on {}", txId, resourceIdStr);
065
066            lockedResources.add(resourceIdStr);
067            transactionLocks.computeIfAbsent(txId, key -> Sets.newConcurrentHashSet())
068                    .add(resourceIdStr);
069        }
070    }
071
072    @Override
073    public void releaseAll(final String txId) {
074        final var locks = transactionLocks.remove(txId);
075        if (locks != null) {
076            locks.forEach(resourceId -> {
077                LOG.debug("Transaction {} releasing lock on {}", txId, resourceId);
078                synchronized (acquireInternalLock(resourceId)) {
079                    lockedResources.remove(resourceId);
080                    internalResourceLocks.remove(resourceId);
081                }
082            });
083        }
084    }
085
086    private Object acquireInternalLock(final String resourceId) {
087        return internalResourceLocks.computeIfAbsent(resourceId, key -> new Object());
088    }
089
090    private boolean transactionHoldsLock(final String txId, final String resourceId) {
091        final var locks = transactionLocks.get(txId);
092        return locks != null && locks.contains(resourceId);
093    }
094
095}