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}