/**
* 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;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.Collection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
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;
/**
* TODO test all methods in SchedulerService
*/
@Ignore("https://issues.openmrs.org/browse/TRUNK-4212")
public class SchedulerServiceTest extends BaseContextSensitiveTest {
// so that we can guarantee tests running accurately instead of tests interfering with the next
public final Integer TASK_TEST_METHOD_LOCK = Integer.valueOf(1);
// used to check for concurrent task execution. Only initialized by code protected by TASK_TEST_METHOD_LOCK.
public static CountDownLatch latch;
public static AtomicBoolean awaitFailed = new AtomicBoolean(false);
public static AtomicBoolean consecutiveInitResult = new AtomicBoolean(false);
// time to wait for concurrent tasks to execute, should only wait this long if there's a test failure
public static final long CONCURRENT_TASK_WAIT_MS = 30000;
public static Logger log = LogManager.getLogger(SchedulerServiceTest.class);
@Before
public void setUp() throws Exception {
// Temporary logger level changes to debug TRUNK-4212
LogManager.getLogger("org.hibernate.SQL").setLevel(Level.DEBUG);
LogManager.getLogger("org.hibernate.type").setLevel(Level.TRACE);
LogManager.getLogger("org.openmrs.api").setLevel(Level.DEBUG);
LogManager.getLogger("org.openmrs.scheduler").setLevel(Level.DEBUG);
log.debug("SchedulerServiceTest setup() start");
Context.flushSession();
Collection<TaskDefinition> tasks = Context.getSchedulerService().getRegisteredTasks();
for (TaskDefinition task : tasks) {
Context.getSchedulerService().shutdownTask(task);
Context.getSchedulerService().deleteTask(task.getId());
}
Context.flushSession();
log.debug("SchedulerServiceTest setup() complete");
}
@After
public void cleanUp() throws Exception {
// Temporary logger level changes to debug TRUNK-4212
LogManager.getLogger("org.hibernate.SQL").setLevel(Level.WARN);
LogManager.getLogger("org.hibernate.type").setLevel(Level.WARN);
LogManager.getLogger("org.openmrs.api").setLevel(Level.WARN);
LogManager.getLogger("org.openmrs.scheduler").setLevel(Level.WARN);
}
@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);
}
private TaskDefinition makeRepeatingTaskThatStartsImmediately(String taskClassName) {
TaskDefinition taskDef = new TaskDefinition();
taskDef.setTaskClass(taskClassName);
taskDef.setStartOnStartup(false);
taskDef.setStartTime(null);
taskDef.setName("name");
taskDef.setRepeatInterval(CONCURRENT_TASK_WAIT_MS * 10); // latch should timeout before task ever repeats
// save task definition to generate a unique ID, otherwise the scheduler thinks they're duplicates and tries to shut one down
Context.getSchedulerService().saveTaskDefinition(taskDef);
return taskDef;
}
/**
* Demonstrates concurrent running for tasks
*/
@Test
public void shouldAllowTwoTasksToRunConcurrently() throws Exception {
TaskDefinition t1 = makeRepeatingTaskThatStartsImmediately(LatchExecuteTask.class.getName());
TaskDefinition t2 = makeRepeatingTaskThatStartsImmediately(LatchExecuteTask.class.getName());
checkTasksRunConcurrently(t1, t2);
}
/**
* Demonstrates concurrent initializing for tasks
*/
@Test
public void shouldAllowTwoTasksInitMethodsToRunConcurrently() throws Exception {
TaskDefinition t3 = makeRepeatingTaskThatStartsImmediately(LatchInitializeTask.class.getName());
TaskDefinition t4 = makeRepeatingTaskThatStartsImmediately(LatchInitializeTask.class.getName());
checkTasksRunConcurrently(t3, t4);
}
private void checkTasksRunConcurrently(TaskDefinition t1, TaskDefinition t2) throws SchedulerException,
InterruptedException {
SchedulerService schedulerService = Context.getSchedulerService();
// synchronized on a class level object in case a test runner is running test methods concurrently
synchronized (TASK_TEST_METHOD_LOCK) {
latch = new CountDownLatch(2);
awaitFailed.set(false);
schedulerService.scheduleTask(t1);
schedulerService.scheduleTask(t2);
// wait for the tasks to call countDown()
assertTrue("methods ran consecutively or not at all", latch
.await(CONCURRENT_TASK_WAIT_MS, TimeUnit.MILLISECONDS));
// the main await() didn't fail so both tasks ran and called countDown(),
// but if the first await() failed and the latch still reached 0 then the tasks must have been running consecutively
assertTrue("methods ran consecutively", !awaitFailed.get());
}
schedulerService.shutdownTask(t1);
schedulerService.shutdownTask(t2);
}
public abstract static class LatchTask extends AbstractTask {
protected void waitForLatch() {
try {
latch.countDown();
// wait here until the other task thread(s) also countDown the latch
// if they do then they must be executing concurrently with this task
if (!latch.await(CONCURRENT_TASK_WAIT_MS, TimeUnit.MILLISECONDS)) {
// this wait timed out, record it as otherwise the next
// task(s) could execute consecutively rather than concurrently
awaitFailed.set(true);
}
}
catch (InterruptedException ignored) {}
}
}
/**
* task that waits in its initialize method until all other tasks on the same latch have called
* initialize()
*/
public static class LatchInitializeTask extends LatchTask {
@Override
public void initialize(TaskDefinition config) {
super.initialize(config);
waitForLatch();
}
@Override
public void execute() {
}
}
/**
* task that waits in its execute method until all other tasks on the same latch have called
* execute()
*/
public static class LatchExecuteTask extends LatchTask {
@Override
public void initialize(TaskDefinition config) {
super.initialize(config);
}
@Override
public void execute() {
waitForLatch();
}
}
/**
* task that checks for its execute method running at the same time as its initialize method
*/
public static class InitSequenceTestTask extends AbstractTask {
@Override
public void initialize(TaskDefinition config) {
super.initialize(config);
// wait for any other thread to run the execute method
try {
Thread.sleep(700);
}
catch (InterruptedException ignored) {}
// set to false if execute() method was running concurrently and has cleared the latch
consecutiveInitResult.set(latch.getCount() != 0);
}
@Override
public void execute() {
// clear the latch to signal the main thread
latch.countDown();
}
}
/**
* 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)
*/
@Test
public void shouldNotAllowTaskExecuteToRunBeforeInitializationIsComplete() throws Exception {
SchedulerService schedulerService = Context.getSchedulerService();
TaskDefinition t5 = new TaskDefinition();
t5.setStartOnStartup(false);
t5.setStartTime(null); // immediate start
t5.setTaskClass(InitSequenceTestTask.class.getName());
t5.setName("name");
t5.setRepeatInterval(CONCURRENT_TASK_WAIT_MS * 4);
synchronized (TASK_TEST_METHOD_LOCK) {
// wait for the task to complete
latch = new CountDownLatch(1);
consecutiveInitResult.set(false);
schedulerService.saveTaskDefinition(t5);
schedulerService.scheduleTask(t5);
assertTrue("Init and execute methods should run consecutively", latch.await(CONCURRENT_TASK_WAIT_MS,
TimeUnit.MILLISECONDS)
&& consecutiveInitResult.get());
}
schedulerService.shutdownTask(t5);
}
@Test
public void saveTask_shouldSaveTaskToTheDatabase() throws Exception {
log.debug("saveTask_shouldSaveTaskToTheDatabase start");
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(10000000L);
def.setTaskClass(LatchExecuteTask.class.getName());
synchronized (TASK_TEST_METHOD_LOCK) {
Collection<TaskDefinition> tasks = service.getRegisteredTasks();
for (TaskDefinition task : tasks) {
log.debug("Task dump 1: " + task);
}
int size = tasks.size();
service.saveTaskDefinition(def);
tasks = service.getRegisteredTasks();
for (TaskDefinition task : tasks) {
log.debug("Task dump 2:" + task);
}
Assert.assertEquals(size + 1, tasks.size());
}
def = service.getTaskByName(TASK_NAME);
Assert.assertEquals(Context.getAuthenticatedUser().getUserId(), def.getCreator().getUserId());
log.debug("saveTask_shouldSaveTaskToTheDatabase end");
}
/**
* Sample task that does not extend AbstractTask
*/
public static class BareTask implements Task {
@Override
public void execute() {
latch.countDown();
}
@Override
public TaskDefinition getTaskDefinition() {
return null;
}
@Override
public void initialize(TaskDefinition definition) {
}
@Override
public boolean isExecuting() {
return false;
}
@Override
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.setName("Task");
td.setStartOnStartup(false);
td.setTaskClass(BareTask.class.getName());
td.setStartTime(null);
td.setName("name");
td.setRepeatInterval(5000l);
synchronized (TASK_TEST_METHOD_LOCK) {
latch = new CountDownLatch(1);
schedulerService.saveTaskDefinition(td);
schedulerService.scheduleTask(td);
assertTrue(latch.await(CONCURRENT_TASK_WAIT_MS, TimeUnit.MILLISECONDS));
}
}
/**
* Just stores the execution time.
*/
public static class StoreExecutionTimeTask extends AbstractTask {
@Override
public void execute() {
actualExecutionTime = System.currentTimeMillis();
// signal the test method that the task has executed
latch.countDown();
}
}
public static Long actualExecutionTime;
/**
* Check saved last execution time.
*/
@Test
public void shouldSaveLastExecutionTime() throws Exception {
log.debug("shouldSaveLastExecutionTime start");
final String NAME = "StoreExecutionTime Task";
SchedulerService service = Context.getSchedulerService();
TaskDefinition td = new TaskDefinition();
td.setName(NAME);
td.setStartOnStartup(false);
td.setTaskClass(StoreExecutionTimeTask.class.getName());
td.setStartTime(null);
td.setRepeatInterval(0l);//0 indicates single execution
synchronized (TASK_TEST_METHOD_LOCK) {
latch = new CountDownLatch(1);
service.saveTaskDefinition(td);
service.scheduleTask(td);
// wait for the task to execute
assertTrue("task didn't execute", latch.await(CONCURRENT_TASK_WAIT_MS, TimeUnit.MILLISECONDS));
}
log.debug("shouldSaveLastExecutionTime task done");
// wait for the SchedulerService to update the execution time
for (int x = 0; x < 100; x++) {
// refetch the task
td = service.getTaskByName(NAME);
if (td.getLastExecutionTime() != null) {
log.debug("shouldSaveLastExecutionTime wait done");
break;
}
Thread.sleep(200);
}
assertNotNull(
"actualExecutionTime is null, so either the SessionTask.execute method hasn't finished or didn't get run",
actualExecutionTime);
assertNotNull("lastExecutionTime is null, so the SchedulerService didn't save it", td.getLastExecutionTime());
assertEquals("Last execution time in seconds is wrong", actualExecutionTime / 1000, td.getLastExecutionTime()
.getTime() / 1000, 1);
}
}