package org.ovirt.engine.core.bll.tasks;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.ovirt.engine.core.bll.CommandBase;
import org.ovirt.engine.core.bll.CommandsFactory;
import org.ovirt.engine.core.bll.aaa.SsoSessionUtils;
import org.ovirt.engine.core.bll.context.CommandContext;
import org.ovirt.engine.core.bll.context.EngineContext;
import org.ovirt.engine.core.bll.job.ExecutionContext;
import org.ovirt.engine.core.bll.tasks.interfaces.CommandContextsCache;
import org.ovirt.engine.core.common.businessentities.AsyncTask;
import org.ovirt.engine.core.common.businessentities.CommandAssociatedEntity;
import org.ovirt.engine.core.common.businessentities.CommandEntity;
import org.ovirt.engine.core.common.config.Config;
import org.ovirt.engine.core.common.config.ConfigValues;
import org.ovirt.engine.core.compat.CommandStatus;
import org.ovirt.engine.core.compat.DateTime;
import org.ovirt.engine.core.compat.Guid;
import org.ovirt.engine.core.dao.AsyncTaskDao;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class CommandsRepository {
private static final Logger log = LoggerFactory.getLogger(CommandsRepository.class);
private final ConcurrentMap<Guid, CallbackTiming> callbacksTiming;
private final CommandsCache commandsCache;
private final CommandContextsCache contextsCache;
private final ConcurrentHashMap<Guid, List<Guid>> childHierarchy;
private final ConcurrentMap<Guid, CoCoEventSubscriber> subscriptions;
private Integer pollingRate = Config.<Integer>getValue(ConfigValues.AsyncCommandPollingLoopInSeconds);
private final Object LOCK;
private volatile boolean childHierarchyInitialized;
@Inject
private AsyncTaskDao asyncTaskDao;
@Inject
public CommandsRepository(CommandsCache commandsCache, CommandContextsCache contextsCache) {
this.commandsCache = commandsCache;
this.contextsCache = contextsCache;
callbacksTiming = new ConcurrentHashMap<>();
childHierarchy = new ConcurrentHashMap<>();
subscriptions = new ConcurrentHashMap<>();
LOCK = new Object();
}
public void addToCallbackMap(CommandEntity cmdEntity) {
if (!callbacksTiming.containsKey(cmdEntity.getId())) {
CommandBase<?> cmd = retrieveCommand(cmdEntity.getId());
if (cmd != null && cmd.getCallback() != null) {
CallbackTiming callbackTiming = new CallbackTiming(cmd.getCallback(), pollingRate);
if (cmdEntity.isWaitingForEvent()) {
long waitOnEventEndTime = System.currentTimeMillis()
+ TimeUnit.MINUTES.toMillis(Config.<Integer>getValue(ConfigValues.CoCoWaitForEventInMinutes));
callbackTiming.setWaitOnEventEndTime(waitOnEventEndTime);
}
addToCallbackMap(cmdEntity.getId(), callbackTiming);
}
}
}
/**
* This method is responsible to update the statuses of all the commands that ACTIVE and persisted
* but aren't managed to status that'll reflect that their execution was ended with failure.
*/
@PostConstruct
private void handleUnmanagedCommands() {
List<AsyncTask> asyncTasks = asyncTaskDao.getAll();
Set<Guid> asyncTaskManagerManagedCommands = asyncTasks.stream().filter(x -> x.getVdsmTaskId() != null)
.map(x -> x.getRootCommandId()).collect(Collectors.toSet());
asyncTaskManagerManagedCommands.addAll(asyncTasks.stream().filter(x -> x.getVdsmTaskId() != null)
.map(x -> x.getCommandId()).collect(Collectors.toSet()));
// this will update all the commands that aren't managed by callback/async task manager and are active
// and will set their status to ENDED_WITH_FAILURE.
getCommands(false).stream()
.filter(x -> !x.isCallbackEnabled())
.filter(x -> x.getCommandStatus() == CommandStatus.ACTIVE)
.filter(x -> !asyncTaskManagerManagedCommands.contains(x.getId()))
.forEach(x -> commandsCache.updateCommandStatus(x.getId(), CommandStatus.ENDED_WITH_FAILURE));
// active commands managed by callbacks and not managed by async task manager need to reacquire locks
// on engine restart
getCommands(false).stream()
.filter(x -> x.isCallbackEnabled())
.filter(x -> !x.isCallbackNotified())
.filter(x -> x.getCommandStatus().isDuringExecution())
.filter(x -> !asyncTaskManagerManagedCommands.contains(x.getId()))
.map(x -> retrieveCommand(x.getId()))
.filter(Objects::nonNull)
.forEach(CommandBase::reacquireLocks);
}
public void addToCallbackMap(Guid commandId, CallbackTiming callbackTiming) {
callbacksTiming.put(commandId, callbackTiming);
}
public void persistCommand(CommandEntity cmdEntity, CommandContext cmdContext) {
initChildHierarchy();
if (Guid.isNullOrEmpty(cmdEntity.getId())) {
return;
}
persistCommand(cmdEntity);
if (cmdContext != null) {
contextsCache.put(cmdEntity.getId(), cmdContext);
}
}
public void persistCommand(CommandEntity cmdEntity) {
if (Guid.isNullOrEmpty(cmdEntity.getId())) {
return;
}
CommandEntity existingCmdEntity = commandsCache.get(cmdEntity.getId());
if (existingCmdEntity != null) {
cmdEntity.setExecuted(existingCmdEntity.isExecuted());
cmdEntity.setCallbackNotified(existingCmdEntity.isCallbackNotified());
}
commandsCache.put(cmdEntity);
// check if callback is enabled or if parent command has callback enabled
if (cmdEntity.isCallbackEnabled() ||
(!Guid.isNullOrEmpty(cmdEntity.getParentCommandId()) &&
commandsCache.get(cmdEntity.getParentCommandId()) != null &&
commandsCache.get(cmdEntity.getParentCommandId()).isCallbackEnabled()
)) {
buildCmdHierarchy(cmdEntity);
if (!cmdEntity.isCallbackNotified()) {
addToCallbackMap(cmdEntity);
}
}
}
public CommandContext retrieveCommandContext(Guid cmdId) {
return contextsCache.get(cmdId);
}
public CommandBase<?> retrieveCommand(Guid commandId) {
return retrieveCommand(commandsCache.get(commandId), retrieveCommandContext(commandId));
}
public void updateCommandStatus(final Guid commandId, final CommandStatus status) {
commandsCache.updateCommandStatus(commandId, status);
}
private CommandBase<?> retrieveCommand(CommandEntity cmdEntity, CommandContext cmdContext) {
CommandBase<?> command = null;
if (cmdEntity != null) {
if (cmdContext == null) {
cmdContext = new CommandContext(new EngineContext()).withExecutionContext(new ExecutionContext());
}
command = CommandsFactory.createCommand(cmdEntity.getCommandType(), cmdEntity.getCommandParameters(), cmdContext);
if (command != null) {
command.setCommandStatus(cmdEntity.getCommandStatus(), false);
command.setCommandData(cmdEntity.getData());
command.setReturnValue(cmdEntity.getReturnValue());
if (!Guid.isNullOrEmpty(cmdEntity.getParentCommandId()) &&
!cmdEntity.getParentCommandId().equals(cmdEntity.getId()) &&
command.getParameters().getParentParameters() == null) {
CommandBase<?> parentCommand = retrieveCommand(cmdEntity.getParentCommandId());
if (parentCommand != null) {
command.getParameters().setParentParameters(parentCommand.getParameters());
}
}
}
}
return command;
}
public CommandStatus getCommandStatus(final Guid commandId) {
CommandEntity cmdEntity = commandsCache.get(commandId);
if (cmdEntity != null) {
return cmdEntity.getCommandStatus();
}
return CommandStatus.UNKNOWN;
}
public CommandEntity getCommandEntity(Guid commandId) {
return Guid.isNullOrEmpty(commandId) ? null : commandsCache.get(commandId);
}
public void updateCallbackNotified(final Guid commandId) {
commandsCache.updateCallbackNotified(commandId);
}
/**
* @param onlyWithCallbackEnabled Specifies if the returned commands' callbacks are enabled or not.
* @return Returns commands with callback enabled or disabled, based on given parameter
*/
public List<CommandEntity> getCommands(boolean onlyWithCallbackEnabled) {
List<CommandEntity> cmdEntities = new ArrayList<>();
CommandEntity cmdEntity;
for (Guid cmdId : commandsCache.keySet()) {
cmdEntity = commandsCache.get(cmdId);
if (!onlyWithCallbackEnabled || commandsCache.get(cmdId).isCallbackEnabled()) {
cmdEntities.add(cmdEntity);
}
}
return cmdEntities;
}
public void removeCommand(Guid commandId) {
commandsCache.remove(commandId);
contextsCache.remove(commandId);
updateCmdHierarchy(commandId);
}
private void initChildHierarchy() {
if (!childHierarchyInitialized) {
synchronized(LOCK) {
if (!childHierarchyInitialized) {
childHierarchy.clear();
getCommands(false).forEach(this::buildCmdHierarchy);
}
childHierarchyInitialized = true;
}
}
}
private void buildCmdHierarchy(CommandEntity cmdEntity) {
if (!Guid.isNullOrEmpty(cmdEntity.getParentCommandId()) && !cmdEntity.getId().equals(cmdEntity.getParentCommandId())) {
childHierarchy.putIfAbsent(cmdEntity.getParentCommandId(), new ArrayList<>());
if (!childHierarchy.get(cmdEntity.getParentCommandId()).contains(cmdEntity.getId())) {
childHierarchy.get(cmdEntity.getParentCommandId()).add(cmdEntity.getId());
}
}
}
private void updateCmdHierarchy(Guid cmdId) {
for (List<Guid> childIds : childHierarchy.values()) {
if (childIds.contains(cmdId)) {
childIds.remove(cmdId);
break;
}
}
if (childHierarchy.containsKey(cmdId) && childHierarchy.get(cmdId).size() == 0) {
childHierarchy.remove(cmdId);
}
}
public void removeAllCommandsBeforeDate(final DateTime cutoff) {
commandsCache.removeAllCommandsBeforeDate(cutoff);
synchronized(LOCK) {
childHierarchyInitialized = false;
}
}
public List<Guid> getChildCommandIds(Guid cmdId) {
initChildHierarchy();
if (childHierarchy.containsKey(cmdId)) {
return childHierarchy.get(cmdId);
}
return Collections.emptyList();
}
public ConcurrentMap<Guid, CallbackTiming> getCallbacksTiming() {
return callbacksTiming;
}
public void persistCommandAssociatedEntities(Collection<CommandAssociatedEntity> cmdAssociatedEntities) {
commandsCache.persistCommandAssociatedEntities(cmdAssociatedEntities);
}
public List<CommandEntity> getChildCmdsByParentCmdId(Guid cmdId) {
return commandsCache.getChildCmdsByParentCmdId(cmdId);
}
public List<Guid> getCommandIdsByEntityId(Guid entityId) {
return commandsCache.getCommandIdsByEntityId(entityId);
}
public List<Guid> getCommandIdsBySessionSeqId(long engineSessionSeqId) {
List<Guid> cmdIds = new ArrayList<>();
CommandEntity cmdEntity;
for (Guid cmdId : commandsCache.keySet()) {
cmdEntity = commandsCache.get(cmdId);
if (cmdEntity != null && cmdEntity.getEngineSessionSeqId() != SsoSessionUtils.EMPTY_SESSION_SEQ_ID &&
cmdEntity.getEngineSessionSeqId() == engineSessionSeqId) {
cmdIds.add(cmdId);
}
}
return cmdIds;
}
public List<CommandAssociatedEntity> getCommandAssociatedEntities(Guid cmdId) {
return commandsCache.getCommandAssociatedEntities(cmdId);
}
public void updateCommandData(Guid commandId, Map<String, Serializable> data) {
commandsCache.updateCommandData(commandId, data);
}
public void updateCommandExecuted(Guid commandId) {
commandsCache.updateCommandExecuted(commandId);
}
public boolean hasCommandEntitiesWithRootCommandId(Guid rootCommandId) {
CommandEntity cmdEntity;
for (Guid cmdId : commandsCache.keySet()) {
cmdEntity = commandsCache.get(cmdId);
if (cmdEntity != null && !Guid.isNullOrEmpty(cmdEntity.getRootCommandId()) &&
!cmdEntity.getRootCommandId().equals(cmdId) &&
cmdEntity.getRootCommandId().equals(rootCommandId)) {
return true;
}
}
return false;
}
public CallbackTiming getCallbackTiming(Guid commandId) {
if (Guid.isNullOrEmpty(commandId)) {
return null;
}
return callbacksTiming.get(commandId);
}
public void markExpiredCommandsAsFailure() {
for (Guid commandId : callbacksTiming.keySet()) {
List<Guid> childCmdIds = getChildCommandIds(commandId);
if (childCmdIds.isEmpty()) {
markExpiredCommandAsFailure(commandId);
} else {
childCmdIds.forEach(this::markExpiredCommandAsFailure);
}
}
}
private void markExpiredCommandAsFailure(Guid cmdId) {
CommandEntity cmdEntity = getCommandEntity(cmdId);
if (cmdEntity != null && cmdEntity.getCommandStatus() == CommandStatus.ACTIVE) {
Calendar cal = Calendar.getInstance();
Integer cmdLifeTimeInMin = cmdEntity.getCommandParameters().getLifeInMinutes();
cal.add(Calendar.MINUTE, -1 * (cmdLifeTimeInMin == null ?
Config.<Integer>getValue(ConfigValues.CoCoLifeInMinutes) :
cmdLifeTimeInMin));
if (cmdEntity.getCreatedAt().getTime() < cal.getTime().getTime()) {
log.warn("Marking expired command as Failed: command '{} ({})' that started at '{}' has been marked as Failed.",
cmdEntity.getCommandType(),
cmdEntity.getId(),
cmdEntity.getCreatedAt());
updateCommandStatus(cmdId, CommandStatus.FAILED);
}
}
}
public void addEventSubscription(CommandEntity command, CoCoEventSubscriber subscription) {
subscriptions.putIfAbsent(command.getId(), subscription);
}
public void removeEventSubscription(Guid commandId) {
CoCoEventSubscriber subscriber = subscriptions.remove(commandId);
if (subscriber != null) {
subscriber.cancel();
}
}
}