package io.eguan.dtx; /* * #%L * Project eguan * %% * Copyright (C) 2012 - 2017 Oodrive * %% * 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. * #L% */ import io.eguan.dtx.DtxTaskApiAbstract.TaskKeeperParameters; import io.eguan.dtx.DtxTaskApiAbstract.TaskLoader; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Timer; import java.util.TimerTask; import java.util.TreeMap; import java.util.UUID; import java.util.WeakHashMap; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.annotation.Nonnull; import javax.annotation.concurrent.GuardedBy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Keeper of the {@link DtxTask}. Keeps the last known state and the extra datas of the {@link DtxTask}. * * @author oodrive * @author ebredzinski * @author llambert * @author pwehrle * */ final class TaskKeeper { private final class TaskPurger extends TimerTask { @Override public void run() { try { purgeTasks(); } catch (final Throwable t) { // Ignore LOGGER.warn("Purge has failed " + t); } } } /** * Extra datas of a {@link DtxTask}. Name, description, task information and the last status of the task. * * */ private static final class TaskExtraData { private String name; private String description; private UUID resourceId; private DtxTaskStatus lastStatus = DtxTaskStatus.UNKNOWN; private DtxTaskInfo taskInfo; private long timestamp = DtxConstants.DEFAULT_TIMESTAMP_VALUE; protected TaskExtraData(final long timestamp) { super(); this.timestamp = timestamp; } final String getName() { return name; } final void setName(final String name) { this.name = name; } final String getDescription() { return description; } final void setDescription(final String description) { this.description = description; } final UUID getResourceId() { return resourceId; } final void setResourceId(final UUID resourceId) { this.resourceId = resourceId; } final DtxTaskStatus getLastStatus() { return lastStatus; } final void setLastStatus(final DtxTaskStatus lastStatus) { this.lastStatus = lastStatus; } final DtxTaskInfo getTaskInfo() { return taskInfo; } final void setTaskInfo(final DtxTaskInfo taskInfo) { this.taskInfo = taskInfo; } final long getTimestamp() { return timestamp; } final void setTimestamp(final long timestamp) { this.timestamp = timestamp; } } private static final Logger LOGGER = LoggerFactory.getLogger(TaskKeeper.class); private static final DtxTaskAdm[] EMPTY_TASK_ADM_ARRAY = new DtxTaskAdm[0]; /** Extra data of known tasks. */ @GuardedBy("taskLock") private final Map<UUID, TaskExtraData> taskExtraDatas = new HashMap<>(); /** Transaction identifier of known tasks. */ @GuardedBy("taskLock") private final TreeMap<Long, UUID> transactionIds = new TreeMap<Long, UUID>(); /** Transaction identifier of unknown tasks. */ private final Map<UUID, TaskLoader> unknownTaskLoaders = new WeakHashMap<>(); private final TaskKeeperParameters parameters; private TaskPurger purger; private final Timer timer = new Timer("taskKeeper"); /** * Shared lock for task management with concurrent modification. */ private final ReentrantReadWriteLock taskLock = new ReentrantReadWriteLock(); /** * Constructs a new instance. */ TaskKeeper(final TaskKeeperParameters parameters) { super(); this.parameters = parameters; } /** * Get a {@link TaskLoader} from a given UUID * * @param taskId * the non-<code>null</code> target task's {@link UUID} */ private final TaskLoader getUnknownTaskLoader(final UUID taskUuid) { synchronized (unknownTaskLoaders) { return unknownTaskLoaders.get(taskUuid); } } /** * Set a new {@link TaskLoader} corresponding to a given task ID * * @param taskId * the non-<code>null</code> target task's {@link UUID} * @param task * the non-<code>null</code> {@link TaskLoader} */ private final TaskLoader setUnknownTaskLoader(final UUID taskUuid, final TaskLoader task) { synchronized (unknownTaskLoaders) { return unknownTaskLoaders.put(taskUuid, task); } } /** * Start the thread which purges the task cache. * * @throws: IllegalArgumentException - if delay < 0, or delay + System.currentTimeMillis() < 0, or period <= 0 */ final void startPurge() { LOGGER.debug("Start tasks purge with absolute duration: " + parameters.getAbsoluteDuration() + " absolute size: " + parameters.getAbsoluteSize() + " max duration: " + parameters.getMaxDuration() + " max size: " + parameters.getMaxSize() + " delay: " + parameters.getDelay() + " period: " + parameters.getPeriod()); // Check parameters if (parameters.getAbsoluteDuration() <= 0 || parameters.getAbsoluteSize() <= 0 || parameters.getMaxDuration() <= 0 || parameters.getMaxSize() <= 0) { throw new IllegalArgumentException("Task Keeper parameters values must be all positive"); } if (purger == null) { purger = new TaskPurger(); } try { timer.scheduleAtFixedRate(purger, parameters.getDelay(), parameters.getPeriod()); } catch (final IllegalStateException e) { LOGGER.warn("Purge already started"); } } /** * Stop the thread which purges the task cache. */ final void stopPurge() { if (purger != null) { LOGGER.debug("Stop purge"); purger.cancel(); purger = null; } } /** * Create a new object extraData. * * @param taskId * the non-<code>null</code> target task's {@link UUID} * @return {@link TaskExtraData} for that task. */ private final TaskExtraData createTaskExtraData(final UUID taskId) { TaskExtraData extraData = taskExtraDatas.get(taskId); if (extraData == null) { extraData = new TaskExtraData(System.currentTimeMillis()); taskExtraDatas.put(taskId, extraData); } return extraData; } /** * Create or update a task depending if already exists or not. * * @param taskId * the non-<code>null</code> target task's {@link UUID} * @param txId * the transaction identifier corresponding to the task * @param resourceId * the non-<code>null</code> resource ID to set * @param status * the non-<code>null</code> task status * @param dtxTaskInfo * the information on the task. */ final void setTask(@Nonnull final UUID taskId, final long txId, @Nonnull final UUID resourceId, @Nonnull final DtxTaskStatus status, final DtxTaskInfo dtxTaskInfo) { taskLock.writeLock().lock(); try { final TaskExtraData extraData = createTaskExtraData(Objects.requireNonNull(taskId)); extraData.setResourceId(Objects.requireNonNull(resourceId)); extraData.setLastStatus(Objects.requireNonNull(status)); if (dtxTaskInfo != null) { extraData.setTaskInfo(dtxTaskInfo); } if (txId != DtxConstants.DEFAULT_LAST_TX_VALUE) { setTaskTransactionId(Objects.requireNonNull(taskId), txId); } } finally { taskLock.writeLock().unlock(); } } /** * Purge the oldest tasks, depending on taskKeeper parameters. * * The task Keeper keep only {@link TaskKeeperParameters#getMaxSize()} done tasks with a duration higher than * {@link TaskKeeperParameters#getMaxDuration()}. Only the done tasks with a duration smaller than * {@link TaskKeeperParameters#getAbsoluteDuration()} are kept. And only * {@link TaskKeeperParameters#getAbsoluteSize()} done tasks are kept. The task keeper keep all the not done tasks. * */ private final void purgeTasks() { LOGGER.debug("purge tasks"); taskLock.writeLock().lock(); try { final long currentTimestamp = System.currentTimeMillis(); int countTotalDoneTasks = 0; int countOlderDoneTasks = 0; Long firstOlderTxId = null; final Iterator<Entry<Long, UUID>> txIdIterator = transactionIds.descendingMap().entrySet().iterator(); while (txIdIterator.hasNext()) { final Entry<Long, UUID> entry = txIdIterator.next(); final TaskExtraData extraData = taskExtraDatas.get(entry.getValue()); // Task not ended must not be purged. Ignore it. if (!extraData.lastStatus.isDone()) { continue; } else { final long taskDuration = currentTimestamp - extraData.getTimestamp(); // If the task duration is higher than absolute duration, the followers will be too. if (taskDuration > parameters.getAbsoluteDuration()) { firstOlderTxId = entry.getKey(); break; } else { // Check if the absolute size has not been achieved if (countTotalDoneTasks < parameters.getAbsoluteSize()) { countTotalDoneTasks++; // Check if the max size with max duration has not been achieved. if (taskDuration > parameters.getMaxDuration()) { if (countOlderDoneTasks < parameters.getMaxSize()) { countOlderDoneTasks++; } else { firstOlderTxId = entry.getKey(); break; } } } else { firstOlderTxId = entry.getKey(); break; } } } } // Delete the entry up to this last entry if (firstOlderTxId != null) { final Iterator<Entry<Long, UUID>> iterator = transactionIds.headMap(firstOlderTxId).entrySet() .iterator(); while (iterator.hasNext()) { final Entry<Long, UUID> entry = iterator.next(); final TaskExtraData extraData = taskExtraDatas.get(entry.getValue()); if (!extraData.lastStatus.isDone()) { continue; } else { // remove entry from taskExtraData map taskExtraDatas.remove(entry.getValue()); // remove entry from transactionIds map iterator.remove(); } } // remove the firstOlderTxId ; taskExtraDatas.remove(transactionIds.get(firstOlderTxId)); transactionIds.remove(firstOlderTxId); } } finally { taskLock.writeLock().unlock(); } } /** * Return the total number of tasks with a done status. * */ private final long getTotalDoneTasks() { long count = 0; for (final Entry<UUID, TaskExtraData> task : taskExtraDatas.entrySet()) { if (task.getValue().getLastStatus().isDone()) count++; } return count; } /** * Return the number of tasks with a done status and a duration higher than max duration. * */ private final long getDoneTasksWithMaxDuration(final long currentTimestamp) { long count = 0; for (final Entry<UUID, TaskExtraData> task : taskExtraDatas.entrySet()) { if (task.getValue().getLastStatus().isDone() && (currentTimestamp - task.getValue().getTimestamp()) > parameters.getMaxDuration()) { count++; } } return count; } /** * Load or not a task. Remove the oldest task if necessary, depending on taskKeeper parameters * * The task Keeper keep only {@link TaskKeeperParameters#getMaxSize()} done tasks with a duration higher than * {@link TaskKeeperParameters#getMaxDuration()}. Only the done tasks with a duration smaller than * {@link TaskKeeperParameters#getAbsoluteDuration()} are kept. And only * {@link TaskKeeperParameters#getAbsoluteSize()} done tasks are kept. The task keeper keep all the not done tasks. * * @param taskId * the non-<code>null</code> target task's {@link UUID} * @param txId * the transaction identifier corresponding to the task * @param resourceId * the non-<code>null</code> resource ID to set * @param status * the non-<code>null</code> task status * @param dtxTaskInfo * the information on the task. */ final void loadTask(@Nonnull final UUID taskId, final long txId, @Nonnull final UUID resourceId, @Nonnull final DtxTaskStatus status, final DtxTaskInfo dtxTaskInfo, final long timestamp) { LOGGER.debug("load task: " + taskId + " timestamp: " + timestamp); taskLock.writeLock().lock(); try { final long currentTimestamp = System.currentTimeMillis(); long duration = 0; boolean removeBeforeAdd = false; long countTotalDoneTasks = getTotalDoneTasks(); long countDoneTasksWithMaxDuration = getDoneTasksWithMaxDuration(currentTimestamp); // Check if timestamp is valid. if (timestamp <= 0) { throw new IllegalArgumentException("Invalid timestamp"); } // If status is not done add it directly. if (status.isDone()) { duration = currentTimestamp - timestamp; // If the duration is outside the absolute duration, do not load it. if (duration > parameters.getAbsoluteDuration()) { return; } // Check if the task is outside the absolute size. if (countTotalDoneTasks < parameters.getAbsoluteSize()) { countTotalDoneTasks++; } else { // Absolute size is achieved, the older task must be removed. removeBeforeAdd = true; } // The task is outside the max duration. if (duration > parameters.getMaxDuration()) { if (countDoneTasksWithMaxDuration < parameters.getMaxSize()) { countDoneTasksWithMaxDuration++; } else { // The max Size limit is exceeded, the older task must be removed. removeBeforeAdd = true; } } // Remove first done entry. if (removeBeforeAdd) { final Iterator<Entry<Long, UUID>> txIdIterator = transactionIds.entrySet().iterator(); while (txIdIterator.hasNext()) { final Entry<Long, UUID> entry = txIdIterator.next(); final TaskExtraData extraData = taskExtraDatas.get(entry.getValue()); // Skip task with not done status. if (!extraData.lastStatus.isDone()) { continue; } else { if (timestamp > extraData.getTimestamp()) { taskExtraDatas.remove(entry.getValue()); txIdIterator.remove(); break; } else { // the task is older than the oldest of the task keeper, do not load it return; } } } } } // Load the new task. final TaskExtraData extraData = createTaskExtraData(Objects.requireNonNull(taskId)); extraData.setResourceId(Objects.requireNonNull(resourceId)); extraData.setLastStatus(Objects.requireNonNull(status)); if (dtxTaskInfo != null) { extraData.setTaskInfo(dtxTaskInfo); } // Update the default timestamp with the new one. extraData.setTimestamp(timestamp); if (txId != DtxConstants.DEFAULT_LAST_TX_VALUE) { setTaskTransactionId(Objects.requireNonNull(taskId), txId); } } finally { taskLock.writeLock().unlock(); } } /** * Set the name and the description of the task. One or both may be <code>null</code>. * * @param taskId * the non-<code>null</code> target task's {@link UUID} * @param name * the optional name * @param description * the optional description */ final void setTaskReadableId(@Nonnull final UUID taskId, final String name, final String description) { taskLock.writeLock().lock(); try { final TaskExtraData extraData = createTaskExtraData(Objects.requireNonNull(taskId)); extraData.setName(name); extraData.setDescription(description); } finally { taskLock.writeLock().unlock(); } } /** * Set the transaction ID corresponding to the task ID * * @param taskId * the non-<code>null</code> target task's {@link UUID} * @param txId * the transaction ID to set */ final void setTaskTransactionId(@Nonnull final UUID taskId, final long txId) { taskLock.writeLock().lock(); try { if (txId != DtxConstants.DEFAULT_LAST_TX_VALUE) { TaskExtraData extraData = taskExtraDatas.get(taskId); if (extraData == null) { extraData = new TaskExtraData(System.currentTimeMillis()); taskExtraDatas.put(taskId, extraData); } transactionIds.put(Long.valueOf(txId), Objects.requireNonNull(taskId)); } } finally { taskLock.writeLock().unlock(); } } /** * Set the status. * * @param taskId * the non-<code>null</code> target task's {@link UUID} * @param status * the status to set */ final void setTaskStatus(@Nonnull final UUID taskId, @Nonnull final DtxTaskStatus status) { taskLock.writeLock().lock(); try { final TaskExtraData extraData = createTaskExtraData(Objects.requireNonNull(taskId)); extraData.setLastStatus(Objects.requireNonNull(status)); } finally { taskLock.writeLock().unlock(); } } /** * Set the status. * * @param taskId * the non-<code>null</code> target task's {@link UUID} * @param status * the status to set */ final void setTaskStatus(final long transactionId, @Nonnull final DtxTaskStatus status) { taskLock.writeLock().lock(); try { final UUID taskId = transactionIds.get(Long.valueOf(transactionId)); if (taskId != null) { final TaskExtraData extraData = createTaskExtraData(Objects.requireNonNull(taskId)); extraData.setLastStatus(Objects.requireNonNull(status)); } else { LOGGER.info("Task can not be found in cache"); } } finally { taskLock.writeLock().unlock(); } } /** * Sets the dtx task info. * * @param taskId * the non-<code>null</code> target task's {@link UUID} * @param taskInfo * some data coded in a {@link DtxTaskInfo}. */ final void setDtxTaskInfo(@Nonnull final UUID taskId, final DtxTaskInfo taskInfo) { taskLock.writeLock().lock(); try { final TaskExtraData extraData = createTaskExtraData(Objects.requireNonNull(taskId)); extraData.setTaskInfo(taskInfo); } finally { taskLock.writeLock(); } } /** * Return true if dtxTaskInfo is already set for a given task ID. * * @param taskId * the non-<code>null</code> target task's {@link UUID} * @return true is the dtxTaskInfo is not null, false otherwise. */ final boolean isDtxTaskInfoSet(final UUID taskId) { // Search the task in the current hashmap. taskLock.readLock().lock(); try { final TaskExtraData extraData = taskExtraDatas.get(taskId); if (extraData != null) { return (extraData.getTaskInfo() != null); } else { return false; } } finally { taskLock.readLock().unlock(); } } /** * Gets the dtx task info. * * @param taskId * the target task's {@link UUID} * @return the DtxTaskInfo object, null if the task does not exist or its info does not exist. * */ final DtxTaskInfo getDtxTaskInfo(final UUID taskId, final DtxTaskApiAbstract dtxTaskApiAbstract) { // Search the task in the current hashmap. taskLock.readLock().lock(); try { final TaskExtraData extraData = taskExtraDatas.get(taskId); if (extraData != null) { return extraData.getTaskInfo(); } } finally { taskLock.readLock().unlock(); } // Search the task in the unknown tasks map. TaskLoader task = getUnknownTaskLoader(taskId); if (task != null) { return task.getInfo(); } if (dtxTaskApiAbstract == null) { return null; } else { // Search the task in the journal. task = dtxTaskApiAbstract.readTask(taskId); setUnknownTaskLoader(taskId, task); return task.getInfo(); } } /** * Gets the dtx task timestamp. * * @param taskId * the target task's {@link UUID} * @return the timestamp * */ final long getTaskTimeStamp(final UUID taskId) { taskLock.readLock().lock(); try { final TaskExtraData extraData = taskExtraDatas.get(taskId); if (extraData == null) { return DtxConstants.DEFAULT_TIMESTAMP_VALUE; } else { return extraData.getTimestamp(); } } finally { taskLock.readLock().unlock(); } } /** * Create the {@link DtxTaskAdm} corresponding to the given task ID. * * @param taskId * id of the task * @return the immutable task object. */ final DtxTaskAdm getDtxTask(final UUID taskId, final DtxTaskApiAbstract dtxTaskApiAbstract) { taskLock.readLock().lock(); try { final TaskExtraData extraData = taskExtraDatas.get(taskId); // If the task is not found, return an empty task with status UNKNOWN if (extraData != null) { return new DtxTaskAdm(taskId, extraData.getName(), extraData.getDescription(), extraData.getResourceId(), extraData.getLastStatus()); } } finally { taskLock.readLock().unlock(); } // Search the task in the unknown tasks map. TaskLoader task = getUnknownTaskLoader(taskId); if (task != null) { return task.getDtxTaskAdm(); } if (dtxTaskApiAbstract != null) { // Search the task in the journal. task = dtxTaskApiAbstract.readTask(taskId); setUnknownTaskLoader(taskId, task); synchronized (task) { return task.getDtxTaskAdm(); } } else { return new DtxTaskAdm(taskId, null, null, null, DtxTaskStatus.UNKNOWN); } } /** * Gets the list of known tasks. * * @param dtxTaskApi * task API to be able to get the status of tasks. * @return the current status of every task. */ final DtxTaskAdm[] getTasks() { taskLock.readLock().lock(); try { final DtxTaskAdm[] result = new DtxTaskAdm[taskExtraDatas.size()]; int i = 0; for (final Entry<UUID, TaskExtraData> task : taskExtraDatas.entrySet()) { final UUID taskId = task.getKey(); final TaskExtraData extraData = task.getValue(); result[i++] = new DtxTaskAdm(taskId, extraData.getName(), extraData.getDescription(), extraData.getResourceId(), extraData.getLastStatus()); } return result; } finally { taskLock.readLock().unlock(); } } /** * Gets the list of known tasks for a given resource manager. * * @param dtxTaskApi * task API to be able to get the status of tasks. * @param resourceId * id of the resource manager. * @return the current status of every task. */ final DtxTaskAdm[] getResourceManagerTasks(final UUID resourceId) { taskLock.readLock().lock(); try { final ArrayList<DtxTaskAdm> resultList = new ArrayList<DtxTaskAdm>(taskExtraDatas.size()); for (final Entry<UUID, TaskExtraData> task : taskExtraDatas.entrySet()) { final UUID taskId = task.getKey(); final TaskExtraData extraData = task.getValue(); final UUID id = extraData.getResourceId(); if (id.equals(resourceId)) { resultList.add(new DtxTaskAdm(taskId, extraData.getName(), extraData.getDescription(), extraData .getResourceId(), extraData.getLastStatus())); } } return resultList.toArray(EMPTY_TASK_ADM_ARRAY); } finally { taskLock.readLock().unlock(); } } }