Docs

Documentation versions (currently viewingVaadin 24)

Handling Long-Running Tasks

How to handle long-running tasks in Vaadin applications.
Important
This page is being migrated to the new Building Applications section. See the Background Jobs and Server Push documentation pages.

Often, server-side tasks can take a long time to complete. For example, a task that requires fetching a large amount of data from the database can take a long time to finish. In such cases, a poorly designed application can freeze the UI and prevent the user from interacting with the application.

This guide shows you how to handle long-running tasks in Vaadin applications in a way that:

  1. Allows the user to continue interacting with the application while the task is running, possibly allowing the user to cancel the task.

  2. Allows the user to see the progress of the task.

The examples used here are based on Spring Boot, but a similar approach can be used with other technologies.

The Problem with Long-Running Tasks

To illustrate how a poorly designed handling of long-running tasks can affect users, consider the following backend service that simulates a long-running task.

@Service
public class BackendService {

    public String longRunningTask() {
        try {
            // Simulate a long running task
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "Some result";
    }

}

Consider also that the BackendService.longRunningTask() method is being called from the following Vaadin UI:

@Route("")
public class MainView extends VerticalLayout {

    public MainView(BackendService backendService) {
        Button startButton = new Button("Start long-running task", clickEvent -> {
            String result = backendService.longRunningTask();
            Notification.show(result);
        });

        Button isBlockedButton = new Button("Is UI blocked?", clickEvent -> {
            Notification.show("UI isn't blocked!");
        });

        add(startButton, isBlockedButton);
    }

}

In this example, if the user clicks the "Start long-running task" button, the application freezes the UI and prevent the user from interacting with the other parts of the application (for example, with the isBlockedButton). This happens because Vaadin is waiting for the long-running task to finish before it sends the response back to the user, at which point the user can continue interacting with the application.

Handling Long-Running Tasks Asynchronously

The recommended way to handle long-running tasks is to use an asynchronous approach. This means that the long-running task is executed in a separate thread, and the UI isn’t blocked while the task is running.

An asynchronous model can be achieved in several ways. But in the context of a Spring Boot application, one way is to use the @Async annotation. The @Async annotation is used to mark a method as an asynchronous task. From the Vaadin UI side, a response to the user request is immediately sent back to the browser, and thus the user can continue interacting with the application right away without being blocked by the long-running task. When the asynchronous task is finished, Vaadin uses Server Push to let the user know that the task is completed.

The following example shows how the BackendService.longRunningTask() method can be adjusted to run asynchronously in a separate thread.

@Service
public class BackendService {

    @Async 1
    public ListenableFuture<String> longRunningTask() { 2
        try {
            // Simulate a long running task
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return AsyncResult.forValue("Some result"); 3
    }

}
  1. @Async annotation to mark the method for asynchronous execution.

  2. The method now returns a ListenableFuture object.

  3. The method’s return value is a ListenableFuture object that contains the result of the asynchronous task.

Now the BackendService.longRunningTask() method is annotated with the @Async annotation, and the long-running task is executed in a separate thread. The BackendService.longRunningTask() method now returns a ListenableFuture<String> instead of a String (returning a ListenableFuture or a CompletableFuture is a requirement for any asynchronous service). The ListenableFuture is a special type of Future that allows the caller to register a callback to be notified when the task is completed.

With these changes in place, you can change the UI to allow the user to start the long-running task and still be able to interact with the application. Vaadin can then use the ListenableFuture and the UI.access() method of Server Push to notify the user when the task is completed. This is how MainView.java could look now:

@Route("")
public class MainView extends VerticalLayout {

    public MainView(BackendService backendService) {
        Button startButton = new Button("Start long-running task", clickEvent -> {
            UI ui = clickEvent.getSource().getUI().orElseThrow(); 1
            ListenableFuture<String> future = backendService.longRunningTask();
            future.addCallback(
                    successResult -> updateUi(ui, "Task finished: " + successResult), 2
                    failureException -> updateUi(ui, "Task failed: " + failureException.getMessage()) 3
            );
        });

        Button isBlockedButton = new Button("Is UI blocked?", clickEvent -> {
            Notification.show("UI isn't blocked!");
        });

        add(startButton, isBlockedButton);
    }

    private void updateUi(UI ui, String result) { 4
        ui.access(() -> {
            Notification.show(result);
        });
    }

}
  1. Save the current UI in a local variable, so that you can use it later to update the UI through the UI.access() method.

