Skip to content
Frieder edited this page Feb 6, 2024 · 2 revisions

Tasks

Motivation

In Java FX the UI runs in one thread, called the FXApplicationThread. In order to keep the UI fluent, work must be forked into the background. Usually in Correo work is triggered from the UI and will wait asynchronous for the results.

This is solved with tasks.

How to use tasks?

A task must be instantiated and ran afterwards.

This will look like this:

publishTaskFactory.create(getConnectionId(), messageDTO)
           .onSuccess(this::onSuccess)
           .onError(this::onError)
           .run();

All tasks must implement the Task interface. There are some generics you should know about.

Generics

Generics Description
<T> Type of result if the task succeeded.
<P> Type of data that is send during progress.
<E> Type of result in case of task failure. This is an enum often.

Chained listener methods

Method name Signature of Callback Description
onStart(Callback) () -> void Will be triggered before the task will be executed.
onProgress(Callback) (P) -> void Might be triggered never or multiple times, if the task supports progress.
onSuccess(Callback) (T) -> void Will be called if the task succeeded without error
onError(Callback) (TaskErrorResult<E>) -> void Will be called if the execution of the task produced an error. For more information see error handling.
onFinally(Callback) () -> void Will be called, no matter if the task succeeded or failed.

Note: Alle callbacks are automatically executed in the FXApplicationThread. So no need to run Platform.runLater manually here.

Execute methods

Method name Description
run() Executes the task.

How to handle task results?

Each task automatically supports the future-like chain methods listed above. This is the preferred way to handle results. Nevertheless if your task produces events via EventBus you might want to listen to these instead using the future-like methods.

In every case it make sense to implement at least the onError callback, in order to be able to give your users feedback in worst case.

Error Handling

The onError callback is called with a TaskErrorResult<E>. While E is defined as the custom error object specific for the current task, the TaskErrorResult contains more information.

Method name Description
boolean isExpected() true if the error was expected and an error object with type E exists, otherwise false.
E getExpectedError() Returns the expected error object if it exists. This is usally an enum.
Throwable getUnexpectedError() Gives you the exception that was thrown unexpected. If you don't have custom expected error cases etc. you should at least implement a handling for the unexpected errors. Nevertheless if you don't do so. Correo will print a warning and catch those exceptions to display the message in an alert dialog.

Full usage example

    @Inject
    public MyClass(PublishTaskFactory publishTaskFactory){
      this.publishTaskFactory = publishTaskFactory;
    }

    @FXML
    public void onButtonClick(){
        publishTaskFactory.create(getConnectionId(), messageDTO)
               .onSuccess(this::onSuccess)
               .onProgress(this::onProgress)
               .onError(this::onError)
               .run();
    }

    private void onSuccess(MyResultDTO myResultDTO){
      descriptionLabel.setText(myResultDTO.getStatusDescription());
    }

    private void onProgress(Integer status){
      statusLabel.setText(status);
    }

    private void onError(TaskErrorResult<MyTask.Error> errorResult){
        switch(errorResult.getExpectedError()){
            case EMPTY -> AlertHelper.error("Empty message.");
            case IOERROR -> AlertHelper.error("Unable to read file.");
            default -> {}
        }

        if(errorResult.getUnexpectedError() != null){
            AlertHelper.unexpectedError(errorResult.getUnexpectedError());
        }
    }

Create your own tasks

Whenever you do work it is required that you don't do this in the FXApplicationThread. Whenever possible use a task.

In order to achieve this it is quiet easy to create your own tasks.

Basically a task must implement the Task interface. As this is only an interface there are different implementations for several use cases. The main method you must implement is the execute method, which does the real work of the task. Everything else is optional.

Simple Tasks

Usually one does not need the full feature set.

SimpleTask Execute, but result does not matter.

A SimpleTask is a task, that neither has a response type T nor a specific error type E nor does provide any sort of progress.

import org.correomqtt.business.concurrent.SimpleTask

class MyTask extends SimpleTask {

    public void execute(){
        // do something
    }

}

Usally a task needs some input data. Those input data will be given into the constructor. So the example above will look like this:

