Refactored task end event to be Spring Boot's ApplicationReadyEvent

In testing and other use cases, the ability to keep a context open
beyond the end of a task can be useful.  This change provides the
ability to keep the context alive once a task has completed while also
explicitly shutting the context down (by default) once a task has ended.

Resolves spring-cloud/spring-cloud-task#102
This commit is contained in:
Michael Minella
2016-04-05 11:44:09 -05:00
parent 3d3b90812e
commit 0a56672171
9 changed files with 88 additions and 107 deletions

View File

@@ -61,6 +61,8 @@ import static org.junit.Assert.assertEquals;
*/
public class TaskBatchExecutionListenerTests {
public static final String[] ARGS = new String[] {"--spring.cloud.task.closecontext.enable=false"};
private ConfigurableApplicationContext applicationContext;
@After
@@ -76,7 +78,7 @@ public class TaskBatchExecutionListenerTests {
PropertyPlaceholderAutoConfiguration.class,
EmbeddedDataSourceConfiguration.class,
BatchAutoConfiguration.class,
TaskBatchAutoConfiguration.class}, new String[0]);
TaskBatchAutoConfiguration.class}, ARGS);
TaskExplorer taskExplorer = this.applicationContext.getBean(TaskExplorer.class);
@@ -94,7 +96,7 @@ public class TaskBatchExecutionListenerTests {
PropertyPlaceholderAutoConfiguration.class,
EmbeddedDataSourceConfiguration.class,
BatchAutoConfiguration.class,
TaskBatchAutoConfiguration.class}, new String[0]);
TaskBatchAutoConfiguration.class}, ARGS);
TaskExplorer taskExplorer = this.applicationContext.getBean(TaskExplorer.class);
@@ -112,7 +114,7 @@ public class TaskBatchExecutionListenerTests {
PropertyPlaceholderAutoConfiguration.class,
EmbeddedDataSourceConfiguration.class,
BatchAutoConfiguration.class,
TaskBatchAutoConfiguration.class}, new String[0]);
TaskBatchAutoConfiguration.class}, ARGS);
TaskExplorer taskExplorer = this.applicationContext.getBean(TaskExplorer.class);
@@ -128,7 +130,7 @@ public class TaskBatchExecutionListenerTests {
this.applicationContext = SpringApplication.run(new Object[] {JobConfiguration.class,
PropertyPlaceholderAutoConfiguration.class,
BatchAutoConfiguration.class,
TaskBatchAutoConfiguration.class}, new String[0]);
TaskBatchAutoConfiguration.class}, ARGS);
TaskExplorer taskExplorer = this.applicationContext.getBean(TaskExplorer.class);
@@ -145,7 +147,7 @@ public class TaskBatchExecutionListenerTests {
this.applicationContext = SpringApplication.run(new Object[] {MultipleJobConfiguration.class,
PropertyPlaceholderAutoConfiguration.class,
BatchAutoConfiguration.class,
TaskBatchAutoConfiguration.class}, new String[0]);
TaskBatchAutoConfiguration.class}, ARGS);
TaskExplorer taskExplorer = this.applicationContext.getBean(TaskExplorer.class);

View File

