Guide to Spring Cloud Task: Short-Lived Spring Boot Microservices

Introduction

Microservices are being developed all around us nowadays. Many of these services are short-lived. Scheduled tasks, data synchronization, data aggregation, report generation and similar services are short-lived. They are typically expected to start, run to completion and end.

Many external applications and schedulers have been built to meet this purpose, however, sometimes you need a custom task that requires deep integration with your organization's application.

The Spring Boot platform makes this functionality available to developers via the Spring Cloud Task API.

What is Spring Cloud Task?

Normally, it is expected that services are long-running. An average Spring Boot service includes an embedded web server like Tomcat, Jetty or Undertow. A service is ended because it is stopped on purpose or a runtime error like an OOM (OutOfMemoryError) occurred.

Spring Boot was built this way, but as paradigms shifted and the microservice architecture became popular - many services became short-lived. This was overkill as a short-lived notification service doesn't need to have an embedded server and could be a lot more lightweight.

Spring Cloud Task is Spring’s answer to the problem of short-lived microservices in Spring Boot.

With Spring Cloud Task, you get an on-demand JVM process that performs a task and immediately ends.

In this article, we'll be linking a lot to the official Spring Cloud Task project available on Github.

Spring Cloud Task Technical Architecture

Spring Cloud Task uses a few annotations to set up the system and a database (at least for production) to record the result of each invocation.

To make your Spring Boot application a cloud task, you need to annotate one of your application's configuration classes with @EnableTask.

This annotation imports the TaskLifecycleConfiguration into your project. The TaskLifecycleConfiguration class is the configuration class for the TaskLifecycleListener, TaskRepository and other useful classes needed to bring all the functionalities of Spring Cloud Task to life.

Implementation

Spring Initializr

A good way to bootstrap your skeleton Spring Boot project is to use Spring Initializr. Select your preferred database dependency, the Spring Cloud Task dependency and the Spring Data JPA dependency:

spring initializr dependencies

If you've already got a project running, using Maven, add the adequate dependencies:

<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-task</artifactId>
     <version>${version}</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>${version}</version>
</dependency>

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>${version}</version>
</dependency>

The spring-cloud-starter-task dependency includes spring-boot-starter, spring-cloud-task-core, spring-cloud-task-batch and spring-cloud-task-stream.

The spring-cloud-task-core dependency is the main one we'll be using - you can import the spring-boot-starter and the former dependency separately.

Alternatively, if you're using Gradle:

compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-task', version: '2.2.3.RELEASE'

Configuring the Application

To register an app as a Spring Cloud Task, you have to annotate one of the configuration classes with @EnableTask:

@EnableTask
@SpringBootApplication
public class SampleSpringCloudTaskApplication {
    public static void main(String[] args) {
        SpringApplication.run(SampleSpringCloudTaskApplication.class, args);
    }
}

Since the @SpringBootApplication annotation is a combination of @EnableAutoConfiguration, @Configuration and @ComponentScan, it's perfectly fine to annotate the main class with the @EnableTask annotation.

TaskRepository

A task can be created without a database. In that case, it uses an in-memory instance of H2 to manage the task repository events. This is fine for development, but not production. For production, a data source is recommended. It preserves all records of your task executions and errors.

All task events are persisted using the TaskRepository.

In a single-datasource project, Spring Cloud Task creates the task tables in the specified database.

However, in a multi-datasource project, you have to choose the datasource to use with Spring Cloud Task. An example of a multi-datasource project can be found in the Spring Cloud Task sample projects.

In the example, 2 datasources are specified in the DataSoureConfiguration class:

@Configuration
public class DataSourceConfiguration {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .build();
    }

    @Bean
    public DataSource secondDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .build();
    }
}

In order to specify the data source to be used by Spring Cloud Task, a CustomTaskConfigurer component is created. It extends DefaultTaskConfigurer, passing the qualified data source in the constructor:

@Component
public class CustomTaskConfigurer extends DefaultTaskConfigurer {
    @Autowired
    public CustomTaskConfigurer(@Qualifier("secondDataSource") DataSource dataSource)  {
        super(dataSource);
    }
}

Unless a custom TaskRepository is specified by extending the SimpleTaskRepository, the SimpleTaskRepository is the default TaskRepository used by Spring Cloud Task.

TaskConfigurer

The TaskConfigurer is used for customizing Spring Cloud Task configurations. DefaultTaskConfigurer is the default configurer used if no custom task configurer that implements the TaskConfigurer interface is provided.

DefaultTaskConfigurer provides map-based components if no data source is available and a JDBC-components if a data source is provided.

TaskExplorer

The TaskExplorer, as its name suggests, is an explorer for task executions. It is useful for gathering actual task information from the task repository.

By default, Spring Cloud Tasks uses the SimpleTaskExplorer.

From the TaskExplorer, you can query a lot of useful information about the task executions like total count of TaskExecutions, currently running TaskExecutions , finding all TaskExecutions, etc.

TaskExecution

TaskExecution is the state of the Task for each Execution. All the information stored in the TaskRepository is modeled in this class. It is the basic unit for every task.