  2. The callback is called when the task is completed successfully.

  3. The callback is called if the task failed.

  4. The UI.access() method is used to update the UI in a thread-safe manner through server-side push.

You’re still not done. For the above example to work as intended, you need two extra annotations for the @Async annotation and the UI.access() method to work.

  • For the @Async annotation, you need to add the @EnableAsync annotation to the application.

  • For the UI.access() method, you need to add the @Push annotation to the class implementing the AppShellConfigurator interface.

You can make both changes in the same class as illustrated in the following Application class (which both extends SpringBootServletInitializer and implements AppShellConfigurator):

@SpringBootApplication
@Push
@EnableAsync
public class Application extends SpringBootServletInitializer implements AppShellConfigurator {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

Showing Progress to the User

With the Vaadin component Progress Bar, you can provide an indicator that a long-running action is currently in progress. The following adjusts the above MainView example to show a progress bar when the user clicks the "Start long-running task" button.

@Route("")
public class MainView extends VerticalLayout {

    private ProgressBar progressBar = new ProgressBar(); 1

    public MainView(BackendService backendService) {
        progressBar.setWidth("15em");
        progressBar.setIndeterminate(true);
        progressBar.setVisible(false); 2

        Button startButton = new Button("Start long-running task", clickEvent -> {
            UI ui = clickEvent.getSource().getUI().orElseThrow();
            ListenableFuture<String> future = backendService.longRunningTask();

            progressBar.setVisible(true); 3

            future.addCallback(
                    successResult -> updateUi(ui, "Task finished: " + successResult),
                    failureException -> updateUi(ui, "Task failed: " + failureException.getMessage())
            );
        });

        Button isBlockedButton = new Button("Is UI blocked?", clickEvent -> {
            Notification.show("UI isn't blocked!");
        });

        add(startButton, progressBar, isBlockedButton);
    }

    private void updateUi(UI ui, String result) {
        ui.access(() -> {
            Notification.show(result);
            progressBar.setVisible(false); 4
        });
    }

}
  1. First, create a ProgressBar object.

  2. After configuring the ProgressBar, hide it by default.

  3. Show the ProgressBar when the task is started.

  4. When the long-running task is completed or errors out, hide the ProgressBar again.

Here is the animation of the MainView showing the progress bar.

Long-Running Task with ProgressBar

Canceling a Long Running Task

For your task to be cancellable, the following conditions must be met:

  1. Your @Async method must return a Future.

  2. The running task must be cancellable.

The modified MainView class below shows how to add a Button to cancel the long-running task.

@Route("")
public class MainView extends VerticalLayout {

    private ProgressBar progressBar = new ProgressBar();
    private Button cancelButton = new Button("Cancel task execution");

    public MainView(BackendService backendService) {
        progressBar.setWidth("15em");
        progressBar.setIndeterminate(true);

        progressBar.setVisible(false);
        cancelButton.setVisible(false); 1

        Button startButton = new Button("Start long-running task", clickEvent -> {
            UI ui = clickEvent.getSource().getUI().orElseThrow();
            ListenableFuture<String> future = backendService.longRunningTask();

            progressBar.setVisible(true);
            cancelButton.setVisible(true); 2
            cancelButton.addClickListener(e -> future.cancel(true)); 3

            future.addCallback(
                    successResult -> updateUi(ui, "Task finished: " + successResult),
                    failureException -> updateUi(ui, "Task failed: " + failureException.getMessage())
            );
        });

        Button isBlockedButton = new Button("Is UI blocked?", clickEvent -> {
            Notification.show("UI isn't blocked!");
        });

        add(startButton, new HorizontalLayout(progressBar, cancelButton), isBlockedButton);
    }

    private void updateUi(UI ui, String result) {
        ui.access(() -> {
            Notification.show(result);
            progressBar.setVisible(false);
            cancelButton.setVisible(false); 4
        });
    }

}
  1. Like the ProgressBar, hide the Cancel Button by default.

  2. Show the Cancel Button when the task is started.

  3. The Future representing the long-running task is canceled when the Cancel Button is clicked.

  4. When the task is completed or canceled, hide the cancel Button.

Here is the animation of the MainView with a Cancel Button.

Long-Running task with ProgressBar and cancel Button

C15BD166-7C06-4C9E-8686-6FCDCDF31CE1