@@ -28,16 +28,18 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ExitCodeEvent;
import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.task.repository.TaskExecution;
import org.springframework.cloud.task.repository.TaskNameResolver;
import org.springframework.cloud.task.repository.TaskRepository;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.SmartLifecycle;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.util.Assert;
@@ -50,17 +52,21 @@ import org.springframework.util.Assert;
* <ul>
* <li>{@link ContextRefreshedEvent} - Used to identify the start of a task. A task
* is expected to contain a single application context.</li>
* <li>{@link ContextClosedEvent} - Used to identify the successful end of a task.</li>
* <li>{@link ApplicationReadyEvent} - Used to identify the successful end of a task.</li>
* <li>{@link ApplicationFailedEvent} - Used to identify the failure of a task.</li>
* </ul>
*
* <b>NOTE:</b> Multiple contexts (including parent/child relationships) will result in
* only the first context refresh that contains this instance being recorded.
* <b>Note:</b> By default, the context will be closed at the completion of a task (once
* the task repository has been updated). This behavior can be configured via the
* property <code>spring.cloud.task.closecontext.enable</code> (defaults to true).
*
* @author Michael Minella
*/
public class TaskLifecycleListener implements ApplicationListener<ApplicationEvent>, SmartLifecycle {
@Autowired
private ConfigurableApplicationContext context;
@Autowired(required = false)
private Collection<TaskExecutionListener> taskExecutionListeners;
@@ -80,6 +86,9 @@ public class TaskLifecycleListener implements ApplicationListener<ApplicationEve
private ExitCodeEvent exitCodeEvent;
@Value("${spring.cloud.task.closecontext.enable:true}")
private Boolean closeContext;
/**
* @param taskRepository The repository to record executions in.
*/
@@ -99,7 +108,7 @@ public class TaskLifecycleListener implements ApplicationListener<ApplicationEve
* task. Specifically:
* <ul>
* <li>{@link ContextRefreshedEvent} - Start of a task</li>
* <li>{@link ContextClosedEvent} - Successful end of a task</li>
* <li>{@link ApplicationReadyEvent} - Successful end of a task</li>
* <li>{@link ApplicationFailedEvent} - Failure of a task</li>
* </ul>
*
@@ -112,8 +121,9 @@ public class TaskLifecycleListener implements ApplicationListener<ApplicationEve
}
else if(applicationEvent instanceof ExitCodeEvent){
this.exitCodeEvent = (ExitCodeEvent) applicationEvent;
doTaskEnd();
}
else if(applicationEvent instanceof ContextClosedEvent) {
else if(applicationEvent instanceof ApplicationReadyEvent) {
doTaskEnd();
}
}
@@ -152,6 +162,10 @@ public class TaskLifecycleListener implements ApplicationListener<ApplicationEve
taskExecution.setExitMessage(invokeOnTaskEnd(taskExecution).getExitMessage());
taskRepository.completeTaskExecution(taskExecution.getExecutionId(), taskExecution.getExitCode(),
taskExecution.getEndTime(), taskExecution.getExitMessage());
if(this.closeContext) {
this.context.close();
}
}
else {
logger.error("An event to end a task has been received for a task that has " +

View File

@@ -25,6 +25,7 @@ import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.task.listener.annotation.AfterTask;
import org.springframework.cloud.task.listener.annotation.BeforeTask;
import org.springframework.cloud.task.listener.annotation.FailedTask;
@@ -36,7 +37,6 @@ import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextClosedEvent;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -56,7 +56,7 @@ public class TaskExecutionListenerTests {
@After
public void tearDown() {
if(context != null) {
if(context != null && context.isActive()) {
context.close();
}
}
@@ -84,7 +84,7 @@ public class TaskExecutionListenerTests {
setupContextForTaskExecutionListener();
DefaultTaskListenerConfiguration.TestTaskExecutionListener taskExecutionListener =
context.getBean(DefaultTaskListenerConfiguration.TestTaskExecutionListener.class);
context.publishEvent(new ContextClosedEvent(context));
context.publishEvent(new ApplicationReadyEvent(new SpringApplication(), new String[0], context));
TaskExecution taskExecution = new TaskExecution(0, 0, "wombat",
new Date(), new Date(), null, new ArrayList<String>());
@@ -99,10 +99,11 @@ public class TaskExecutionListenerTests {
public void testTaskFail() {
RuntimeException exception = new RuntimeException(EXCEPTION_MESSAGE);
setupContextForTaskExecutionListener();
context.publishEvent(new ApplicationFailedEvent(new SpringApplication(), new String[0], context, exception));
context.publishEvent(new ContextClosedEvent(context));
SpringApplication application = new SpringApplication();
context.publishEvent(new ApplicationFailedEvent(application, new String[0], context, exception));
DefaultTaskListenerConfiguration.TestTaskExecutionListener taskExecutionListener =
context.getBean(DefaultTaskListenerConfiguration.TestTaskExecutionListener.class);
context.publishEvent(new ApplicationReadyEvent(application, new String[0], context));
TaskExecution taskExecution = new TaskExecution(0, 1, "wombat", new Date(),
new Date(), null, new ArrayList<String>());
@@ -132,7 +133,7 @@ public class TaskExecutionListenerTests {
setupContextForAnnotatedListener();
DefaultAnnotationConfiguration.AnnotatedTaskListener annotatedListener =
context.getBean(DefaultAnnotationConfiguration.AnnotatedTaskListener.class);
context.publishEvent(new ContextClosedEvent(context));
context.publishEvent(new ApplicationReadyEvent(new SpringApplication(), new String[0], context));
TaskExecution taskExecution = new TaskExecution(0, 0, "wombat",
new Date(), new Date(), null, new ArrayList<String>());
@@ -147,10 +148,11 @@ public class TaskExecutionListenerTests {
public void testAnnotationFail() {
RuntimeException exception = new RuntimeException(EXCEPTION_MESSAGE);
setupContextForAnnotatedListener();
context.publishEvent(new ApplicationFailedEvent(new SpringApplication(), new String[0], context, exception));
context.publishEvent(new ContextClosedEvent(context));
SpringApplication application = new SpringApplication();
context.publishEvent(new ApplicationFailedEvent(application, new String[0], context, exception));
DefaultAnnotationConfiguration.AnnotatedTaskListener annotatedListener =
context.getBean(DefaultAnnotationConfiguration.AnnotatedTaskListener.class);
context.publishEvent(new ApplicationReadyEvent(application, new String[0], context));
TaskExecution taskExecution = new TaskExecution(0, 1, "wombat", new Date(),
new Date(), null, new ArrayList<String>());

View File

@@ -16,11 +16,6 @@
package org.springframework.cloud.task.listener;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -30,21 +25,28 @@ import java.util.Set;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.task.repository.TaskExecution;
import org.springframework.cloud.task.repository.TaskExplorer;
import org.springframework.cloud.task.util.TestDefaultConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/**
* Verifies that the TaskLifecycleListener Methods record the appropriate log header entries and
* result codes.
@@ -67,12 +69,15 @@ public class TaskLifecycleListenerTests {
@After
public void tearDown() {
context.close();
if(context != null && context.isActive()) {
context.close();
}
}
@Test
public void testTaskCreate() {
context.refresh();
this.taskExplorer = context.getBean(TaskExplorer.class);
verifyTaskExecution(0, false, 0, null);
}
@@ -80,14 +85,16 @@ public class TaskLifecycleListenerTests {
public void testTaskCreateWithArgs() {
context.register(ArgsConfiguration.class);
context.refresh();
this.taskExplorer = context.getBean(TaskExplorer.class);
verifyTaskExecution(2, false, 0, null);
}
@Test
public void testTaskUpdate() {
context.refresh();
this.taskExplorer = context.getBean(TaskExplorer.class);
context.publishEvent(new ContextClosedEvent(context));
context.publishEvent(new ApplicationReadyEvent(new SpringApplication(), new String[0], context));
verifyTaskExecution(0, true, 0, null);
}
@@ -96,14 +103,28 @@ public class TaskLifecycleListenerTests {
public void testTaskFailedUpdate() {
context.refresh();
RuntimeException exception = new RuntimeException("This was expected");
context.publishEvent(new ApplicationFailedEvent(new SpringApplication(), new String[0], context, exception));
context.publishEvent(new ContextClosedEvent(context));
SpringApplication application = new SpringApplication();
context.publishEvent(new ApplicationFailedEvent(application, new String[0], context, exception));
this.taskExplorer = context.getBean(TaskExplorer.class);
context.publishEvent(new ApplicationReadyEvent(application, new String[0], context));
verifyTaskExecution(0, true, 1, exception);
}
@Test
public void testNoClosingOfContext() {
ConfigurableApplicationContext applicationContext = SpringApplication.run(new Object[] {TestDefaultConfiguration.class, PropertyPlaceholderAutoConfiguration.class},
new String[] {"--spring.cloud.task.closecontext.enable=false"});
try {
assertTrue(applicationContext.isActive());
}
finally {
applicationContext.close();
}
}
private void verifyTaskExecution(int numberOfParams, boolean update, Integer exitCode, Throwable exception) {
this.taskExplorer = context.getBean(TaskExplorer.class);
Sort sort = new Sort("id");

View File

@@ -1,65 +0,0 @@
/*
* Copyright 2015 the original author or authors.
*
* 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.
*/
package org.springframework.cloud.task.repository.support;
import java.util.Date;
import ch.qos.logback.core.Appender;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.task.repository.TaskExecution;
import org.springframework.cloud.task.repository.TaskRepository;
import org.springframework.cloud.task.util.TaskExecutionCreator;
import org.springframework.cloud.task.util.TestDefaultConfiguration;
import org.springframework.cloud.task.util.TestVerifierUtils;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Verifies that the SimpleTaskRepository has correct prefixes written to logs.
* @author Glenn Renfro
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TestDefaultConfiguration.class)
public class SimpleTaskRepositoryLoggerTests {
@Autowired
private TaskRepository taskRepository;
@Test
public void testCreateTaskExecution() {
final Appender mockAppender = TestVerifierUtils.getMockAppender();
TaskExecution expectedTaskExecution =
TaskExecutionCreator.createAndStoreTaskExecutionNoParams(taskRepository);
TestVerifierUtils.verifyLogEntryExists(mockAppender,
"Creating: TaskExecution{executionId=" + expectedTaskExecution.getExecutionId());
}
@Test
public void testTaskComplete() {
final Appender mockAppender = TestVerifierUtils.getMockAppender();
TaskExecution expectedTaskExecution =
TaskExecutionCreator.createAndStoreTaskExecutionNoParams(taskRepository);
expectedTaskExecution.setEndTime(new Date());
TaskExecutionCreator.completeExecution(taskRepository, expectedTaskExecution);
TestVerifierUtils.verifyLogEntryExists(mockAppender,
"Updating: TaskExecution with executionId="
+ expectedTaskExecution.getExecutionId());
}
}

View File

@@ -37,7 +37,6 @@ start event. This event is triggered via `SmartLifecycle#start` being triggered
Spring Framework. This indicates to the system that all beans are ready for use and is
before the execution of any of the `*Runner`s provided by Spring Boot.
NOTE: The recording of a task will only occur upon the successful bootstrapping of an
`ApplicationContext`. If the context fails to bootstrap at all, the task's execution will
not be recorded.
@@ -46,6 +45,10 @@ Upon completion of all of the `*Runner#run` calls from Spring Boot or the failur
`ApplicationContext` (indicated via a `ApplicationFailedEvent`), the task execution is
updated in the repository with the results.
NOTE: At the completion of a task (all `*Runner#run` methods are called and the task
repository has been updated) the `ApplicationContext` will be closed. This behavior can
be overriden by setting the property `spring.cloud.task.closecontext.enabled` to false.
[[features-task-execution-details]]
=== The TaskExecution
@@ -70,7 +73,7 @@ assumed to be 0.
|The time the task was started as indicated by the `SmartLifecycle#start` call.
|`endTime`
|The time the task was completed as indicated by the `ContextClosedEvent`.
|The time the task was completed as indicated by the `ApplicationReadyEvent`.
|`exitMessage`
|Any information available at the time of exit. If an exception is the cause of the end

View File

@@ -44,9 +44,9 @@ public class BatchJobApplicationTests {
final String CREATE_TASK_MESSAGE = "Creating: TaskExecution{executionId=";
final String UPDATE_TASK_MESSAGE = "Updating: TaskExecution with executionId=";
final String JOB_ASSOCIATION_MESSAGE = "The job execution id ";
final String EXIT_CODE_MESSAGE = "with the following {exitCode=0";
assertEquals(0, SpringApplication.exit(SpringApplication
.run(BatchJobApplication.class)));
SpringApplication.run(BatchJobApplication.class);
String output = this.outputCapture.toString();
assertTrue("Unable to find the timestamp: " + output,
@@ -55,6 +55,8 @@ public class BatchJobApplicationTests {
output.contains(CREATE_TASK_MESSAGE));
assertTrue("Test results do not show success message: " + output,
output.contains(UPDATE_TASK_MESSAGE));
assertTrue("Test results do not show success message: " + output,
output.contains(EXIT_CODE_MESSAGE));
int i = output.indexOf(JOB_ASSOCIATION_MESSAGE);

View File

@@ -16,17 +16,18 @@
package org.springframework.cloud.task.timestamp;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.test.OutputCapture;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* Verifies that the Task Application outputs the correct task log entries.
*
@@ -42,10 +43,10 @@ public class TaskApplicationTests {
final String TEST_DATE_DOTS = ".......";
final String CREATE_TASK_MESSAGE = "Creating: TaskExecution{executionId=";
final String UPDATE_TASK_MESSAGE = "Updating: TaskExecution with executionId=";
final String EXIT_CODE_MESSAGE = "with the following {exitCode=0";
String[] args = { "--format=yyyy" + TEST_DATE_DOTS };
assertEquals(0, SpringApplication.exit(SpringApplication
.run(TaskApplication.class, args)));
SpringApplication.run(TaskApplication.class, args);
String output = this.outputCapture.toString();
assertTrue("Unable to find the timestamp: " + output,
@@ -54,6 +55,8 @@ public class TaskApplicationTests {
output.contains(CREATE_TASK_MESSAGE));
assertTrue("Test results do not show success message: " + output,
output.contains(UPDATE_TASK_MESSAGE));
assertTrue("Test results have incorrect exit code: " + output,
output.contains(EXIT_CODE_MESSAGE));
String taskTitle = "Demo Timestamp Task";
Pattern pattern = Pattern.compile(taskTitle);
@@ -62,7 +65,6 @@ public class TaskApplicationTests {
while (matcher.find()) {
count++;
}
assertEquals("The number of task titles did not match expected: ", 3, count);
assertEquals("The number of task titles did not match expected: ", 1, count);
}
}

View File

@@ -14,5 +14,5 @@
# limitations under the License.
#
logging.level.root=DEBUG
logging.level.org.springframework.cloud.task=DEBUG
spring.application.name=Demo Timestamp Task