Some of the information stored is

  • executionId - Unique id associated with the task execution.
  • exitcode - Recorded exit code for the task.
  • taskName - User-defined name for the task.
  • startTime - Task start time.
  • endTime - Timestamp of task completion.
Running a Task

To run our task, we have to implement the Runner interface and provide it as a bean in our configuration class.

Usually, CommandLineRunner or ApplicationRunner is implemented:

@Component
public class SampleCommandLineRunner implements CommandLineRunner {
    @Override
     public void run(String...args) throws Exception {
         System.out.println("Specified Task");
     }
}

    @Configuration
    public class TaskConfiguration {
     @Bean
     public SampleCommandLineRunner sampleCommandLineRunner() {
          return new SampleCommandLineRunner();
     }
}

And with that, from a main method, we can call the SampleCommandLineRunner:

@SpringBootApplication
public class SomeApplication {
    public static void main(String[] args) {
        SpringApplication.run(SampleCommandLineRunner.class, args);
    }
}

TaskExecutionListener

All TaskExecutions have a life cycle. Spring Cloud Task records these events. At the beginning of a task, before any Runner interface implementations have been run, an entry in the TaskRepository that records the start event is created.

When a task is completed or failed, another event is emitted. TaskExecutionListener allows you to register listeners to listen for this event throughout the lifecycle.

You can specify as many listeners as you want to the same event.

Spring provides two approaches for doing this - using the TaskExecutionListener interface or the method bean annotation approach.

For the former, you provide a component that implements the TaskExecutionListener interface and its three methods:

void  onTaskStartup(TaskExecution  taskExecution);
void  onTaskEnd(TaskExecution  taskExecution);
void  onTaskFailed(TaskExecution  taskExecution, Throwable  throwable);
  • onTaskStartup() - Invoked after the TaskExecution has been stored in the TaskRepository.

  • onTaskEnd() - Invoked after the TaskExecution has been updated in the TaskRepository, upon task end.

  • onTaskFailed() - Invoked if an uncaught exception occurs during task execution.

On the other hand, using the method bean annotation approach, you create a component and supply it as a bean in your Spring configuration:

@Configuration
public class AppConfigurtion {

    @Bean
    public AppTaskListener appTaskListener() {
        return new AppTaskListener();
    }
}

@Component
class AppTaskListener {
    @BeforeTask
    public void beforeTaskInvocation(TaskExecution taskExecution) {
        System.out.println("Before task");
    }

    @AfterTask
    public void afterTaskInvocation(TaskExecution taskExecution) {
        System.out.println("After task");
    }

    @FailedTask
    public void afterFailedTaskInvocation(TaskExecution taskExecution, Throwable throwable) {
        System.out.println("Failed task");
    }
}
  • @BeforeTask is analogous to onTaskStartup().
  • @AfterTask is analogous to onTaskEnd().
  • @FailedTask is analogous to onTaskFailed().

Task Exit Messages

Although you can specify any number of listeners to a particular event, if an exception is thrown by a TaskExecutionListener event handler, all listener processing for that event handler stops.

For example, if three onTaskStartup() listeners have started and the first onTaskStartup() event handler throws an exception, the other two onTaskStartup methods are not called.

However, the other event handlers (onTaskEnd() and onTaskFailed()) for the TaskExecutionListeners are called.

When exceptions occur, the TaskExecutionListener returns an exit code and an exit message. Annotating a method with @AfterTask will allow us to set the exit message:

    @AfterTask
    public void afterError(TaskExecution taskExecution) {
        taskExecution.setExitMessage("Custom Exit Message");
    }

An Exit Message can be set at any of the listener events, though only the relevant messages will be set. If a task runs successfuly, the onTaskFailed() event will not fire. When the task ends, the message from the onTaskEnd() event is set.

Customizing Cloud Task

A lot of properties can be overridden by the TaskConfigurer by specifying custom values in the applications.properties or applications.yaml file.

Spring cloud task properties are prefixed with spring.cloud.task in the applications.properties file.

Some of the properties that can be overridden are:

  • spring.cloud.task.tablePrefix - This is the table prefix for the task tables for TaskRepository. The default table prefix is "TASK_".
  • spring.cloud.task.initialize-enabled=false - This is used to enable or disable the task tables creation at task startup. It defaults to true.
  • spring.cloud.task.executionid=yourtaskId - This is used to configure Spring Cloud Task to use your custom task id. By default, spring generates a task execution Id for each task execution.

To see more customizable properties, check out askProperties.

Logging Cloud Task Events

It is usually useful during development to see your application debug logs. In order to change the logging level for a Spring Cloud Task, add this to your applications.properties file:

logging.level.org.springframework.cloud.task=DEBUG

Conclusion

In this article, we introduced Spring Cloud Task, what it is and the problems it solves. We also covered examples of how to set it up with a data source and run a task with the Runner interfaces.

Also, we explained the Spring Cloud Task architecture and all the domain models like TaskExecution, TaskExecutionListener, etc. used to achieve all the functionalities.