SchedulerServiceTest.java

/**
 * The contents of this file are subject to the OpenMRS Public License
 * Version 1.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://license.openmrs.org
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 *
 * Copyright (C) OpenMRS, LLC.  All Rights Reserved.
 */
package org.openmrs.scheduler;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.openmrs.api.context.Context;
import org.openmrs.scheduler.tasks.AbstractTask;
import org.openmrs.test.BaseContextSensitiveTest;
import org.openmrs.util.OpenmrsClassLoader;
import org.springframework.util.StringUtils;

/**
 * TODO test all methods in ScheduleService
 */
public class SchedulerServiceTest extends BaseContextSensitiveTest {
	
	private static Log log = LogFactory.getLog(SchedulerServiceTest.class);
	
	// so that we can guarantee tests running accurately instead of tests interfering with the next
	public final Integer SAVE_TASK_LOCK = new Integer(1);
	
	// each task provides a key that will be used in this map.  The value is the output
	private static Map<String, String> output = new HashMap<String, String>();
	
	@Before
	public void setUp() throws Exception {
		Context.flushSession();
		
		Collection<TaskDefinition> tasks = Context.getSchedulerService().getRegisteredTasks();
		for (TaskDefinition task : tasks) {
			Context.getSchedulerService().shutdownTask(task);
			Context.getSchedulerService().deleteTask(task.getId());
		}
		
		Context.flushSession();
	}
	
	@Test
	public void shouldResolveValidTaskClass() throws Exception {
		String className = "org.openmrs.scheduler.tasks.TestTask";
		Class c = OpenmrsClassLoader.getInstance().loadClass(className);
		Object o = c.newInstance();
		if (o instanceof Task)
			assertTrue("Class " + className + " is a valid Task", true);
		else
			fail("Class " + className + " is not a valid Task");
	}
	
	@Test(expected = ClassNotFoundException.class)
	public void shouldNotResolveInvalidClass() throws Exception {
		String className = "org.openmrs.scheduler.tasks.InvalidTask";
		Class c = OpenmrsClassLoader.getInstance().loadClass(className);
		Object o = c.newInstance();
		if (o instanceof Task)
			fail("Class " + className + " is not supposed to be a valid Task");
		else
			assertTrue("Class " + className + " is not a valid Task", true);
	}
	
	/**
	 * Longer running class used to demonstrate tasks running concurrently
	 */
	public static class ExecutePrintingTask extends AbstractTask {
		
		public void execute() {
			String outputKey = getTaskDefinition().getProperty("outputKey");
			appendOutput(outputKey, getTaskDefinition().getProperty("id"));
			
			try {
				Thread.sleep(Integer.valueOf(getTaskDefinition().getProperty("delay")));
			}
			catch (InterruptedException e) {
				log.error("Error generated", e);
			}
			
			appendOutput(outputKey, getTaskDefinition().getProperty("id"));
			
		}
	}
	
	/**
	 * Helper method to append the given text to the "output" static variable map
	 * <br/>
	 * Map will contain string like "text, text1, text2"
	 * 
	 * @param outputKey the key for the "output" map
	 * @param the text to append to the value in the output map with the given key
	 */
	public synchronized static void appendOutput(String outputKey, String appendText) {
		if (StringUtils.hasLength(output.get(outputKey)))
			output.put(outputKey, output.get(outputKey) + ", " + appendText);
		else
			output.put(outputKey, appendText);
	}
	
	/**
	 * Demonstrates concurrent running for tasks
	 * 
	 * <pre>
	 *             |
	 * SampleTask2 |    ----
	 * SampleTask1 |------------
	 *             |_____________ time
	 *              ^   ^   ^   ^
	 * Output:     S-1 S-2 E-2 E-1
	 * </pre>
	 */
	@Test
	public void shouldAllowTwoTasksToRunConcurrently() throws Exception {
		SchedulerService schedulerService = Context.getSchedulerService();
		
		TaskDefinition t1 = new TaskDefinition();
		t1.setId(1);
		t1.setStartOnStartup(false);
		t1.setStartTime(null);
		t1.setTaskClass(ExecutePrintingTask.class.getName());
		t1.setProperty("id", "TASK-1");
		t1.setProperty("delay", "400"); // must be longer than t2's delay
		t1.setProperty("outputKey", "shouldAllowTwoTasksToRunConcurrently");
		t1.setName("name");
		t1.setRepeatInterval(5000l);
		
		TaskDefinition t2 = new TaskDefinition();
		t2.setId(2);
		t2.setStartOnStartup(false);
		t2.setStartTime(null);
		t2.setTaskClass(ExecutePrintingTask.class.getName());
		t2.setProperty("id", "TASK-2");
		t2.setProperty("delay", "100"); // must be shorter than t1's delay
		t2.setProperty("outputKey", "shouldAllowTwoTasksToRunConcurrently");
		t2.setName("name");
		t2.setRepeatInterval(5000l);
		
		synchronized (SAVE_TASK_LOCK) {
			schedulerService.scheduleTask(t1);
			Thread.sleep(50); // so t2 doesn't start before t1 due to random millisecond offsets
			schedulerService.scheduleTask(t2);
			Thread.sleep(2500); // must be longer than t2's delay
			assertEquals("TASK-1, TASK-2, TASK-2, TASK-1", output.get("shouldAllowTwoTasksToRunConcurrently"));
		}
	}
	
