/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2006-2010 Sun Microsystems, Inc. * Portions Copyright 2013-2015 ForgeRock AS */ package org.opends.server.backends.task; import static org.opends.messages.BackendMessages.*; import static org.opends.server.config.ConfigConstants.*; import static org.opends.server.util.CollectionUtils.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; import java.io.File; import java.io.IOException; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.ResultCode; import org.opends.server.api.AlertGenerator; import org.opends.server.api.DirectoryThread; import org.opends.server.core.DirectoryServer; import org.opends.server.core.SearchOperation; import org.opends.server.core.ServerContext; import org.opends.server.types.*; import org.opends.server.types.LockManager.DNLock; import org.opends.server.util.LDIFException; import org.opends.server.util.LDIFReader; import org.opends.server.util.LDIFWriter; import org.opends.server.util.TimeThread; /** * This class defines a task scheduler for the Directory Server that will * control the execution of scheduled tasks and other administrative functions * that need to occur on a regular basis. */ public class TaskScheduler extends DirectoryThread implements AlertGenerator { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** * The fully-qualified name of this class. */ private static final String CLASS_NAME = "org.opends.server.backends.task.TaskScheduler"; /** * The maximum length of time in milliseconds to sleep between iterations * through the scheduler loop. */ private static long MAX_SLEEP_TIME = 5000; /** Indicates whether the scheduler is currently running. */ private boolean isRunning; /** Indicates whether a request has been received to stop the scheduler. */ private boolean stopRequested; /** The entry that serves as the immediate parent for recurring tasks. */ private Entry recurringTaskParentEntry; /** The entry that serves as the immediate parent for scheduled tasks. */ private Entry scheduledTaskParentEntry; /** The top-level entry at the root of the task tree. */ private Entry taskRootEntry; /** The set of recurring tasks defined in the server. */ private final HashMap<String,RecurringTask> recurringTasks = new HashMap<>(); /** The set of tasks associated with this scheduler. */ private final HashMap<String,Task> tasks = new HashMap<>(); /** The set of worker threads that are actively busy processing tasks. */ private final HashMap<String,TaskThread> activeThreads = new HashMap<>(); /** The thread ID for the next task thread to be created;. */ private int nextThreadID = 1; /** The set of worker threads that may be used to process tasks. */ private final LinkedList<TaskThread> idleThreads = new LinkedList<>(); /** The lock used to provide threadsafe access to the scheduler. */ private final ReentrantLock schedulerLock = new ReentrantLock(); /** The task backend with which this scheduler is associated. */ private TaskBackend taskBackend; /** The thread being used to actually run the scheduler. */ private Thread schedulerThread; /** The set of recently-completed tasks that need to be retained. */ private final TreeSet<Task> completedTasks = new TreeSet<>(); /** The set of tasks that have been scheduled but not yet arrived. */ private final TreeSet<Task> pendingTasks = new TreeSet<>(); /** The set of tasks that are currently running. */ private final TreeSet<Task> runningTasks = new TreeSet<>(); private ServerContext serverContext; /** * Creates a new task scheduler that will be used to ensure that tasks are * invoked at the appropriate times. * @param serverContext * The server context * @param taskBackend The task backend with which this scheduler is * associated. * * @throws InitializationException If a problem occurs while initializing * the scheduler from the backing file. */ public TaskScheduler(ServerContext serverContext, TaskBackend taskBackend) throws InitializationException { super("Task Scheduler Thread"); this.serverContext = serverContext; this.taskBackend = taskBackend; DirectoryServer.registerAlertGenerator(this); initializeTasksFromBackingFile(); for (RecurringTask recurringTask : recurringTasks.values()) { Task task = null; try { task = recurringTask.scheduleNextIteration(new GregorianCalendar()); } catch (DirectoryException de) { logger.error(de.getMessageObject()); } if (task != null) { try { scheduleTask(task, false); } catch (DirectoryException de) { // This task might have been already scheduled from before // and thus got initialized from backing file, otherwise // log error and continue. if (de.getResultCode() != ResultCode.ENTRY_ALREADY_EXISTS) { logger.error(de.getMessageObject()); } } } } } /** * Adds a recurring task to the scheduler, optionally scheduling the first * iteration for processing. * * @param recurringTask The recurring task to add to the scheduler. * @param scheduleIteration Indicates whether to schedule an iteration of * the recurring task. * * @throws DirectoryException If a problem occurs while trying to add the * recurring task (e.g., there's already another * recurring task defined with the same ID). */ public void addRecurringTask(RecurringTask recurringTask, boolean scheduleIteration) throws DirectoryException { schedulerLock.lock(); try { String id = recurringTask.getRecurringTaskID(); if (recurringTasks.containsKey(id)) { LocalizableMessage message = ERR_TASKSCHED_DUPLICATE_RECURRING_ID.get(id); throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, message); } Attribute attr = Attributes.create(ATTR_TASK_STATE, TaskState.RECURRING.toString()); Entry recurringTaskEntry = recurringTask.getRecurringTaskEntry(); recurringTaskEntry.putAttribute(attr.getAttributeType(), newArrayList(attr)); if (scheduleIteration) { Task task = recurringTask.scheduleNextIteration( new GregorianCalendar()); if (task != null) { // If there is an existing task with the same id // and it is in completed state, take its place. Task t = tasks.get(task.getTaskID()); if (t != null && TaskState.isDone(t.getTaskState())) { removeCompletedTask(t.getTaskID()); } scheduleTask(task, false); } } recurringTasks.put(id, recurringTask); writeState(); } finally { schedulerLock.unlock(); } } /** * Removes the recurring task with the given ID. * * @param recurringTaskID The ID of the recurring task to remove. * * @return The recurring task that was removed, or <CODE>null</CODE> if there * was no such recurring task. * * @throws DirectoryException If there is currently a pending or running * iteration of the associated recurring task. */ public RecurringTask removeRecurringTask(String recurringTaskID) throws DirectoryException { schedulerLock.lock(); try { RecurringTask recurringTask = recurringTasks.remove(recurringTaskID); HashMap<String,Task> iterationsMap = new HashMap<>(); for (Task t : tasks.values()) { // Find any existing task iterations and try to cancel them. if (t.getRecurringTaskID() != null && t.getRecurringTaskID().equals(recurringTaskID)) { TaskState state = t.getTaskState(); if (!TaskState.isDone(state) && !TaskState.isCancelled(state)) { cancelTask(t.getTaskID()); } iterationsMap.put(t.getTaskID(), t); } } // Remove any completed task iterations. for (Map.Entry<String,Task> iterationEntry : iterationsMap.entrySet()) { if (TaskState.isDone(iterationEntry.getValue().getTaskState())) { removeCompletedTask(iterationEntry.getKey()); } } writeState(); return recurringTask; } finally { schedulerLock.unlock(); } } /** * Schedules the provided task for execution. If the scheduler is active and * the start time has arrived, then the task will begin execution immediately. * Otherwise, it will be placed in the pending queue to be started at the * appropriate time. * * @param task The task to be scheduled. * @param writeState Indicates whether the current state information for * the scheduler should be persisted to disk once the * task is scheduled. * * @throws DirectoryException If a problem occurs while trying to schedule * the task (e.g., there's already another task * defined with the same ID). */ public void scheduleTask(Task task, boolean writeState) throws DirectoryException { schedulerLock.lock(); try { String id = task.getTaskID(); if (tasks.containsKey(id)) { LocalizableMessage message = ERR_TASKSCHED_DUPLICATE_TASK_ID.get(id); throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, message); } for (String dependencyID : task.getDependencyIDs()) { Task t = tasks.get(dependencyID); if (t == null) { LocalizableMessage message = ERR_TASKSCHED_DEPENDENCY_MISSING.get(id, dependencyID); throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message); } } tasks.put(id, task); TaskState state = shouldStart(task); task.setTaskState(state); if (state == TaskState.RUNNING) { TaskThread taskThread; if (idleThreads.isEmpty()) { taskThread = new TaskThread(this, nextThreadID++); taskThread.start(); } else { taskThread = idleThreads.removeFirst(); } runningTasks.add(task); activeThreads.put(task.getTaskID(), taskThread); taskThread.setTask(task); } else if (TaskState.isDone(state)) { if (state == TaskState.CANCELED_BEFORE_STARTING && task.isRecurring()) { pendingTasks.add(task); } else { completedTasks.add(task); } } else { pendingTasks.add(task); } if (writeState) { writeState(); } } finally { schedulerLock.unlock(); } } /** * Attempts to cancel the task with the given task ID. This will only cancel * the task if it has not yet started running. If it has started, then it * will not be interrupted. * * @param taskID The task ID of the task to cancel. * * @return The requested task, which may or may not have actually been * cancelled (the task state should make it possible to determine * whether it was cancelled), or <CODE>null</CODE> if there is no * such task. */ public Task cancelTask(String taskID) { schedulerLock.lock(); try { Task t = tasks.get(taskID); if (t == null) { return null; } if (TaskState.isPending(t.getTaskState())) { pendingTasks.remove(t); t.setTaskState(TaskState.CANCELED_BEFORE_STARTING); addCompletedTask(t); writeState(); } return t; } finally { schedulerLock.unlock(); } } /** * Removes the specified pending task. It will be completely removed rather * than moving it to the set of completed tasks. * * @param taskID The task ID of the pending task to remove. * * @return The task that was removed. * * @throws DirectoryException If the requested task is not in the pending * queue. */ public Task removePendingTask(String taskID) throws DirectoryException { schedulerLock.lock(); try { Task t = tasks.get(taskID); if (t == null) { LocalizableMessage message = ERR_TASKSCHED_REMOVE_PENDING_NO_SUCH_TASK.get(taskID); throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message); } if (TaskState.isPending(t.getTaskState())) { tasks.remove(taskID); pendingTasks.remove(t); writeState(); return t; } else { LocalizableMessage message = ERR_TASKSCHED_REMOVE_PENDING_NOT_PENDING.get(taskID); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } } finally { schedulerLock.unlock(); } } /** * Removes the specified completed task. * * @param taskID The task ID of the completed task to remove. * * @return The task that was removed. * * @throws DirectoryException If the requested task could not be found. */ public Task removeCompletedTask(String taskID) throws DirectoryException { schedulerLock.lock(); try { Iterator<Task> iterator = completedTasks.iterator(); while (iterator.hasNext()) { Task t = iterator.next(); if (t.getTaskID().equals(taskID)) { iterator.remove(); tasks.remove(taskID); writeState(); return t; } } LocalizableMessage message = ERR_TASKSCHED_REMOVE_COMPLETED_NO_SUCH_TASK.get(taskID); throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message); } finally { schedulerLock.unlock(); } } /** * Indicates that processing has completed on the provided task thread and * that it is now available for processing other tasks. The thread may be * immediately used for processing another task if appropriate. * * @param taskThread The thread that has completed processing on its * previously-assigned task. * @param completedTask The task for which processing has been completed. * @param taskState The task state for this completed task. * * @return <CODE>true</CODE> if the thread should continue running and * wait for the next task to process, or <CODE>false</CODE> if it * should exit immediately. */ public boolean threadDone(TaskThread taskThread, Task completedTask, TaskState taskState) { schedulerLock.lock(); try { completedTask.setCompletionTime(TimeThread.getTime()); completedTask.setTaskState(taskState); addCompletedTask(completedTask); try { completedTask.sendNotificationEMailMessage(); } catch (Exception e) { logger.traceException(e); } String taskID = completedTask.getTaskID(); if (activeThreads.remove(taskID) == null) { return false; } // See if the task is part of a recurring task. // If so, then schedule the next iteration. scheduleNextRecurringTaskIteration(completedTask, new GregorianCalendar()); writeState(); if (isRunning) { idleThreads.add(taskThread); return true; } else { return false; } } finally { schedulerLock.unlock(); } } /** * Check if a given task is a recurring task iteration and re-schedule it. * @param completedTask The task for which processing has been completed. * @param calendar The calendar date and time to schedule from. */ protected void scheduleNextRecurringTaskIteration(Task completedTask, GregorianCalendar calendar) { String recurringTaskID = completedTask.getRecurringTaskID(); if (recurringTaskID != null) { RecurringTask recurringTask = recurringTasks.get(recurringTaskID); if (recurringTask != null) { Task newIteration = null; try { newIteration = recurringTask.scheduleNextIteration(calendar); } catch (DirectoryException de) { logger.error(de.getMessageObject()); } if (newIteration != null) { try { // If there is an existing task with the same id // and it is in completed state, take its place. Task t = tasks.get(newIteration.getTaskID()); if (t != null && TaskState.isDone(t.getTaskState())) { removeCompletedTask(t.getTaskID()); } scheduleTask(newIteration, false); } catch (DirectoryException de) { // This task might have been already scheduled from before // and thus got initialized from backing file, otherwise // log error and continue. if (de.getResultCode() != ResultCode.ENTRY_ALREADY_EXISTS) { logger.traceException(de); LocalizableMessage message = ERR_TASKSCHED_ERROR_SCHEDULING_RECURRING_ITERATION.get( recurringTaskID, de.getMessageObject()); logger.error(message); DirectoryServer.sendAlertNotification(this, ALERT_TYPE_CANNOT_SCHEDULE_RECURRING_ITERATION, message); } } } } } } /** * Adds the provided task to the set of completed tasks associated with the * scheduler. It will be automatically removed after the appropriate * retention time has elapsed. * * @param completedTask The task for which processing has completed. */ public void addCompletedTask(Task completedTask) { // The scheduler lock is reentrant, so even if we already hold it, we can // acquire it again. schedulerLock.lock(); try { completedTasks.add(completedTask); runningTasks.remove(completedTask); // If the task never ran set its completion // time here explicitly so that it can be // correctly evaluated for retention later. if (completedTask.getCompletionTime() == -1) { completedTask.setCompletionTime(TimeThread.getTime()); } } finally { schedulerLock.unlock(); } } /** * Stops the scheduler so that it will not start any scheduled tasks. It will * not attempt to interrupt any tasks that are already running. Note that * once the scheduler has been stopped, it cannot be restarted and it will be * necessary to restart the task backend to start a new scheduler instance. */ public void stopScheduler() { stopRequested = true; try { schedulerThread.interrupt(); } catch (Exception e) { logger.traceException(e); } try { schedulerThread.join(); } catch (Exception e) { logger.traceException(e); } pendingTasks.clear(); runningTasks.clear(); completedTasks.clear(); tasks.clear(); for (TaskThread thread : idleThreads) { LocalizableMessage message = INFO_TASKBE_INTERRUPTED_BY_SHUTDOWN.get(); thread.interruptTask(TaskState.STOPPED_BY_SHUTDOWN, message, true); } } /** * Attempts to interrupt any tasks that are actively running. This will not * make any attempt to stop the scheduler. * * @param interruptState The state that should be assigned to the tasks if * they are successfully interrupted. * @param interruptReason A message indicating the reason that the tasks * are to be interrupted. * @param waitForStop Indicates whether this method should wait until * all active tasks have stopped before returning. */ public void interruptRunningTasks(TaskState interruptState, LocalizableMessage interruptReason, boolean waitForStop) { // Grab a copy of the running threads so that we can operate on them without // holding the lock. LinkedList<TaskThread> threadList = new LinkedList<>(); schedulerLock.lock(); try { threadList.addAll(activeThreads.values()); } finally { schedulerLock.unlock(); } // Iterate through all the task threads and request that they stop // processing. for (TaskThread t : threadList) { try { t.interruptTask(interruptState, interruptReason, true); } catch (Exception e) { logger.traceException(e); } } // If we should actually wait for all the task threads to stop, then do so. if (waitForStop) { for (TaskThread t : threadList) { try { t.join(); } catch (Exception e) { logger.traceException(e); } } } } /** * Operates in a loop, launching tasks at the appropriate time and performing * any necessary periodic cleanup. */ @Override public void run() { isRunning = true; schedulerThread = currentThread(); try { while (! stopRequested) { schedulerLock.lock(); boolean writeState = false; long sleepTime = MAX_SLEEP_TIME; try { // If there are any pending tasks that need to be started, then do so // now. Iterator<Task> iterator = pendingTasks.iterator(); while (iterator.hasNext()) { Task t = iterator.next(); TaskState state = shouldStart(t); if (state == TaskState.RUNNING) { TaskThread taskThread; if (idleThreads.isEmpty()) { taskThread = new TaskThread(this, nextThreadID++); taskThread.start(); } else { taskThread = idleThreads.removeFirst(); } runningTasks.add(t); activeThreads.put(t.getTaskID(), taskThread); taskThread.setTask(t); iterator.remove(); writeState = true; } else if (state == TaskState.WAITING_ON_START_TIME) { // If we're waiting for the start time to arrive, then see if that // will come before the next sleep time is up. long waitTime = t.getScheduledStartTime() - TimeThread.getTime(); sleepTime = Math.min(sleepTime, waitTime); } // Recurring task iteration has to spawn the next one // even if the current iteration has been canceled. else if (state == TaskState.CANCELED_BEFORE_STARTING && t.isRecurring()) { if (t.getScheduledStartTime() > TimeThread.getTime()) { // If we're waiting for the start time to arrive, // then see if that will come before the next sleep time is up. long waitTime = t.getScheduledStartTime() - TimeThread.getTime(); sleepTime = Math.min(sleepTime, waitTime); } else { TaskThread taskThread; if (idleThreads.isEmpty()) { taskThread = new TaskThread(this, nextThreadID++); taskThread.start(); } else { taskThread = idleThreads.removeFirst(); } runningTasks.add(t); activeThreads.put(t.getTaskID(), taskThread); taskThread.setTask(t); } } if (state != t.getTaskState()) { t.setTaskState(state); writeState = true; } } // Clean up any completed tasks that have been around long enough. long retentionTimeMillis = TimeUnit.SECONDS.toMillis(taskBackend.getRetentionTime()); long oldestRetainedCompletionTime = TimeThread.getTime() - retentionTimeMillis; iterator = completedTasks.iterator(); while (iterator.hasNext()) { Task t = iterator.next(); if (t.getCompletionTime() < oldestRetainedCompletionTime) { iterator.remove(); tasks.remove(t.getTaskID()); writeState = true; } } // If anything changed, then make sure that the on-disk state gets // updated. if (writeState) { writeState(); } } finally { schedulerLock.unlock(); } try { if (sleepTime > 0) { Thread.sleep(sleepTime); } } catch (InterruptedException ie) {} } } finally { isRunning = false; } } /** * Determines whether the specified task should start running. This is based * on the start time, the set of dependencies, and whether or not the * scheduler is active. Note that the caller to this method must hold the * scheduler lock. * * @param task The task for which to make the determination. * * @return The task state that should be used for the task. It should be * RUNNING if the task should be started, or some other state if not. */ private TaskState shouldStart(Task task) { // If the task has finished we don't want to restart it TaskState state = task.getTaskState(); // Reset task state if recurring. if (state == TaskState.RECURRING) { state = null; } if (state != null && TaskState.isDone(state)) { return state; } if (! isRunning) { return TaskState.UNSCHEDULED; } if (task.getScheduledStartTime() > TimeThread.getTime()) { return TaskState.WAITING_ON_START_TIME; } LinkedList<String> dependencyIDs = task.getDependencyIDs(); if (dependencyIDs != null) { for (String dependencyID : dependencyIDs) { Task t = tasks.get(dependencyID); if (t != null) { TaskState tState = t.getTaskState(); if (!TaskState.isDone(tState)) { return TaskState.WAITING_ON_DEPENDENCY; } if (!TaskState.isSuccessful(tState)) { FailedDependencyAction action = task.getFailedDependencyAction(); switch (action) { case CANCEL: cancelTask(task.getTaskID()); return task.getTaskState(); case DISABLE: task.setTaskState(TaskState.DISABLED); return task.getTaskState(); default: break; } } } } } return TaskState.RUNNING; } /** * Populates the scheduler with information read from the task backing file. * If no backing file is found, then create a new one. The caller must * already hold the scheduler lock or otherwise ensure that this is a * threadsafe operation. * * @throws InitializationException If a fatal error occurs while attempting * to perform the initialization. */ private void initializeTasksFromBackingFile() throws InitializationException { String backingFilePath = taskBackend.getTaskBackingFile(); try { File backingFile = getFileForPath(backingFilePath); if (! backingFile.exists()) { createNewTaskBackingFile(); return; } LDIFImportConfig importConfig = new LDIFImportConfig(backingFilePath); LDIFReader ldifReader = new LDIFReader(importConfig); taskRootEntry = null; recurringTaskParentEntry = null; scheduledTaskParentEntry = null; while (true) { Entry entry; try { entry = ldifReader.readEntry(); } catch (LDIFException le) { logger.traceException(le); if (le.canContinueReading()) { logger.error(ERR_TASKSCHED_CANNOT_PARSE_ENTRY_RECOVERABLE, backingFilePath, le.getLineNumber(), le.getMessage()); continue; } else { try { ldifReader.close(); } catch (Exception e) { logger.traceException(e); } LocalizableMessage message = ERR_TASKSCHED_CANNOT_PARSE_ENTRY_FATAL.get( backingFilePath, le.getLineNumber(), le.getMessage()); throw new InitializationException(message); } } if (entry == null) { break; } DN entryDN = entry.getName(); if (entryDN.equals(taskBackend.getTaskRootDN())) { taskRootEntry = entry; } else if (entryDN.equals(taskBackend.getRecurringTasksParentDN())) { recurringTaskParentEntry = entry; } else if (entryDN.equals(taskBackend.getScheduledTasksParentDN())) { scheduledTaskParentEntry = entry; } else { DN parentDN = entryDN.getParentDNInSuffix(); if (parentDN == null) { logger.error(ERR_TASKSCHED_ENTRY_HAS_NO_PARENT, entryDN, taskBackend.getTaskRootDN()); } else if (parentDN.equals(taskBackend.getScheduledTasksParentDN())) { try { Task task = entryToScheduledTask(entry, null); if (TaskState.isDone(task.getTaskState())) { String id = task.getTaskID(); if (tasks.containsKey(id)) { logger.warn(WARN_TASKSCHED_DUPLICATE_TASK_ID, id); } else { completedTasks.add(task); tasks.put(id, task); } } else { scheduleTask(task, false); } } catch (DirectoryException de) { logger.traceException(de); logger.error(ERR_TASKSCHED_CANNOT_SCHEDULE_TASK_FROM_ENTRY, entryDN, de.getMessageObject()); } } else if (parentDN.equals(taskBackend.getRecurringTasksParentDN())) { try { RecurringTask recurringTask = entryToRecurringTask(entry); addRecurringTask(recurringTask, false); } catch (DirectoryException de) { logger.traceException(de); logger.error(ERR_TASKSCHED_CANNOT_SCHEDULE_RECURRING_TASK_FROM_ENTRY, entryDN, de.getMessageObject()); } } else { logger.error(ERR_TASKSCHED_INVALID_TASK_ENTRY_DN, entryDN, backingFilePath); } } } ldifReader.close(); } catch (IOException ioe) { logger.traceException(ioe); LocalizableMessage message = ERR_TASKSCHED_ERROR_READING_TASK_BACKING_FILE.get( backingFilePath, stackTraceToSingleLineString(ioe)); throw new InitializationException(message, ioe); } } /** * Creates a new task backing file that contains only the basic structure but * no scheduled or recurring task entries. The caller must already hold the * scheduler lock or otherwise ensure that this is a threadsafe operation. * * @throws InitializationException If a problem occurs while attempting to * create the backing file. */ private void createNewTaskBackingFile() throws InitializationException { String backingFile = taskBackend.getTaskBackingFile(); LDIFExportConfig exportConfig = new LDIFExportConfig(backingFile, ExistingFileBehavior.OVERWRITE); try { LDIFWriter writer = new LDIFWriter(exportConfig); // First, write a header to the top of the file to indicate that it should // not be manually edited. writer.writeComment(INFO_TASKBE_BACKING_FILE_HEADER.get(), 80); // Next, create the required hierarchical entries and add them to the // LDIF. taskRootEntry = createEntry(taskBackend.getTaskRootDN()); writer.writeEntry(taskRootEntry); scheduledTaskParentEntry = createEntry(taskBackend.getScheduledTasksParentDN()); writer.writeEntry(scheduledTaskParentEntry); recurringTaskParentEntry = createEntry(taskBackend.getRecurringTasksParentDN()); writer.writeEntry(recurringTaskParentEntry); // Close the file and we're done. writer.close(); } catch (IOException ioe) { logger.traceException(ioe); LocalizableMessage message = ERR_TASKSCHED_CANNOT_CREATE_BACKING_FILE.get( backingFile, stackTraceToSingleLineString(ioe)); throw new InitializationException(message, ioe); } catch (LDIFException le) { logger.traceException(le); LocalizableMessage message = ERR_TASKSCHED_CANNOT_CREATE_BACKING_FILE.get( backingFile, le.getMessage()); throw new InitializationException(message, le); } } /** * Writes state information about all tasks and recurring tasks to disk. */ public void writeState() { String backingFilePath = taskBackend.getTaskBackingFile(); String tmpFilePath = backingFilePath + ".tmp"; LDIFExportConfig exportConfig = new LDIFExportConfig(tmpFilePath, ExistingFileBehavior.OVERWRITE); schedulerLock.lock(); try { LDIFWriter writer = new LDIFWriter(exportConfig); // First, write a header to the top of the file to indicate that it should // not be manually edited. writer.writeComment(INFO_TASKBE_BACKING_FILE_HEADER.get(), 80); // Next, write the structural entries to the top of the LDIF. writer.writeEntry(taskRootEntry); writer.writeEntry(scheduledTaskParentEntry); writer.writeEntry(recurringTaskParentEntry); // Iterate through all the recurring tasks and write them to LDIF. for (RecurringTask recurringTask : recurringTasks.values()) { writer.writeEntry(recurringTask.getRecurringTaskEntry()); } // Iterate through all the scheduled tasks and write them to LDIF. for (Task task : tasks.values()) { writer.writeEntry(task.getTaskEntry()); } // Close the file. writer.close(); // See if there is a ".save" file. If so, then delete it. File saveFile = getFileForPath(backingFilePath + ".save"); try { if (saveFile.exists()) { saveFile.delete(); } } catch (Exception e) { logger.traceException(e); } // If there is an existing backing file, then rename it to ".save". File backingFile = getFileForPath(backingFilePath); try { if (backingFile.exists()) { backingFile.renameTo(saveFile); } } catch (Exception e) { logger.traceException(e); LocalizableMessage message = WARN_TASKSCHED_CANNOT_RENAME_CURRENT_BACKING_FILE.get( backingFilePath, saveFile.getAbsolutePath(), stackTraceToSingleLineString(e)); logger.warn(message); DirectoryServer.sendAlertNotification( this, ALERT_TYPE_CANNOT_RENAME_CURRENT_TASK_FILE, message); } // Rename the ".tmp" file into place. File tmpFile = getFileForPath(tmpFilePath); try { tmpFile.renameTo(backingFile); } catch (Exception e) { logger.traceException(e); LocalizableMessage message = ERR_TASKSCHED_CANNOT_RENAME_NEW_BACKING_FILE.get( tmpFilePath, backingFilePath, stackTraceToSingleLineString(e)); logger.error(message); DirectoryServer.sendAlertNotification( this, ALERT_TYPE_CANNOT_RENAME_NEW_TASK_FILE, message); } } catch (LDIFException le) { logger.traceException(le); LocalizableMessage message = ERR_TASKSCHED_CANNOT_WRITE_BACKING_FILE.get(tmpFilePath, le .getMessage()); logger.error(message); DirectoryServer.sendAlertNotification(this, ALERT_TYPE_CANNOT_WRITE_TASK_FILE, message); } catch (Exception e) { logger.traceException(e); LocalizableMessage message = ERR_TASKSCHED_CANNOT_WRITE_BACKING_FILE.get(tmpFilePath, stackTraceToSingleLineString(e)); logger.error(message); DirectoryServer.sendAlertNotification(this, ALERT_TYPE_CANNOT_WRITE_TASK_FILE, message); } finally { schedulerLock.unlock(); } } /** * Retrieves the total number of entries in the task backend. * * @return The total number of entries in the task backend. */ public long getEntryCount() { schedulerLock.lock(); try { return tasks.size() + recurringTasks.size() + 3; } finally { schedulerLock.unlock(); } } /** * Retrieves the number of scheduled tasks in the task backend. * * @return The total number of entries in the task backend. */ public long getScheduledTaskCount() { schedulerLock.lock(); try { return tasks.size(); } finally { schedulerLock.unlock(); } } /** * Retrieves the number of recurring tasks in the task backend. * * @return The total number of entries in the task backend. */ public long getRecurringTaskCount() { schedulerLock.lock(); try { return recurringTasks.size(); } finally { schedulerLock.unlock(); } } /** * Retrieves the task backend with which this scheduler is associated. * * @return The task backend with which this scheduler is associated. */ public TaskBackend getTaskBackend() { return taskBackend; } /** * Retrieves the root entry that is the common ancestor for all entries in the * task backend. * * @return The root entry that is the common ancestor for all entries in the * task backend. */ public Entry getTaskRootEntry() { return taskRootEntry.duplicate(true); } /** * Retrieves the entry that is the immediate parent for all scheduled task * entries in the task backend. * * @return The entry that is the immediate parent for all scheduled task * entries in the task backend. */ public Entry getScheduledTaskParentEntry() { return scheduledTaskParentEntry.duplicate(true); } /** * Retrieves the entry that is the immediate parent for all recurring task * entries in the task backend. * * @return The entry that is the immediate parent for all recurring task * entries in the task backend. */ public Entry getRecurringTaskParentEntry() { return recurringTaskParentEntry.duplicate(true); } /** * Retrieves the scheduled task with the given task ID. * * @param taskID The task ID for the scheduled task to retrieve. * * @return The requested scheduled task, or <CODE>null</CODE> if there is no * such task. */ public Task getScheduledTask(String taskID) { schedulerLock.lock(); try { return tasks.get(taskID); } finally { schedulerLock.unlock(); } } /** * Retrieves the scheduled task created from the specified entry. * * @param taskEntryDN The DN of the task configuration entry associated * with the task to retrieve. * * @return The requested scheduled task, or <CODE>null</CODE> if there is no * such task. */ public Task getScheduledTask(DN taskEntryDN) { schedulerLock.lock(); try { for (Task t : tasks.values()) { if (taskEntryDN.equals(t.getTaskEntry().getName())) { return t; } } return null; } finally { schedulerLock.unlock(); } } /** * Indicates whether the current thread already holds a lock on the scheduler. * * @return {@code true} if the current thread holds the scheduler lock, or * {@code false} if not. */ boolean holdsSchedulerLock() { return schedulerLock.isHeldByCurrentThread(); } /** * Attempts to acquire a write lock on the specified entry, trying as many * times as necessary until the lock has been acquired. * * @param entryDN The DN of the entry for which to acquire the write lock. * * @return The write lock that has been acquired for the entry. */ DNLock writeLockEntry(DN entryDN) { DNLock lock = null; while (lock == null) { lock = DirectoryServer.getLockManager().tryWriteLockEntry(entryDN); } return lock; } /** * Attempts to acquire a read lock on the specified entry. * * @param entryDN The DN of the entry for which to acquire the read lock. * * @return The read lock that has been acquired for the entry. * * @throws DirectoryException If the read lock cannot be acquired. */ DNLock readLockEntry(DN entryDN) throws DirectoryException { final DNLock lock = DirectoryServer.getLockManager().tryReadLockEntry(entryDN); if (lock != null) { return lock; } throw new DirectoryException(ResultCode.BUSY, ERR_BACKEND_CANNOT_LOCK_ENTRY.get(entryDN)); } /** * Retrieves the scheduled task entry with the provided DN. The caller should * hold a read lock on the target entry. * * @param scheduledTaskEntryDN The entry DN that indicates which scheduled * task entry to retrieve. * * @return The scheduled task entry with the provided DN, or * <CODE>null</CODE> if no scheduled task has the provided DN. */ public Entry getScheduledTaskEntry(DN scheduledTaskEntryDN) { schedulerLock.lock(); try { for (Task task : tasks.values()) { Entry taskEntry = task.getTaskEntry(); if (scheduledTaskEntryDN.equals(taskEntry.getName())) { return taskEntry.duplicate(true); } } return null; } finally { schedulerLock.unlock(); } } /** * Compares the filter in the provided search operation against each of the * task entries, returning any that match. Note that only the search filter * will be used -- the base and scope will be ignored, so the caller must * ensure that they are correct for scheduled tasks. * * @param searchOperation The search operation to use when performing the * search. * * @return <CODE>true</CODE> if processing should continue on the search * operation, or <CODE>false</CODE> if it should not for some reason * (e.g., a size or time limit was reached). * * @throws DirectoryException If a problem occurs while processing the * search operation against the scheduled tasks. */ public boolean searchScheduledTasks(SearchOperation searchOperation) throws DirectoryException { SearchFilter filter = searchOperation.getFilter(); schedulerLock.lock(); try { for (Task t : tasks.values()) { DN taskEntryDN = t.getTaskEntryDN(); DNLock lock = readLockEntry(taskEntryDN); try { Entry e = t.getTaskEntry().duplicate(true); if (filter.matchesEntry(e) && !searchOperation.returnEntry(e, null)) { return false; } } finally { lock.unlock(); } } return true; } finally { schedulerLock.unlock(); } } /** * Retrieves the recurring task with the given recurring task ID. * * @param recurringTaskID The recurring task ID for the recurring task to * retrieve. * * @return The requested recurring task, or <CODE>null</CODE> if there is no * such recurring task. */ public RecurringTask getRecurringTask(String recurringTaskID) { schedulerLock.lock(); try { return recurringTasks.get(recurringTaskID); } finally { schedulerLock.unlock(); } } /** * Retrieves the recurring task with the given recurring task ID. * * @param recurringTaskEntryDN The recurring task ID for the recurring task * to retrieve. * * @return The requested recurring task, or <CODE>null</CODE> if there is no * such recurring task. */ public RecurringTask getRecurringTask(DN recurringTaskEntryDN) { schedulerLock.lock(); try { for (RecurringTask rt : recurringTasks.values()) { if (recurringTaskEntryDN.equals(rt.getRecurringTaskEntry().getName())) { return rt; } } return null; } finally { schedulerLock.unlock(); } } /** * Retrieves the recurring task entry with the provided DN. The caller should * hold a read lock on the target entry. * * @param recurringTaskEntryDN The entry DN that indicates which recurring * task entry to retrieve. * * @return The recurring task entry with the provided DN, or * <CODE>null</CODE> if no recurring task has the provided DN. */ public Entry getRecurringTaskEntry(DN recurringTaskEntryDN) { schedulerLock.lock(); try { for (RecurringTask recurringTask : recurringTasks.values()) { Entry recurringTaskEntry = recurringTask.getRecurringTaskEntry(); if (recurringTaskEntryDN.equals(recurringTaskEntry.getName())) { return recurringTaskEntry.duplicate(true); } } return null; } finally { schedulerLock.unlock(); } } /** * Compares the filter in the provided search operation against each of the * recurring task entries, returning any that match. Note that only the * search filter will be used -- the base and scope will be ignored, so the * caller must ensure that they are correct for recurring tasks. * * @param searchOperation The search operation to use when performing the * search. * * @return <CODE>true</CODE> if processing should continue on the search * operation, or <CODE>false</CODE> if it should not for some reason * (e.g., a size or time limit was reached). * * @throws DirectoryException If a problem occurs while processing the * search operation against the recurring tasks. */ public boolean searchRecurringTasks(SearchOperation searchOperation) throws DirectoryException { SearchFilter filter = searchOperation.getFilter(); schedulerLock.lock(); try { for (RecurringTask rt : recurringTasks.values()) { DN recurringTaskEntryDN = rt.getRecurringTaskEntryDN(); DNLock lock = readLockEntry(recurringTaskEntryDN); try { Entry e = rt.getRecurringTaskEntry().duplicate(true); if (filter.matchesEntry(e) && ! searchOperation.returnEntry(e, null)) { return false; } } finally { lock.unlock(); } } return true; } finally { schedulerLock.unlock(); } } /** * Decodes the contents of the provided entry as a scheduled task. The * resulting task will not actually be scheduled for processing. * * @param entry The entry to decode as a scheduled task. * @param operation The operation used to create this task in the server, or * {@code null} if the operation is not available. * * @return The scheduled task decoded from the provided entry. * * @throws DirectoryException If the provided entry cannot be decoded as a * scheduled task. */ public Task entryToScheduledTask(Entry entry, Operation operation) throws DirectoryException { // Get the name of the class that implements the task logic. AttributeType attrType = DirectoryServer.getAttributeTypeOrDefault(ATTR_TASK_CLASS.toLowerCase(), ATTR_TASK_CLASS); List<Attribute> attrList = entry.getAttribute(attrType); if (attrList == null || attrList.isEmpty()) { LocalizableMessage message = ERR_TASKSCHED_NO_CLASS_ATTRIBUTE.get(ATTR_TASK_ID); throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); } if (attrList.size() > 1) { LocalizableMessage message = ERR_TASKSCHED_MULTIPLE_CLASS_TYPES.get(ATTR_TASK_ID); throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); } Attribute attr = attrList.get(0); if (attr.isEmpty()) { LocalizableMessage message = ERR_TASKSCHED_NO_CLASS_VALUES.get(ATTR_TASK_ID); throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); } Iterator<ByteString> iterator = attr.iterator(); ByteString value = iterator.next(); if (iterator.hasNext()) { LocalizableMessage message = ERR_TASKSCHED_MULTIPLE_CLASS_VALUES.get(ATTR_TASK_ID); throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, message); } // Try to load the specified class. String taskClassName = value.toString(); Class<?> taskClass; try { taskClass = DirectoryServer.loadClass(taskClassName); } catch (Exception e) { logger.traceException(e); LocalizableMessage message = ERR_TASKSCHED_CANNOT_LOAD_CLASS. get(taskClassName, ATTR_TASK_CLASS, stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } // Instantiate the class as a task. Task task; try { task = (Task) taskClass.newInstance(); } catch (Exception e) { logger.traceException(e); LocalizableMessage message = ERR_TASKSCHED_CANNOT_INSTANTIATE_CLASS_AS_TASK.get( taskClassName, Task.class.getName()); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } // Perform the necessary internal and external initialization for the task. try { task.initializeTaskInternal(serverContext, this, entry); } catch (InitializationException ie) { logger.traceException(ie); LocalizableMessage message = ERR_TASKSCHED_CANNOT_INITIALIZE_INTERNAL.get( taskClassName, ie.getMessage()); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } catch (Exception e) { LocalizableMessage message = ERR_TASKSCHED_CANNOT_INITIALIZE_INTERNAL.get( taskClassName, stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } if (!TaskState.isDone(task.getTaskState()) && !DirectoryServer.getAllowedTasks().contains(taskClassName)) { LocalizableMessage message = ERR_TASKSCHED_NOT_ALLOWED_TASK.get(taskClassName); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } task.setOperation(operation); // Avoid task specific initialization for completed tasks. if (!TaskState.isDone(task.getTaskState())) { task.initializeTask(); } task.setOperation(null); return task; } /** * Decodes the contents of the provided entry as a recurring task. The * resulting recurring task will not actually be added to the scheduler. * * @param entry The entry to decode as a recurring task. * * @return The recurring task decoded from the provided entry. * * @throws DirectoryException If the provided entry cannot be decoded as a * recurring task. */ public RecurringTask entryToRecurringTask(Entry entry) throws DirectoryException { return new RecurringTask(serverContext, this, entry); } /** * Retrieves the DN of the configuration entry with which this alert generator * is associated. * * @return The DN of the configuration entry with which this alert generator * is associated. */ @Override public DN getComponentEntryDN() { return taskBackend.getConfigEntryDN(); } /** * Retrieves the fully-qualified name of the Java class for this alert * generator implementation. * * @return The fully-qualified name of the Java class for this alert * generator implementation. */ @Override public String getClassName() { return CLASS_NAME; } /** * Retrieves information about the set of alerts that this generator may * produce. The map returned should be between the notification type for a * particular notification and the human-readable description for that * notification. This alert generator must not generate any alerts with types * that are not contained in this list. * * @return Information about the set of alerts that this generator may * produce. */ @Override public LinkedHashMap<String,String> getAlerts() { LinkedHashMap<String, String> alerts = new LinkedHashMap<>(); alerts.put(ALERT_TYPE_CANNOT_SCHEDULE_RECURRING_ITERATION, ALERT_DESCRIPTION_CANNOT_SCHEDULE_RECURRING_ITERATION); alerts.put(ALERT_TYPE_CANNOT_RENAME_CURRENT_TASK_FILE, ALERT_DESCRIPTION_CANNOT_RENAME_CURRENT_TASK_FILE); alerts.put(ALERT_TYPE_CANNOT_RENAME_NEW_TASK_FILE, ALERT_DESCRIPTION_CANNOT_RENAME_NEW_TASK_FILE); alerts.put(ALERT_TYPE_CANNOT_WRITE_TASK_FILE, ALERT_DESCRIPTION_CANNOT_WRITE_TASK_FILE); return alerts; } }