A small library that runs multiple tasks in parallel. It works with plain Java and integrates easily with Spring, but has no Spring dependency.
TaskRunner takes a list of TaskRunnerInterface tasks (for example all
Spring-managed beans) and executes them concurrently on a fixed thread pool.
Exceptions thrown inside a task are wrapped and propagated back to the caller,
and each worker thread is renamed to task-<ClassName>-<ThreadId> to make logs
easier to read.
The interface is the filter. Each task implements a marker interface that
says when or in which context it is responsible (for example NoonTask or
MidnightTask). runAll(MidnightTask.class) runs exactly the tasks
implementing MidnightTask and skips all others. This way you hand every task
to the runner once and decide per call which group runs.
- Java 17+
slf4j-api(the only dependency)- Spring is optional. The Spring examples below use Spring Boot 4.x.
Note
For testing only. For the time being, the library is published as a
SNAPSHOT to the Maven Central snapshot repository. Use it to try the
library out. For production, copy the source code into your own project (see
below).
Snapshots are not resolved from Maven Central itself. Add the snapshot
repository and the dependency to your pom.xml. No authentication is required.
<repositories>
<repository>
<id>central-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<dependency>
<groupId>io.github.alaugks</groupId>
<artifactId>task-runner</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>The library consists of only three classes, TaskRunner,
TaskRunnerInterface and TaskRunnerRuntimeException. Copy them into your own project. Keep the license and the
SPDX-License-Identifier header (see License).
Group related tasks behind a shared interface that extends
TaskRunnerInterface. The interface acts as a filter: the runner only runs
the tasks implementing the interface you pass to runAll. Use one interface
per group of tasks that should run together, for example per schedule.
package com.example.tasks;
import io.github.alaugks.taskrunner.TaskRunnerInterface;
public interface NoonTask extends TaskRunnerInterface {
}
public interface MidnightTask extends TaskRunnerInterface {
}You can define as many marker interfaces as you need and call
runAll(Class<T> taskClass) separately for each one.
Each task implements the marker interface of the group it belongs to. The
run() method contains the task logic. SendMessageTask belongs to the
NoonTask group, CleanupTempFilesTask and RefreshCacheTask to the
MidnightTask group.
package com.example.tasks;
public class SendMessageTask implements NoonTask {
@Override
public void run() {
// ...
}
}
public class CleanupTempFilesTask implements MidnightTask {
@Override
public void run() {
// ...
}
}
public class RefreshCacheTask implements MidnightTask {
@Override
public void run() {
// ...
}
}Pass all tasks to the builder and call runAll(Class<T> taskClass)
with the marker interface. The interface filters the tasks: every task
implementing it runs concurrently, all others are ignored. The call returns
once all matching tasks finish. The pool size is optional and defaults to 10.
package com.example;
import io.github.alaugks.taskrunner.TaskRunner;
import io.github.alaugks.taskrunner.TaskRunnerInterface;
import com.example.tasks.CleanupTempFilesTask;
import com.example.tasks.MidnightTask;
import com.example.tasks.NoonTask;
import com.example.tasks.RefreshCacheTask;
import com.example.tasks.SendMessageTask;
import java.util.List;
public class Application {
public static void main(String[] args) {
List<TaskRunnerInterface> tasks = List.of(
new SendMessageTask(),
new CleanupTempFilesTask(),
new RefreshCacheTask()
);
TaskRunner taskRunner = TaskRunner.builder(tasks)
.poolSize(4) // optional, default: 10
.build();
// Runs only the tasks implementing MidnightTask
// (CleanupTempFilesTask, RefreshCacheTask)
taskRunner.runAll(MidnightTask.class);
// Runs only the tasks implementing NoonTask (SendMessageTask)
taskRunner.runAll(NoonTask.class);
}
}Annotate each task with @Component:
package com.example.tasks;
import org.springframework.stereotype.Component;
@Component
public class SendMessageTask implements NoonTask {
@Override
public void run() {
// ...
}
}
@Component
public class CleanupTempFilesTask implements MidnightTask {
@Override
public void run() {
// ...
}
}
@Component
public class RefreshCacheTask implements MidnightTask {
@Override
public void run() {
// ...
}
}Create the runner with its builder and expose it as a bean. The builder takes
all tasks; Spring injects every bean implementing
TaskRunnerInterface. The pool size is optional and defaults to 10.
package com.example.tasks;
import io.github.alaugks.taskrunner.TaskRunner;
import io.github.alaugks.taskrunner.TaskRunnerInterface;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TaskRunnerConfig {
@Bean
public TaskRunner taskRunner(List<TaskRunnerInterface> tasks) {
return TaskRunner.builder(tasks)
.poolSize(4) // optional, default: 10
.build();
}
}Inject TaskRunner and call runAll(Class<T> taskClass) with the marker
interface. The interface filters the beans: every bean implementing it runs
concurrently, all others are ignored. To run a task group on a schedule,
enable Spring scheduling and annotate a method with @Scheduled. The example
below runs NoonTask every day at 12:00 and MidnightTask every day at 00:00.
Enable scheduling once in your application. Then add a scheduled trigger that delegates to TaskRunner.
package com.example.tasks;
import io.github.alaugks.taskrunner.TaskRunner;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
@EnableScheduling
public class ScheduledTasks {
private final TaskRunner taskRunner;
public ScheduledTasks(TaskRunner taskRunner) {
this.taskRunner = taskRunner;
}
@Scheduled(cron = "0 0 12 * * *", zone = "Europe/Berlin")
public void runNoon() {
this.taskRunner.runAll(NoonTask.class);
}
@Scheduled(cron = "0 0 0 * * *", zone = "Europe/Berlin")
public void runMidnight() {
this.taskRunner.runAll(MidnightTask.class);
}
}To adjust the schedule per environment, move the cron expressions to
application.yml:
tasks:
noon:
cron: "0 0 12 * * *"
midnight:
cron: "0 0 0 * * *"and reference them in @Scheduled:
@Scheduled(cron = "${tasks.noon.cron}", zone = "Europe/Berlin")
public void runNoon() {
this.taskRunner.runAll(NoonTask.class);
}- Filtering:
runAll(Class<T> taskClass)runs every task that is an instance oftaskClass. A task implementing several marker interfaces runs whenever one of them is passed. - Concurrency: tasks run on a fixed thread pool. The size is set with
poolSize(int poolSize)on the builder and defaults to 10. - Thread names: each worker thread is named
task-<TaskClassName>-<ThreadId>for the duration of the task. - Error handling:
runAllwaits for the tasks in order. If a task throws, the exception is wrapped in aTaskRunnerRuntimeException(the original exception is its cause). If the calling thread is interrupted, the interrupt flag is restored and aTaskRunnerRuntimeExceptionis thrown. Tasks that are already running are not cancelled. - Logging: task start is logged at
DEBUGvia SLF4J under theio.github.alaugks.taskrunner.TaskRunnerlogger.
mvn clean installLicensed under the Apache License, Version 2.0.