/*
*
* * Copyright 2014 Orient Technologies LTD (info(at)orientechnologies.com)
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
* *
* * For more information: http://www.orientechnologies.com
*
*/
package com.orientechnologies.common.concur.lock;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import com.orientechnologies.common.exception.OException;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Original Lock Manager implementation that uses a concurrent linked hash map to store one entry per key. This could be very
* expensive in case the number of locks are a lot. This implementation works better than {@link OPartitionedLockManager} when
* running distributed because there is no way to
*
* @param <T>
* Type of keys
*
* @author Luca Garulli
*/
public class OOneEntryPerKeyLockManager<T> implements OLockManager<T> {
public enum LOCK {
SHARED, EXCLUSIVE
}
private long acquireTimeout;
protected final ConcurrentLinkedHashMap<T, CountableLock> map;
private final boolean enabled;
private final int amountOfCachedInstances;
private final static Object NULL_KEY = new Object();
@SuppressWarnings("serial")
private static class CountableLock {
private final AtomicInteger countLocks = new AtomicInteger(1);
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
}
public OOneEntryPerKeyLockManager(final boolean iEnabled, final int iAcquireTimeout, final int amountOfCachedInstances) {
this(iEnabled, iAcquireTimeout, OGlobalConfiguration.ENVIRONMENT_LOCK_MANAGER_CONCURRENCY_LEVEL.getValueAsInteger(),
amountOfCachedInstances);
}
public OOneEntryPerKeyLockManager(final boolean iEnabled, final int iAcquireTimeout, final int concurrencyLevel,
final int amountOfCachedInstances) {
this.amountOfCachedInstances = amountOfCachedInstances;
final int cL = closestInteger(concurrencyLevel);
map = new ConcurrentLinkedHashMap.Builder<T, CountableLock>().concurrencyLevel(cL).maximumWeightedCapacity(Long.MAX_VALUE)
.build();
acquireTimeout = iAcquireTimeout;
enabled = iEnabled;
}
@Override
public Lock acquireSharedLock(final T key) {
return acquireLock(key, LOCK.SHARED);
}
@Override
public void releaseSharedLock(final T key) {
releaseLock(Thread.currentThread(), key, LOCK.SHARED);
}
@Override
public Lock acquireExclusiveLock(final T key) {
return acquireLock(key, LOCK.EXCLUSIVE);
}
@Override
public void releaseExclusiveLock(final T key) {
releaseLock(Thread.currentThread(), key, LOCK.EXCLUSIVE);
}
public Lock acquireLock(final T iResourceId, final LOCK iLockType) {
return acquireLock(iResourceId, iLockType, acquireTimeout);
}
public Lock acquireLock(final T iResourceId, final LOCK iLockType, long iTimeout) {
if (!enabled)
return null;
T immutableResource = getImmutableResourceId(iResourceId);
if (immutableResource == null)
immutableResource = (T) NULL_KEY;
CountableLock lock;
do {
lock = map.get(immutableResource);
if (lock != null) {
final int oldLockCount = lock.countLocks.get();
if (oldLockCount >= 0) {
if (lock.countLocks.compareAndSet(oldLockCount, oldLockCount + 1)) {
break;
}
} else {
map.remove(immutableResource, lock);
}
}
} while (lock != null);
if (lock == null) {
while (true) {
lock = new CountableLock();
CountableLock oldLock = map.putIfAbsent(immutableResource, lock);
if (oldLock == null)
break;
lock = oldLock;
final int oldValue = lock.countLocks.get();
if (oldValue >= 0) {
if (lock.countLocks.compareAndSet(oldValue, oldValue + 1)) {
assert map.get(immutableResource) == lock;
break;
}
} else {
map.remove(immutableResource, lock);
}
}
}
if (map.size() > amountOfCachedInstances) {
final Iterator<T> keyToRemoveIterator = map.ascendingKeySetWithLimit(1).iterator();
if (keyToRemoveIterator.hasNext()) {
final T keyToRemove = keyToRemoveIterator.next();
final CountableLock lockToRemove = map.get(keyToRemove);
if (lockToRemove != null) {
final int counter = lockToRemove.countLocks.get();
if (counter == 0 && lockToRemove.countLocks.compareAndSet(counter, -1)) {
assert lockToRemove.countLocks.get() == -1;
map.remove(keyToRemove, lockToRemove);
}
}
}
}
try {
if (iTimeout <= 0) {
if (iLockType == LOCK.SHARED)
lock.readWriteLock.readLock().lock();
else
lock.readWriteLock.writeLock().lock();
} else {
try {
if (iLockType == LOCK.SHARED) {
if (!lock.readWriteLock.readLock().tryLock(iTimeout, TimeUnit.MILLISECONDS))
throw new OLockException(
"Timeout (" + iTimeout + "ms) on acquiring resource '" + iResourceId + "' because is locked from another thread");
} else {
if (!lock.readWriteLock.writeLock().tryLock(iTimeout, TimeUnit.MILLISECONDS))
throw new OLockException(
"Timeout (" + iTimeout + "ms) on acquiring resource '" + iResourceId + "' because is locked from another thread");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw OException.wrapException(new OLockException("Thread interrupted while waiting for resource '" + iResourceId + "'"),
e);
}
}
return new CountableLockWrapper(lock, iLockType == LOCK.SHARED);
} catch (RuntimeException e) {
final int usages = lock.countLocks.decrementAndGet();
if (usages == 0)
map.remove(immutableResource);
throw e;
}
}
public void releaseLock(final Object iRequester, T iResourceId, final LOCK iLockType) throws OLockException {
if (!enabled)
return;
if (iResourceId == null)
iResourceId = (T) NULL_KEY;
final CountableLock lock = map.get(iResourceId);
if (lock == null)
throw new OLockException("Error on releasing a non acquired lock by the requester '" + iRequester
+ "' against the resource: '" + iResourceId + "'");
lock.countLocks.decrementAndGet();
if (iLockType == LOCK.SHARED)
lock.readWriteLock.readLock().unlock();
else
lock.readWriteLock.writeLock().unlock();
}
@Override
public Lock[] acquireExclusiveLocksInBatch(final T... values) {
return acquireLockInBatch(values, true);
}
@Override
public Lock[] acquireSharedLocksInBatch(final T... values) {
return acquireLockInBatch(values, false);
}
@Override
public Lock[] acquireExclusiveLocksInBatch(Collection<T> values) {
if (values == null || values.isEmpty())
return new Lock[0];
final List<Comparable> comparables = new ArrayList<Comparable>();
int seenNulls = 0;
for (T value : values) {
if (value instanceof Comparable) {
comparables.add((Comparable) value);
} else if (value == null) {
++seenNulls;
} else {
throw new IllegalArgumentException(
"In order to lock value in batch it should implement " + Comparable.class.getName() + " interface");
}
}
Collections.sort(comparables);
final Lock[] locks = new Lock[comparables.size() + seenNulls];
int i = 0;
for (int j = 0; j < seenNulls; ++j)
locks[i++] = acquireExclusiveLock((T) NULL_KEY);
for (Comparable value : comparables)
locks[i++] = acquireExclusiveLock((T) value);
return locks;
}
@Override
public void lockAllExclusive() {
for (CountableLock lock : map.values()) {
lock.readWriteLock.writeLock().lock();
}
}
@Override
public void unlockAllExclusive() {
for (CountableLock lock : map.values()) {
lock.readWriteLock.writeLock().unlock();
}
}
// For tests purposes.
public int getCountCurrentLocks() {
return map.size();
}
protected T getImmutableResourceId(final T iResourceId) {
return iResourceId;
}
private static int closestInteger(int value) {
return 1 << (32 - Integer.numberOfLeadingZeros(value - 1));
}
@SuppressWarnings("NullableProblems")
/* internal */ static class CountableLockWrapper implements Lock {
private final CountableLock countableLock;
private final boolean read;
public CountableLockWrapper(CountableLock countableLock, boolean read) {
this.countableLock = countableLock;
this.read = read;
}
/* internal */ int getLockCount() { // for testing purposes
return countableLock.countLocks.get();
}
@Override
public void lock() {
throw new UnsupportedOperationException();
}
@Override
public void lockInterruptibly() throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public boolean tryLock() {
throw new UnsupportedOperationException();
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public void unlock() {
countableLock.countLocks.decrementAndGet();
if (read)
countableLock.readWriteLock.readLock().unlock();
else
countableLock.readWriteLock.writeLock().unlock();
}
@Override
public Condition newCondition() {
throw new UnsupportedOperationException();
}
}
protected Lock[] acquireLockInBatch(T[] values, final boolean exclusiveMode) {
if (values == null || values.length == 0)
return null;
final List<Comparable> comparables = new ArrayList<Comparable>();
int seenNulls = 0;
for (T value : values) {
if (value instanceof Comparable) {
comparables.add((Comparable) value);
} else if (value == null) {
++seenNulls;
} else {
throw new IllegalArgumentException(
"In order to lock value in batch it should implement " + Comparable.class.getName() + " interface");
}
}
Collections.sort(comparables);
final Lock[] locks = new Lock[comparables.size() + seenNulls];
int i = 0;
for (int j = 0; j < seenNulls; ++j)
locks[i++] = exclusiveMode ? acquireExclusiveLock((T) NULL_KEY) : acquireSharedLock((T) NULL_KEY);
for (Comparable value : comparables)
locks[i++] = exclusiveMode ? acquireExclusiveLock((T) value) : acquireSharedLock((T) value);
return locks;
}
}