	/**
	 * Longer init'ing class for concurrent init test
	 */
	public static class SimpleTask extends AbstractTask {
		
		public void initialize(TaskDefinition config) {
			String outputKey = config.getProperty("outputKey");
			appendOutput(outputKey, config.getProperty("id"));
			
			super.initialize(config);
			try {
				// must be less than delay before printing
				Thread.sleep(Integer.valueOf(config.getProperty("delay")));
			}
			catch (InterruptedException e) {
				log.error("Error generated", e);
			}
			
			appendOutput(outputKey, config.getProperty("id"));
		}
		
		public void execute() {
		}
	}
	
	/**
	 * Demonstrates concurrent initializing for tasks
	 * 
	 * <pre>
	 *             |
	 * SampleTask4 |    ----
	 * SampleTask3 |------------
	 *             |_____________ time
	 *              ^   ^   ^   ^
	 * Output:     S-3 S-4 E-4 E-3
	 * </pre>
	 */
	@Test
	public void shouldAllowTwoTasksInitMethodsToRunConcurrently() throws Exception {
		SchedulerService schedulerService = Context.getSchedulerService();
		
		TaskDefinition t3 = new TaskDefinition();
		t3.setStartOnStartup(false);
		t3.setStartTime(null); // so it starts immediately
		t3.setTaskClass(SimpleTask.class.getName());
		t3.setProperty("id", "TASK-3");
		t3.setProperty("delay", "300"); // must be longer than t4's delay
		t3.setProperty("outputKey", "shouldAllowTwoTasksInitMethodsToRunConcurrently");
		t3.setName("name");
		t3.setRepeatInterval(5000l);
		
		TaskDefinition t4 = new TaskDefinition();
		t4.setStartOnStartup(false);
		t4.setStartTime(null); // so it starts immediately
		t4.setTaskClass(SimpleTask.class.getName());
		t4.setProperty("id", "TASK-4");
		t4.setProperty("delay", "100");
		t4.setProperty("outputKey", "shouldAllowTwoTasksInitMethodsToRunConcurrently");
		t4.setName("name");
		t4.setRepeatInterval(5000l);
		
		// both of these tasks start immediately
		synchronized (SAVE_TASK_LOCK) {
			schedulerService.scheduleTask(t3); // starts first, ends last
			schedulerService.scheduleTask(t4); // starts last, ends first
		}
		Thread.sleep(500); // must be greater than task3 delay so that it prints out its end
		assertEquals("TASK-3, TASK-4, TASK-4, TASK-3", output.get("shouldAllowTwoTasksInitMethodsToRunConcurrently"));
		
		// cleanup
		schedulerService.shutdownTask(t3);
		schedulerService.shutdownTask(t4);
	}
	
	public static class SampleTask5 extends AbstractTask {
		
		public void initialize(TaskDefinition config) {
			
			String outputKey = config.getProperty("outputKey");
			appendOutput(outputKey, "INIT-START-5");
			
			super.initialize(config);
			try {
				Thread.sleep(700);
			}
			catch (InterruptedException e) {
				log.error("Error generated", e);
			}
			
			appendOutput(outputKey, "INIT-END-5");
		}
		
		public void execute() {
			String outputKey = getTaskDefinition().getProperty("outputKey");
			appendOutput(outputKey, "IN EXECUTE");
		}
	}
	
