jmix-framework / jmix

Jmix framework
https://www.jmix.io
Apache License 2.0
561 stars 121 forks source link

Simplified API for UI asynchronous tasks #3425

Closed gorbunkov closed 1 month ago

gorbunkov commented 3 months ago

Overview

The new API is intended to be used instead of Background Task in simple cases. Background tasks are powerful, but sometimes users don't need all their featues like showing a modal dialog or handling the task execution progress.

For a problem that sounds like "load some data in the background and after the data is loaded update some UI component" the new UiAsyncTasks service may be used.

Usage examples:

        uiAsyncTasks.supplierConfigurer(() -> {
                        return customerService.loadAllCustomersForCurrentUser();
                })
                .withResultHandler(customers -> {
                    customersDc.getMutableItems().addAll(customers);
                    notifications.create("Customers loaded: " + customers.size())
                            .show();
                })
                .supplyAsync();

When the asynchronous task returns no result, the runAsyncBuilder may be used:

        uiAsyncTasks.runnableConfigurer(() -> customerService.doSomeTaskWithNoResult())
                .withResultHandler(() -> {
                    resultTextField.setValue("Async task completed successfully");
                })
                .runAsync();

Both supplyAsync() and runAsync() method return a CompletableFuture.

For the thread that executes the main asynchronous task the current authentication is copied to the SecurityContext, so the code will be executed with the permissions of the user who is working with the UI View.

The code that handles the result may access Vaadin components because the UiAsyncTasks service wraps it into the UI.access(() -> ...).

Exception Handling

The default exception handler will write a stacktrace to the application log. If you need a custom behavior, use the withExceptionHandler() method.

        uiAsyncTasks.supplyAsyncBuilder(() ->
                        someService.loadData()
                )
                .withResultHandler(result -> {
                    updateUi(result);
                })
                .withExceptionHandler(ex -> {
                    errorTextField.setValue(ExceptionUtils.getStackTrace(ex));
                })
                .supplyAsync();

Timeout

Timeout may be set for the individual CompletableFuture:

        uiAsyncTasks.supplyAsyncBuilder(() ->
                        customerService.loadAllCustomersForCurrentUser()
                )
                .withResultHandler(customers -> {
                    customersDc.getMutableItems().addAll(customers);
                    notifications.create("Customers loaded: " + customers.size())
                            .show();
                })
                .withTimeout(60, TimeUnit.SECONDS)
                .withExceptionHandler(ex -> {
                    String errorText;
                    if (ex instanceof TimeoutException) {
                        errorText = "Timeout exceeded";
                    } else {
                        errorText = ExceptionUtils.getStackTrace(ex);
                    }
                    errorTextField.setValue(errorText);
                })
                .supplyAsync();

When the timeout is exceeded the TimeoutException is thrown. The TimeoutException can be processed in the withExceptionHandler() method.

If no explicit timeout is defined, then the default timeout value from the application properties is used.

Configuration Properties

# default timeout value for CompletableFutures
jmix.ui.async-task.default-timeout-sec = 300

# Maximum thread pool size for the executor service used for asynchronous tasks execution
jmix.ui.async-task.executor-service.maximum-pool-size = 10