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 static org.fcrepo.kernel.api.lock.ResourceLockType.EXCLUSIVE;
009import static org.fcrepo.kernel.api.lock.ResourceLockType.NONEXCLUSIVE;
010
011import java.util.HashSet;
012import java.util.Map;
013import java.util.Objects;
014import java.util.Set;
015import java.util.concurrent.ConcurrentHashMap;
016import java.util.concurrent.TimeUnit;
017
018import org.fcrepo.kernel.api.exception.ConcurrentUpdateException;
019import org.fcrepo.kernel.api.identifiers.FedoraId;
020import org.fcrepo.kernel.api.lock.ResourceLock;
021import org.fcrepo.kernel.api.lock.ResourceLockManager;
022import org.fcrepo.kernel.api.lock.ResourceLockType;
023
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026import org.springframework.stereotype.Component;
027
028import com.github.benmanes.caffeine.cache.Caffeine;
029import com.google.common.collect.Sets;
030
031/**
032 * In memory resource lock manager
033 *
034 * @author pwinckles
035 */
036@Component
037public class InMemoryResourceLockManager implements ResourceLockManager {
038
039    private static final Logger LOG = LoggerFactory.getLogger(InMemoryResourceLockManager.class);
040
041    private final Map<String, Set<ResourceLock>> transactionLocks;
042    private final Map<FedoraId, Set<ResourceLock>> resourceLocks;
043
044    /**
045     * The internal lock is used so that internal to this class there is only one thread at a time acquiring or
046     * releasing locks on a specific resource.
047     */
048    private final Map<String, Object> internalResourceLocks;
049
050    public InMemoryResourceLockManager() {
051        transactionLocks = new ConcurrentHashMap<>();
052        resourceLocks = new ConcurrentHashMap<>();
053        internalResourceLocks = Caffeine.newBuilder()
054                .expireAfterAccess(10, TimeUnit.MINUTES)
055                .<String, Object>build()
056                .asMap();
057    }
058
059    @Override
060    public void acquireExclusive(final String txId, final FedoraId resourceId) {
061        acquireInternal(txId, resourceId, EXCLUSIVE);
062    }
063
064    @Override
065    public void acquireNonExclusive(final String txId, final FedoraId resourceId) {
066        acquireInternal(txId, resourceId, NONEXCLUSIVE);
067    }
068
069    private void acquireInternal(final String txId, final FedoraId resourceId, final ResourceLockType lockType) {
070        final var resourceLock = new ResourceLockImpl(lockType, txId, resourceId);
071
072        if (transactionHoldsAdequateLock(resourceLock)) {
073            return;
074        }
075
076        synchronized (acquireInternalLock(resourceId)) {
077            if (transactionHoldsAdequateLock(resourceLock)) {
078                return;
079            }
080
081            final var locks = resourceLocks.get(resourceId);
082
083            if (locks != null) {
084                for (final var lock : locks) {
085                    // Throw an exception if either:
086                    // 1. We need an exclusive lock, but another tx already holds any kind of lock
087                    // 2. We need a non-exclusive lock, but another tx holds an exclusive lock
088                    if ((lockType == EXCLUSIVE && !lock.getTransactionId().equals(txId))
089                            || lock.hasLockType(EXCLUSIVE)) {
090                        throw new ConcurrentUpdateException(resourceId.getResourceId(), txId, lock.getTransactionId());
091                    }
092                }
093            }
094
095            LOG.debug("Transaction {} acquiring lock on {}", txId, resourceId.getResourceId());
096
097            // This does not need to be a synchronized collection because we already synchronize internally on the
098            // resource id, so it's not possible to modify concurrently.
099            //
100            // Because we're using set to store the resource locks and the resource's identity is based on its
101            // transaction id and resource id, then a tx will only ever have at most one lock per resource.
102            // This works because we do not release locks individually, but rather all at once.
103            resourceLocks.computeIfAbsent(resourceId, key -> new HashSet<>()).add(resourceLock);
104            transactionLocks.computeIfAbsent(txId, key -> Sets.newConcurrentHashSet()).add(resourceLock);
105        }
106    }
107
108    @Override
109    public void releaseAll(final String txId) {
110        final var txLocks = transactionLocks.remove(txId);
111        if (txLocks != null) {
112            txLocks.forEach(lock -> {
113                LOG.debug("Transaction {} releasing lock on {}", txId, lock);
114                synchronized (acquireInternalLock(lock.getResourceId())) {
115                    final var locks = resourceLocks.get(lock.getResourceId());
116                    locks.remove(lock);
117                    if (locks.isEmpty()) {
118                        resourceLocks.remove(lock.getResourceId());
119                    }
120                }
121            });
122        }
123    }
124
125    private Object acquireInternalLock(final FedoraId resourceId) {
126        return internalResourceLocks.computeIfAbsent(resourceId.getResourceId(), key -> new Object());
127    }
128
129    /**
130     * Returns true if the transaction already holds an adequate lock on the resource. This means that it holds an
131     * exclusive lock if an exclusive lock is requested, or any lock if a non-exclusive lock is requested.
132     *
133     * @param requested the requested resource lock
134     * @return true if the transaction already holds an adequate lock
135     */
136    private boolean transactionHoldsAdequateLock(final ResourceLock requested) {
137        final var locks = transactionLocks.get(requested.getTransactionId());
138
139        if (locks == null) {
140            return false;
141        }
142
143        final var held = locks.stream().filter(l -> Objects.equals(requested, l)).findFirst();
144
145        return held.map(l -> l.isAdequate(requested.getLockType()))
146                .orElse(false);
147    }
148
149}