package org.ovirt.engine.core.bll.lock;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.ConcurrencyManagement;
import javax.ejb.ConcurrencyManagementType;
import javax.ejb.Local;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import org.ovirt.engine.core.common.errors.EngineMessage;
import org.ovirt.engine.core.common.locks.LockInfo;
import org.ovirt.engine.core.common.utils.Pair;
import org.ovirt.engine.core.utils.lock.EngineLock;
import org.ovirt.engine.core.utils.lock.LockManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The following class an implementation of internal locking mechanism
*/
@Startup
@Singleton(name = "LockManager")
@ConcurrencyManagement(ConcurrencyManagementType.BEAN)
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
@Local(LockManager.class)
public class InMemoryLockManager implements LockManager, LockManagerMonitorMXBean {
private static final Pair<Boolean, Set<String>> LOCK_INSERT_SUCCESS_RESULT = new Pair<>(Boolean.TRUE, Collections.<String>emptySet());
/** A map which is contains all internal representation of locks **/
private final Map<String, InternalLockView> locks = new HashMap<>();
/** A lock which is used to synchronized acquireLock(), acquireLockWait() and releaseLock() operations **/
private final Lock globalLock = new ReentrantLock();
/** A condition which is used in order to notify for waiting threads that some lock was released**/
private final Condition releasedLock = globalLock.newCondition();
private MBeanServer platformMBeanServer;
private ObjectName objectName = null;
private static final Logger log = LoggerFactory.getLogger(InMemoryLockManager.class);
@PostConstruct
public void registerInJMX() {
try {
objectName = new ObjectName("InMemoryLockManager:type=" + this.getClass().getName());
platformMBeanServer = ManagementFactory.getPlatformMBeanServer();
platformMBeanServer.registerMBean(this, objectName);
} catch (Exception e) {
throw new IllegalStateException("Problem during registration of Monitoring into JMX:" + e);
}
}
@PreDestroy
public void unregisterFromJMX() {
try {
platformMBeanServer.unregisterMBean(this.objectName);
} catch (Exception e) {
throw new IllegalStateException("Problem during unregistration of Monitoring into JMX:" + e);
}
}
@Override
public Pair<Boolean, Set<String>> acquireLock(EngineLock lock) {
log.debug("Before acquiring lock '{}'", lock);
globalLock.lock();
try {
return acquireLockInternal(lock);
} finally {
globalLock.unlock();
}
}
@Override
public void acquireLockWait(EngineLock lock) {
log.debug("Before acquiring and wait lock '{}'", lock);
validateLockForAcquireAndWait(lock);
globalLock.lock();
try {
while (!acquireLockInternal(lock).getFirst()) {
log.info("Failed to acquire lock and wait lock '{}'", lock);
releasedLock.await();
}
} catch (InterruptedException ignore) {
} finally {
globalLock.unlock();
}
}
private void validateLockForAcquireAndWait(EngineLock lock) {
if (lock.getSharedLocks() != null && lock.getExclusiveLocks().size() > 1) {
log.error("Trying to acquire or wait on shared or more than one exclussive locks '{}'", lock);
throw new IllegalArgumentException("Trying to acquire or wait on shared or more than one exclussive locks");
}
}
@Override
public void releaseLock(EngineLock lock) {
log.debug("Before releasing a lock '{}'", lock);
globalLock.lock();
try {
if (lock.getSharedLocks() != null) {
for (Entry<String, Pair<String, String>> entry : lock.getSharedLocks().entrySet()) {
releaseSharedLock(buildHashMapKey(entry), entry.getValue().getSecond());
}
}
if (lock.getExclusiveLocks() != null) {
for (Entry<String, Pair<String, String>> entry : lock.getExclusiveLocks().entrySet()) {
releaseExclusiveLock(buildHashMapKey(entry));
}
}
releasedLock.signalAll();
} finally {
globalLock.unlock();
}
}
@Override
public void clear() {
log.warn("Cleaning all in memory locks");
globalLock.lock();
try {
locks.clear();
releasedLock.signalAll();
} finally {
globalLock.unlock();
}
}
@Override
public boolean releaseLock(String lockId) {
log.warn("The following lock is going to be released via external call, lockId '{}', error message can be"
+ " left for shared lock",
lockId);
globalLock.lock();
try {
InternalLockView lock = locks.get(lockId);
if (lock == null) {
log.warn("Lock with id '{}' does not exist and can not be released via external call", lockId);
return false;
}
if (lock.getExclusive()) {
releaseExclusiveLock(lockId);
} else {
releaseSharedLock(lockId, null);
}
releasedLock.signalAll();
} finally {
globalLock.unlock();
}
log.warn("Lock '{}' was released via external call", lockId);
return true;
}
@Override
public List<String> showAllLocks() {
log.debug("All in memory locks will be shown");
globalLock.lock();
try {
return locks.entrySet().stream().map(this::createLockDescription).collect(Collectors.toList());
} finally {
globalLock.unlock();
log.debug("All in memory locks were shown");
}
}
private String createLockDescription(Entry<String, InternalLockView> e) {
return new StringBuilder("The object id is : ")
.append(e.getKey())
.append(' ')
.append(e.getValue()).toString();
}
/**
* Internal method should build a key for lock
*/
private String buildHashMapKey(Entry<String, Pair<String, String>> entry) {
return entry.getKey() + entry.getValue().getFirst();
}
/**
* The following method contains a logic for acquiring a lock The method is contains two steps:
* 1. The lock can be acquired
* 2. If the first step successes acquire a lock
*/
private Pair<Boolean, Set<String>> acquireLockInternal(EngineLock lock) {
boolean checkOnly = true;
for (int i = 0; i < 2; i++) {
if (lock.getSharedLocks() != null) {
for (Entry<String, Pair<String, String>> entry : lock.getSharedLocks().entrySet()) {
Pair<Boolean, Set<String>> result =
insertSharedLock(buildHashMapKey(entry), entry.getValue().getSecond(), checkOnly);
if (!result.getFirst()) {
log.debug("Failed to acquire lock. Shared lock is taken for key '{}', value '{}'",
entry.getKey(),
entry.getValue().getFirst());
return result;
}
}
}
if (lock.getExclusiveLocks() != null) {
for (Entry<String, Pair<String, String>> entry : lock.getExclusiveLocks().entrySet()) {
Pair<Boolean, Set<String>> result =
insertExclusiveLock(buildHashMapKey(entry), entry.getValue().getSecond(), checkOnly);
if (!result.getFirst()) {
log.debug("Failed to acquire lock. Exclusive lock is taken for key '{}', value '{}'",
entry.getKey(),
entry.getValue().getFirst());
return result;
}
}
}
checkOnly = false;
}
log.debug("Success acquiring lock '{}' succeeded ", lock);
return LOCK_INSERT_SUCCESS_RESULT;
}
/**
* The following method should insert an "shared" internal lock
* @param message
* - error message associated with lock
* @param isCheckOnly
* - is insert or check if lock can be inserted
*/
private Pair<Boolean, Set<String>> insertSharedLock(String key, String message, boolean isCheckOnly) {
InternalLockView lock = locks.get(key);
if (lock != null) {
if (!isCheckOnly) {
lock.increaseCount();
lock.addMessage(message);
} else if (lock.getExclusive()) {
return new Pair<>(Boolean.FALSE, lock.getMessages());
}
} else if (!isCheckOnly) {
locks.put(key, new InternalLockView(1, message, false));
}
return LOCK_INSERT_SUCCESS_RESULT;
}
/**
* The following method will add exclusive lock, the exclusive key can be
* added only if there is not exist any shared or exclusive lock for given key
*/
private Pair<Boolean, Set<String>> insertExclusiveLock(String key, String message, boolean isCheckOnly) {
InternalLockView lock = locks.get(key);
if (lock != null) {
return new Pair<>(Boolean.FALSE, lock.getMessages());
}
if (!isCheckOnly) {
locks.put(key, new InternalLockView(0, message, true));
}
return LOCK_INSERT_SUCCESS_RESULT;
}
private void releaseExclusiveLock(String key) {
InternalLockView lock = locks.get(key);
if (lock != null && lock.getExclusive()) {
locks.remove(key);
log.debug("The exclusive lock for key '{}' is released and lock is removed from map", key);
} else if (lock == null) {
log.warn("Trying to release exclusive lock which does not exist, lock key: '{}'", key);
} else {
log.warn("Trying to release exclusive lock but lock is not exclusive. lock key: '{}'", key);
}
}
private void releaseSharedLock(String key, String message) {
InternalLockView lock = locks.get(key);
if (lock != null) {
if (lock.getCount() > 0) {
lock.decreaseCount();
log.debug("The shared lock for key '{}' is released.", key);
if (lock.getCount() == 0) {
locks.remove(key);
log.debug("The shared lock for key '{}' is removed from map", key);
} else {
lock.removeMessage(message);
}
} else {
log.warn("Trying to decrease a shared lock for key: '{}' , but shared index is 0", key);
}
} else {
log.warn("Trying to release a shared lock for key: '{}' , but lock does not exist", key);
}
}
@Override
public LockInfo getLockInfo(String key) {
InternalLockView internalLockView = locks.get(key);
if (internalLockView == null) {
return null;
}
Set<String> messages = internalLockView.getMessages();
messages.remove(EngineMessage.ACTION_TYPE_FAILED_OBJECT_LOCKED.name());
if (messages.isEmpty()) {
// EngineMessage.ACTION_TYPE_FAILED_OBJECT_LOCKED should only be used for
// short locks (locks for the execute phase) so we filter it and if no
// other lock exists, the entity should be displayed as unlocked
return null;
}
return new LockInfo(internalLockView.getExclusive(), messages);
}
/**
* The following class represents different locks which are kept inside InMemoryLockManager
*/
private static class InternalLockView {
/** Number for shared locks **/
private int count;
/** Indicate if the lock is exclusive and not allowing any other exclusive/shared locks with the same key **/
private final boolean exclusive;
/** Contains error messages for that key **/
private List<String> messages;
public InternalLockView(int count, String message, boolean exclusive) {
this.count = count;
this.exclusive = exclusive;
messages = new ArrayList<>();
messages.add(message);
}
public boolean getExclusive() {
return exclusive;
}
public int getCount() {
return count;
}
public void increaseCount() {
count++;
}
public void decreaseCount() {
count--;
}
public Set<String> getMessages() {
return new HashSet<>(messages);
}
public void addMessage(String message) {
messages.add(message);
}
public void removeMessage(String message) {
if (message != null) {
messages.remove(message);
}
}
@Override
public String toString() {
if(exclusive) {
return "The lock is exclusive";
}
return "The lock is shared and a number of shared locks is " + count;
}
}
}