/**
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.scheduler.timer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.Timer;
import java.util.TreeMap;
import java.util.WeakHashMap;
import org.openmrs.api.APIException;
import org.openmrs.api.impl.BaseOpenmrsService;
import org.openmrs.scheduler.SchedulerConstants;
import org.openmrs.scheduler.SchedulerException;
import org.openmrs.scheduler.SchedulerService;
import org.openmrs.scheduler.SchedulerUtil;
import org.openmrs.scheduler.Task;
import org.openmrs.scheduler.TaskDefinition;
import org.openmrs.scheduler.TaskFactory;
import org.openmrs.scheduler.db.SchedulerDAO;
import org.openmrs.util.OpenmrsMemento;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.orm.ObjectRetrievalFailureException;
import org.springframework.transaction.annotation.Transactional;
/**
* Simple scheduler service that uses JDK timer to trigger and execute scheduled tasks.
*/
@Transactional
public class TimerSchedulerServiceImpl extends BaseOpenmrsService implements SchedulerService {
/**
* Logger
*/
private Logger log = LoggerFactory.getLogger(getClass());
/**
* Registered task list
*/
private Set<TaskDefinition> registeredTasks = new HashSet<TaskDefinition>();
/**
* Scheduled Task Map
*/
private static Map<Integer, TimerSchedulerTask> scheduledTasks = new WeakHashMap<Integer, TimerSchedulerTask>();
/**
* A single timer used to keep track of all scheduled tasks. The Timer's associated thread
* should run as a daemon. A deamon thread is called for if the timer will be used to schedule
* repeating "maintenance activities", which must be performed as long as the application is
* running, but should not prolong the lifetime of the application.
*
* @see java.util.Timer#Timer(boolean)
*/
private Map<TaskDefinition, Timer> taskDefinitionTimerMap = new HashMap<TaskDefinition, Timer>();
/**
* Global data access object context
*/
private SchedulerDAO schedulerDAO;
/**
* Gets the scheduler data access object.
*/
public SchedulerDAO getSchedulerDAO() {
return this.schedulerDAO;
}
/**
* Sets the scheduler data access object.
*/
public void setSchedulerDAO(SchedulerDAO dao) {
this.schedulerDAO = dao;
}
/**
* Start up hook for the scheduler and all of its scheduled tasks.
*/
@Override
public void onStartup() {
log.debug("Starting scheduler service ...");
// Get all of the tasks in the database
Collection<TaskDefinition> taskDefinitions = getSchedulerDAO().getTasks();
// Iterate through the tasks and start them if their startOnStartup flag is true
if (taskDefinitions != null) {
for (TaskDefinition taskDefinition : taskDefinitions) {
try {
// If the task is configured to start on startup, we schedule it to run
// Otherwise it needs to be started manually.
if (taskDefinition.getStartOnStartup()) {
scheduleTask(taskDefinition);
}
}
catch (Exception e) {
log.error("Failed to schedule task for class " + taskDefinition.getTaskClass(), e);
}
}
}
}
public static void setScheduledTasks(Map<Integer, TimerSchedulerTask> scheduledTasks) {
if (scheduledTasks != null) {
TimerSchedulerServiceImpl.scheduledTasks = scheduledTasks;
} else {
TimerSchedulerServiceImpl.scheduledTasks = new WeakHashMap<Integer, TimerSchedulerTask>();
}
}
/**
* Shutdown hook for the scheduler and all of its scheduled tasks.
*/
@Override
public void onShutdown() {
log.debug("Gracefully shutting down scheduler service ...");
// gracefully shutdown all tasks and remove all references to the timers, scheduler
try {
shutdownAllTasks();
cancelAllTimers(); // Just a precaution - this shouldn't be necessary if shutdownAllTasks() does its job
}
catch (APIException e) {
log.error("Failed to stop all tasks due to API exception", e);
}
finally {
setScheduledTasks(null);
}
}
/**
* Convenience method to stop all tasks in the {@link #taskDefinitionTimerMap}
*/
private void cancelAllTimers() {
for (Timer timer : taskDefinitionTimerMap.values()) {
timer.cancel();
}
}
/**
* Shutdown all running tasks.
*/
public void shutdownAllTasks() {
// iterate over this (copied) list of tasks and stop them all
for (TaskDefinition task : getScheduledTasks()) {
try {
shutdownTask(task);
}
catch (SchedulerException e) {
log.error("Failed to stop task " + task.getTaskClass() + " due to Scheduler exception", e);
}
catch (APIException e) {
log.error("Failed to stop task " + task.getTaskClass() + " due to API exception", e);
}
}
}
/**
* Get the {@link Timer} that is assigned to the given {@link TaskDefinition} object. If a Timer
* doesn't exist yet, one is created, added to {@link #taskDefinitionTimerMap} and then returned
*
* @param taskDefinition the {@link TaskDefinition} to look for
* @return the {@link Timer} associated with the given {@link TaskDefinition}
*/
private Timer getTimer(TaskDefinition taskDefinition) {
Timer timer;
if (taskDefinitionTimerMap.containsKey(taskDefinition)) {
timer = taskDefinitionTimerMap.get(taskDefinition);
} else {
timer = new Timer(true);
taskDefinitionTimerMap.put(taskDefinition, timer);
}
return timer;
}
/**
* Schedule the given task according to the given schedule.
*
* @param taskDefinition the task to be scheduled
* @should should handle zero repeat interval
*/
@Override
public Task scheduleTask(TaskDefinition taskDefinition) throws SchedulerException {
Task clientTask = null;
if (taskDefinition != null) {
// Cancel any existing timer tasks for the same task definition
// TODO Make sure this is the desired behavior
// TODO Do we ever want the same task definition to run more than once?
TimerSchedulerTask schedulerTask = scheduledTasks.get(taskDefinition.getId());
if (schedulerTask != null) {
//schedulerTask.cancel();
log.info("Shutting down the existing instance of this task to avoid conflicts!!");
schedulerTask.shutdown();
}
try {
// Create new task from task definition
clientTask = TaskFactory.getInstance().createInstance(taskDefinition);
// if we were unable to get a class, just quit
if (clientTask != null) {
schedulerTask = new TimerSchedulerTask(clientTask);
taskDefinition.setTaskInstance(clientTask);
// Once this method is called, the timer is set to start at the given start time.
// NOTE: We need to adjust the repeat interval as the JDK Timer expects time in milliseconds and
// we record by seconds.
long repeatInterval = 0;
if (taskDefinition.getRepeatInterval() != null) {
repeatInterval = taskDefinition.getRepeatInterval() * SchedulerConstants.SCHEDULER_MILLIS_PER_SECOND;
}
if (taskDefinition.getStartTime() != null) {
// Need to calculate the "next execution time" because the scheduled time is most likely in the past
// and the JDK timer will run the task X number of times from the start time until now to catch up.
Date nextTime = SchedulerUtil.getNextExecution(taskDefinition);
// Start task at fixed rate at given future date and repeat as directed
log.info("Starting task ... the task will execute for the first time at " + nextTime);
if (repeatInterval > 0) {
// Schedule the task to run at a fixed rate
getTimer(taskDefinition).scheduleAtFixedRate(schedulerTask, nextTime, repeatInterval);
} else {
// Schedule the task to be non-repeating
getTimer(taskDefinition).schedule(schedulerTask, nextTime);
}
} else if (repeatInterval > 0) {
// Start task on repeating schedule, delay for SCHEDULER_DEFAULT_DELAY seconds
log.info("Delaying start time by " + SchedulerConstants.SCHEDULER_DEFAULT_DELAY + " seconds");
getTimer(taskDefinition).scheduleAtFixedRate(schedulerTask,
SchedulerConstants.SCHEDULER_DEFAULT_DELAY, repeatInterval);
} else {
// schedule for single execution, starting now
log.info("Starting one-shot task");
getTimer(taskDefinition).schedule(schedulerTask, new Date());
}
// Update task that has been started
log.debug("Registering timer for task " + taskDefinition.getId());
// Add the new timer to the scheduler running task list
scheduledTasks.put(taskDefinition.getId(), schedulerTask);
// Update the timer status in the database
taskDefinition.setStarted(true);
saveTaskDefinition(taskDefinition);
}
}
catch (Exception e) {
log.error("Failed to schedule task " + taskDefinition.getName(), e);
throw new SchedulerException("Failed to schedule task", e);
}
}
return clientTask;
}
/**
* Stops a running task.
*
* @param taskDefinition the task to be stopped
* @see org.openmrs.scheduler.SchedulerService#shutdownTask(TaskDefinition)
*/
@Override
public void shutdownTask(TaskDefinition taskDefinition) throws SchedulerException {
if (taskDefinition != null) {
// Remove the task from the scheduled tasks and shutdown the timer
TimerSchedulerTask schedulerTask = scheduledTasks.remove(taskDefinition.getId());
if (schedulerTask != null) {
schedulerTask.shutdown(); // Stops the timer and tells the timer task to release its resources
}
// Update task that has been started
taskDefinition.setStarted(false);
saveTaskDefinition(taskDefinition);
}
}
/**
* Loop over all currently started tasks and cycle them. This should be done after the
* classloader has been changed (e.g. during module start/stop)
*/
@Override
public void rescheduleAllTasks() throws SchedulerException {
for (TaskDefinition task : getScheduledTasks()) {
try {
rescheduleTask(task);
}
catch (SchedulerException e) {
log.error("Failed to restart task: " + task.getName(), e);
}
}
}
/**
* @see org.openmrs.scheduler.SchedulerService#rescheduleTask(org.openmrs.scheduler.TaskDefinition)
*/
@Override
public Task rescheduleTask(TaskDefinition taskDefinition) throws SchedulerException {
shutdownTask(taskDefinition);
return scheduleTask(taskDefinition);
}
/**
* Register a new task by adding it to our task map with an empty schedule map.
*
* @param definition task to register
*/
public void registerTask(TaskDefinition definition) {
registeredTasks.add(definition);
}
/**
* Get all scheduled tasks.
*
* @return all scheduled tasks
*/
@Override
public Collection<TaskDefinition> getScheduledTasks() {
// The real list of scheduled tasks is kept up-to-date in the scheduledTasks map
// TODO change the index for the scheduledTasks map to be the TaskDefinition rather than the ID
List<TaskDefinition> list = new ArrayList<TaskDefinition>();
if (scheduledTasks != null) {
Set<Integer> taskIds = scheduledTasks.keySet();
for (Integer id : taskIds) {
TaskDefinition task = getTask(id);
log.debug("Adding scheduled task " + id + " to list (" + task.getRepeatInterval() + ")");
list.add(task);
}
}
return list;
}
/**
* Get all registered tasks.
*
* @return all registerd tasks
*/
@Override
@Transactional(readOnly = true)
public Collection<TaskDefinition> getRegisteredTasks() {
return getSchedulerDAO().getTasks();
}
/**
* Get the task with the given identifier.
*
* @param id the identifier of the task
*/
@Override
@Transactional(readOnly = true)
public TaskDefinition getTask(Integer id) {
if (log.isDebugEnabled()) {
log.debug("get task " + id);
}
return getSchedulerDAO().getTask(id);
}
/**
* Get the task with the given name.
*
* @param name name of the task
*/
@Override
@Transactional(readOnly = true)
public TaskDefinition getTaskByName(String name) {
if (log.isDebugEnabled()) {
log.debug("get task " + name);
}
TaskDefinition foundTask = null;
try {
foundTask = getSchedulerDAO().getTaskByName(name);
}
catch (ObjectRetrievalFailureException orfe) {
log.warn("getTaskByName(" + name + ") failed, because: " + orfe);
}
return foundTask;
}
/**
* Save a task in the database.
*
* @param task the <code>TaskDefinition</code> to save
*/
@Override
public void saveTaskDefinition(TaskDefinition task) {
if (task.getId() != null) {
getSchedulerDAO().updateTask(task);
} else {
getSchedulerDAO().createTask(task);
}
}
/**
* Delete the task with the given identifier.
*
* @param id the identifier of the task
*/
@Override
public void deleteTask(Integer id) {
TaskDefinition task = getTask(id);
if (task.getStarted()) {
throw new APIException("Scheduler.timer.task.delete", (Object[]) null);
}
// delete the task
getSchedulerDAO().deleteTask(id);
}
/**
* Get system variables.
*/
@Override
public SortedMap<String, String> getSystemVariables() {
SortedMap<String, String> systemVariables = new TreeMap<String, String>();
// scheduler username and password can be found in the global properties
// TODO Look into java.util.concurrent.TimeUnit class.
// TODO Remove this from global properties. This is a constant value that should never change.
systemVariables.put("SCHEDULER_MILLIS_PER_SECOND", String.valueOf(SchedulerConstants.SCHEDULER_MILLIS_PER_SECOND));
return systemVariables;
}
/**
* Saves and stops all active tasks
*
* @return OpenmrsMemento
*/
@Override
public OpenmrsMemento saveToMemento() {
Set<Integer> tasks = new HashSet<Integer>();
for (TaskDefinition task : getScheduledTasks()) {
tasks.add(task.getId());
try {
shutdownTask(task);
}
catch (SchedulerException e) {
// just swallow exceptions
log.debug("Failed to stop task while saving memento " + task.getName(), e);
}
}
TimerSchedulerMemento memento = new TimerSchedulerMemento(tasks);
memento.saveErrorTasks();
return memento;
}
/**
*
*/
@Override
@SuppressWarnings("unchecked")
public void restoreFromMemento(OpenmrsMemento memento) {
if (memento != null && memento instanceof TimerSchedulerMemento) {
TimerSchedulerMemento timerMemento = (TimerSchedulerMemento) memento;
Set<Integer> taskIds = (HashSet<Integer>) timerMemento.getState();
// try to start all of the tasks that were stopped right before this restore
for (Integer taskId : taskIds) {
TaskDefinition task = getTask(taskId);
try {
scheduleTask(task);
}
catch (Exception e) {
// essentially swallow exceptions
log.debug("EXPECTED ERROR IF STOPPING THIS TASK'S MODULE: Unable to start task " + taskId, e);
// save this errored task and try again next time we restore
timerMemento.addErrorTask(taskId);
}
}
timerMemento = null; // so the old cl can be gc'd
}
}
/**
* @see org.openmrs.scheduler.SchedulerService#getStatus(java.lang.Integer) TODO
* internationalization of string status messages
*/
@Override
public String getStatus(Integer id) {
// Get the scheduled timer task
TimerSchedulerTask scheduledTask = scheduledTasks.get(id);
if (scheduledTask != null) {
if (scheduledTask.scheduledExecutionTime() > 0) {
return "Scheduled to execute at " + new Date(scheduledTask.scheduledExecutionTime());
} else {
return "Currently executing";
}
}
return "Not Running";
}
@Override
public void scheduleIfNotRunning(TaskDefinition taskDef) {
Task task = taskDef.getTaskInstance();
if (task == null) {
try {
scheduleTask(taskDef);
}
catch (SchedulerException e) {
log.error("Failed to schedule task, because:", e);
}
} else if (!task.isExecuting()) {
try {
rescheduleTask(taskDef);
}
catch (SchedulerException e) {
log.error("Failed to re-schedule task, because:", e);
}
}
}
}