	/**
	 * Demonstrates that initialization of a task is accomplished before its execution without
	 * interleaving, which is a non-trivial behavior in the presence of a threaded initialization
	 * method (as implemented in TaskThreadedInitializationWrapper)
	 * 
	 * <pre>
	 *             |
	 * SampleTask5 |------------
	 *             |_____________ time
	 *              ^   ^   ^   ^
	 * Output:     IS  IE   S   E
	 * </pre>
	 */
	@Test
	public void shouldNotAllowTaskExecuteToRunBeforeInitializationIsComplete() throws Exception {
		SchedulerService schedulerService = Context.getSchedulerService();
		
		TaskDefinition t5 = new TaskDefinition();
		t5.setId(5);
		t5.setStartOnStartup(false);
		t5.setStartTime(null); // immediate start
		t5.setTaskClass(SampleTask5.class.getName());
		t5.setProperty("outputKey", "shouldNotAllowTaskExecuteToRunBeforeInitializationIsComplete");
		t5.setName("name");
		t5.setRepeatInterval(5000l);
		
		synchronized (SAVE_TASK_LOCK) {
			schedulerService.scheduleTask(t5);
			Thread.sleep(2500);
			assertEquals("INIT-START-5, INIT-END-5, IN EXECUTE", output
			        .get("shouldNotAllowTaskExecuteToRunBeforeInitializationIsComplete"));
		}
	}
	
	@Test
	public void saveTask_shouldSaveTaskToTheDatabase() throws Exception {
		SchedulerService service = Context.getSchedulerService();
		
		TaskDefinition def = new TaskDefinition();
		final String TASK_NAME = "This is my test! 123459876";
		def.setName(TASK_NAME);
		def.setStartOnStartup(false);
		def.setRepeatInterval(10L);
		def.setTaskClass(ExecutePrintingTask.class.getName());
		
		synchronized (SAVE_TASK_LOCK) {
			int size = service.getRegisteredTasks().size();
			service.saveTask(def);
			Assert.assertEquals(size + 1, service.getRegisteredTasks().size());
		}
		
		def = service.getTaskByName(TASK_NAME);
		Assert.assertEquals(Context.getAuthenticatedUser().getUserId(), def.getCreator().getUserId());
	}
	
	/**
	 * Sample task that does not extend AbstractTask
	 */
	public static class BareTask implements Task {
		
		public static ArrayList outputList = new ArrayList();
		
		public void execute() {
			synchronized (outputList) {
				outputList.add("TEST");
			}
		}
		
		public TaskDefinition getTaskDefinition() {
			return null;
		}
		
		public void initialize(TaskDefinition definition) {
		}
		
		public boolean isExecuting() {
			return false;
		}
		
		public void shutdown() {
		}
	}
	
	/**
	 * Task which does not return TaskDefinition in getTaskDefinition should run without throwing
	 * exceptions.
	 * 
	 * @throws Exception
	 */
	@Test
	public void shouldNotThrowExceptionWhenTaskDefinitionIsNull() throws Exception {
		SchedulerService schedulerService = Context.getSchedulerService();
		
		TaskDefinition td = new TaskDefinition();
		td.setId(10);
		td.setName("Task");
		td.setStartOnStartup(false);
		td.setTaskClass(BareTask.class.getName());
		td.setStartTime(null);
		td.setName("name");
		td.setRepeatInterval(5000l);
		
		synchronized (SAVE_TASK_LOCK) {
			schedulerService.scheduleTask(td);
		}
		Thread.sleep(500);
		
		assertTrue(BareTask.outputList.contains("TEST"));
	}
	
	/**
	 * Task opens a session and stores the execution time.
	 */
	public static class SessionTask extends AbstractTask {
		
		public void execute() {
			try {
				// something would happen here...
				
				actualExecutionTime = System.currentTimeMillis();
			}
			finally {}
			
		}
	}
	
	public static Long actualExecutionTime;
	
	/**
	 * Check saved last execution time.
	 */
	@Test
	public void shouldSaveLastExecutionTime() throws Exception {
		final String NAME = "Session Task";
		SchedulerService service = Context.getSchedulerService();
		
		TaskDefinition td = new TaskDefinition();
		td.setName(NAME);
		td.setStartOnStartup(false);
		td.setTaskClass(SessionTask.class.getName());
		td.setStartTime(null);
		td.setRepeatInterval(new Long(0));//0 indicates single execution
		synchronized (SAVE_TASK_LOCK) {
			service.saveTask(td);
			service.scheduleTask(td);
		}
		
		// refetch the task
		td = service.getTaskByName(NAME);
		
		// sleep a while until the task has executed, up to 30 times
		for (int x = 0; x < 30 && (actualExecutionTime == null || td.getLastExecutionTime() == null); x++)
			Thread.sleep(200);
		
		Assert
		        .assertNotNull(
		            "The actualExecutionTime variable is null, so either the SessionTask.execute method hasn't finished or didn't get run",
		            actualExecutionTime);
		assertEquals("Last execution time in seconds is wrong", actualExecutionTime.longValue() / 1000, td
		        .getLastExecutionTime().getTime() / 1000, 1);
	}
}