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}