import org.correomqtt.business.concurrent.SimpleResultTask

class MyTask extends SimpleTask {

    private final String myParameter1;
    private final Integer myParameter2

    public MyTask(String myParameter1, Integer myParameter2){
        this.myParameter1 = myParameter1;
        this.myParameter2 = myParameter2;
    }

    public MyResultDTO execute(){
        // do something with the attributes to produce a MyResultDTO
        return myResultDTO;
    }

}

SimpleErrorTask Only the error matters.

If you do not have a result type, but different error cases: SimpleErrorTask.

import org.correomqtt.business.concurrent.SimpleErrorTask

class MyTask extends SimpleErrorTask<MyTask.Error> {

    public enum Error {
        EMPTY,
        IOERROR
    }

    private final String filename;

    public MyTask(String filename) {
        this.filename = filename;
    }

    public void execute(){
        if(filename == null || filename.isEmpty()){
            throw new TaskException(Error.EMPTY);
        }

        try {
            // do something e.g. with the filesystem
        } catch(IOException e){
            // log the IOException
            throw new TaskException(Error.IOERROR);
        }
    }
}

SimpleProgressTask Only the progress matters.

Maybe a rare use case, but if you only want to talk about progress and not about result or errors SimpleProgressTask should be used.

import org.correomqtt.business.concurrent.SimpleProgressTask

class MyTask extends SimpleProgressTask<Integer> {

    public MyTask(...){
        ...
    }

    public void execute(){
        for(int step = 0; step < 100; step++){
            // do the step, which might take some time
            reportProgress(step);
        }
    }
}

While this is a simple example with just an Integer, the progess type P might be more complex of course.

Advanced tasks

In the last chapter we saw different use cases of tasks. Providing results, producing progress and throw errors. Of course these can be mixed together.

NoProgressTask Result and error

import org.correomqtt.business.concurrent.NoProgressTask<MyResultDTO,MyTask.Error> {

class MyTask extends NoProgressTask<MyResultDTO,MyTask.Error> {

    public enum Error {
        EMPTY,
        IOERROR
    }

    private final String filename;

    public MyTask(String filename) {
        this.filename = filename;
    }

    public MyResultDTO execute(){
        if(filename == null || filename.isEmpty()){
            throw new TaskException(Error.EMPTY);
        }

        try {
            // do something e.g. with the filesystem
            // produce a MyResultDTO
            return myResultDTO;
        } catch(IOException e){
            // log the IOException
            throw new TaskException(Error.IOERROR);
        }
    }
}

FullTask Everything

import org.correomqtt.business.concurrent.NoProgressTask<MyResultDTO,MyTask.Error> {

class MyTask extends NoProgressTask<MyResultDTO,Integer,MyTask.Error> {

    public enum Error {
        EMPTY,
        IOERROR
    }

    private final String filename;

    public MyTask(String filename) {
        this.filename = filename;
    }

    public MyResultDTO execute(){
        if(filename == null || filename.isEmpty()){
            throw new TaskException(Error.EMPTY);
        }

        try {
            // do something e.g. with the filesystem
            // produce a MyResultDTO

            for(int step = 0; step < 100; step++){
                // do the step, which might take some time
                reportProgress(step);
            }

            return myResultDTO;
        } catch(IOException e){
            // log the IOException
            throw new TaskException(Error.IOERROR);
        }
    }
}

Task Hooks

If you implement a task it is possible to hook into the task lifecycle by overriding methods.

class MyTask extends FullTask<T,P,E> {

    ...

    protected void beforeHook() {
        // Called before everything is started
    }

    protected void successHook(T result) {
        // Called after the execute method returned without error
    }

    protected void errorHook(TaskErrorResult<E> errorResult) {
        // Called if an error occured during execution.
        // This will contain expected errors as well as unexpected exceptions
        // In case of tasks without custom errors, the error result is a SimpleTaskErrorResult,
        // which is not generic and does contain an exception only.
    }

    protected void finalHook() {
        // Called after the execution finished, not matter if with error or with success.
    }

    ...
}