javafxservice

Task succeeded never called


I'm writing a "headless" JavaFx application that is controlled remotely. For certain commands I have to create a Service to make some simulations in parallel and afterwards create some plots in a Stage make some Snapshots and send the result via stream.

The problem I'm facing is that even if the call method is finished, the task doesn't move to the succeeded state and hence the succeeded method is never called. Actaully the task remaines in the SCHEDULED state (and then gets garbage collected?).

The following simple example illustrates the behavoir:

import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.stage.Stage;

public class TestClass extends Application {

    BooleanProperty finished = new SimpleBooleanProperty(false);

    public static void main(String[] args) {
        launch(args);
    }
    @Override
    public void start(Stage stage) throws Exception {
        while (!finished.get()) {
            Thread.sleep(1000);
            Service<Void> service = new Service<Void>() {
                @Override
                protected Task<Void> createTask() {
                    return new Task<Void>() {
                        @Override
                        protected Void call() throws Exception {
                            System.out.println("Running");
                            return null;
                        }

                        @Override
                        protected void succeeded() {
                            super.succeeded();
                            System.out.println("Finished");
                        }
                    };
                }
            };
            service.stateProperty().addListener((observableValue, oldState, newState) -> {
                          System.out.println(oldState + "->" + newState);
            });

            service.start();
        }
    }
}

I can't call the succeeded function in the call method, since plots have to be done on the JavaFx Thread.

Also declaring the service as a class attribute and assigning the service outside of the loop doesn't work. It seems that the sleep blocks somehow the service execution.

Also surrounding the while loop in a Platform.runLater doesn't help.


Solution

  • Issues with your approach:

    1. You are consuming the JavaFX thread and not releasing it.
    2. You are looping in a while loop which does not return the JavaFX thread to the JavaFX system so it can use it.
    3. You are blocking the JavaFX thread by sleeping it, which you should never do.
    4. A Service is designed to be restartable, you should just use one, otherwise you can just use a Task instead of creating a new Service.
    5. You may have other issues with your approach.

    For a headless app, you may wish to use the Platform startup and exit methods instead of subclassing Application but that is optional and in some cases an Application could be used.

    Example JavaFX client-server app

    Here is an example of a client-server app which works similar to the goal described in your question and comments. The server is requested by the client to do some processing, render some UI, and return a snapshot as an image, which is displayed on the client.

    To use it, run the GreetingServer, then separately run the GreetingClientApp.

    Enter a name into the client app, and press greet, it will send a message to the server which will construct a greeting in a scene, snapshot the result, and encode it as a png, which it returns in a stream to the client. The client decodes the png stream to a JavaFX image and adds the image to a list view of greeting images.

    When you shut down the client app, it will send a signal to the server to shut down.

    The entries in the greeting list look like text, but they are images returned from the server.

    client

    The server console output for the sample data entered in the client screenshot is:

    JavaFX platform started
    Http server started
    Greeted Kim
    Greeted Greg
    Greeted George
    Greeted Jerry
    Http server shutdown
    JavaFX platform shutdown
    Handled bye
    

    I guess the server could have been a JavaFX application, but I decided to explicitly start and stop the JavaFX platform on the server instead.

    The example uses HTTP as the communication protocol, but you could use another protocol, or fashion your own using sockets if you prefer. (Using a different protocol is outside the scope of what I would be prepared to answer here).

    There is no JavaFX Task or Service in the server implementation for this application. There is no need for a JavaFX task on the server for this example as none of the server operations performed on the JavaFX thread are long-running.

    Tasks are created by the client to make asynchronous requests to the server.

    The example requires Java 22 for the _ syntax support.

    module-info.java

    module org.example.greeting {
        requires java.desktop;
        requires javafx.controls;
        requires javafx.swing;
    
        requires jdk.httpserver;
        requires java.net.http;
    
        exports org.example.greeting.client;
        exports org.example.greeting.server;
    }
    

    Client Code

    GreetingClientApp.java

    package org.example.greeting.client;
    
    import javafx.application.Application;
    import javafx.application.Platform;
    import javafx.geometry.Insets;
    import javafx.scene.Scene;
    import javafx.scene.control.*;
    import javafx.scene.image.Image;
    import javafx.scene.layout.HBox;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class GreetingClientApp extends Application {
        private final GreetingClient greetingClient = new GreetingClient();
        private final ExecutorService clientExecutor = Executors.newVirtualThreadPerTaskExecutor();
    
        @Override
        public void start(Stage stage) {
            ListView<Image> greetings = new ListView<>();
            greetings.setCellFactory(_ -> new ImageListCell());
            greetings.setPrefHeight(100);
    
            Button greet = new Button("Greet");
    
            TextField name = new TextField("Kim");
            name.setOnAction(_ -> greet.fire());
    
            greet.setOnAction(_ -> {
                if (!name.getText().isBlank()) {
                    greet(name, greetings);
                    name.clear();
                }
            });
    
            Button bye = new Button("Bye");
            bye.setOnAction(_ -> Platform.exit());
    
            VBox layout = new VBox(
                    10,
                    new Label("Name"),
                    new HBox(10, name, greet),
                    new Label("Greetings"),
                    greetings,
                    bye
            );
    
            layout.setPadding(new Insets(10));
    
            stage.setScene(new Scene(layout));
            stage.show();
        }
    
        private void greet(TextField name, ListView<Image> greetings) {
            GreetingTask greetingTask = new GreetingTask(
                    greetingClient,
                    name.getText()
            );
    
            greetingTask.setOnSucceeded(_ ->
                    greetings.getItems().add(
                            greetingTask.getValue()
                    )
            );
    
            clientExecutor.execute(greetingTask);
        }
    
        private void bye() {
            ByeTask byeTask = new ByeTask(
                    greetingClient
            );
    
            clientExecutor.execute(byeTask);
        }
    
        public void stop() {
            bye();
            clientExecutor.shutdown();
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    

    GreetingTask.java

    package org.example.greeting.client;
    
    import javafx.concurrent.Task;
    import javafx.scene.image.Image;
    
    public class GreetingTask extends Task<Image> {
        private final GreetingClient client;
        private final String name;
    
        public GreetingTask(GreetingClient client, String name) {
            this.client = client;
            this.name = name;
        }
    
        @Override
        protected Image call() {
            return client.greet(name);
        }
    }
    

    ByeTask.java

    package org.example.greeting.client;
    
    import javafx.concurrent.Task;
    
    public class ByeTask extends Task<Void> {
        private final GreetingClient client;
    
        public ByeTask(GreetingClient client) {
            this.client = client;
        }
    
        @Override
        protected Void call() {
            client.bye();
    
            return null;
        }
    }
    

    GreetingClient.java

    package org.example.greeting.client;
    
    import javafx.scene.image.Image;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.URI;
    import java.net.http.HttpClient;
    import java.net.http.HttpRequest;
    import java.net.http.HttpResponse;
    import java.time.Duration;
    
    public class GreetingClient {
        private static final String serverURIString = "http://localhost:8080";
    
        public Image greet(String name) {
            try (HttpClient client = HttpClient.newBuilder().build()) {
                HttpRequest request = HttpRequest.newBuilder()
                        .uri(URI.create(serverURIString + "/greet"))
                        .timeout(Duration.ofSeconds(10))
                        .header("Content-Type", "text/plain")
                        .POST(HttpRequest.BodyPublishers.ofString(name))
                        .build();
    
                HttpResponse<InputStream> response = client.send(
                        request,
                        HttpResponse.BodyHandlers.ofInputStream()
                );
    
                return new Image(response.body());
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                System.out.println("Interrupted greeting for " + name);
            }
    
            return null;
        }
    
        public void bye() {
            try (HttpClient client = HttpClient.newBuilder().build()) {
                HttpRequest request = HttpRequest.newBuilder()
                        .uri(URI.create(serverURIString + "/bye"))
                        .timeout(Duration.ofSeconds(10))
                        .header("Content-Type", "text/plain")
                        .GET()
                        .build();
    
                client.send(
                        request,
                        HttpResponse.BodyHandlers.discarding()
                );
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                System.out.println("Interrupted bye");
            }
        }    
    }
    

    ImageListCell.java

    package org.example.greeting.client;
    
    import javafx.scene.control.ListCell;
    import javafx.scene.image.Image;
    import javafx.scene.image.ImageView;
    
    public class ImageListCell extends ListCell<Image> {
        private final ImageView imageView = new ImageView();
    
        @Override
        protected void updateItem(Image image, boolean empty) {
            super.updateItem(image, empty);
    
            if (image == null || empty) {
                imageView.setImage(null);
                setGraphic(null);
                return;
            }
    
            imageView.setImage(image);
            setGraphic(imageView);
        }
    }
    

    Server Code

    GreetingServer

    package org.example.greeting.server;
    
    import com.sun.net.httpserver.HttpServer;
    import javafx.application.Platform;
    
    import java.net.InetSocketAddress;
    import java.util.concurrent.*;
    
    class GreetingServer {
        private static final int PORT = 8080;
        private HttpServer httpServer = null;
    
        public static void main(String[] args) throws Exception {
            GreetingServer server = new GreetingServer();
            server.start();
        }
    
        private void start() throws Exception {
            try {
                CompletableFuture<Void> fxStartupFuture = new CompletableFuture<>();
                Platform.startup(() -> {
                    System.out.println("JavaFX platform started");
                    fxStartupFuture.complete(null);
                });
                fxStartupFuture.get();
    
                UI ui = new UI();
    
                httpServer = HttpServer.create(new InetSocketAddress(PORT), 0);
                httpServer.createContext("/greet", new GreetHandler(ui));
                httpServer.createContext("/bye", new ShutdownHandler(this));
                httpServer.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
                httpServer.start();
    
                System.out.println("Http server started");
            } catch (Exception e) {
                System.out.println("Greeting server startup failed");
                throw e;
            }
        }
    
        public void stop() {
            httpServer.stop(5);
            System.out.println("Http server shutdown");
            Platform.exit();
            System.out.println("JavaFX platform shutdown");
        }
    }
    

    UI.java

    package org.example.greeting.server;
    
    import javafx.scene.Group;
    import javafx.scene.Scene;
    import javafx.scene.control.Label;
    import javafx.scene.image.Image;
    import javafx.scene.paint.Color;
    
    import java.util.concurrent.CompletableFuture;
    
    class UI {
        private final Label label = new Label();
    
        public void greet(String name) {
            label.setText("Hello " + name);
        }
    
        public void snapshot(CompletableFuture<Image> snapshotFuture) {
            final Scene scene = new Scene(new Group(label), Color.TRANSPARENT);
            scene.snapshot(snapshotResult -> {
                        snapshotFuture.complete(snapshotResult.getImage());
                        return null;
                    },
                    null
            );
        }
    }
    

    GreetHandler.java

    package org.example.greeting.server;
    
    import com.sun.net.httpserver.HttpExchange;
    import com.sun.net.httpserver.HttpHandler;
    import javafx.application.Platform;
    import javafx.embed.swing.SwingFXUtils;
    import javafx.scene.image.Image;
    
    import javax.imageio.ImageIO;
    import java.io.IOException;
    import java.nio.charset.StandardCharsets;
    import java.util.concurrent.CompletableFuture;
    import java.util.concurrent.ExecutionException;
    
    class GreetHandler implements HttpHandler {
        public final UI ui;
    
        public GreetHandler(UI ui) {
            this.ui = ui;
        }
    
        @Override
        public void handle(HttpExchange exchange) throws IOException {
            try (exchange) {
                String name = new String(
                        exchange.getRequestBody().readAllBytes(),
                        StandardCharsets.UTF_8
                );
    
                CompletableFuture<Image> snapshotFuture = new CompletableFuture<>();
    
                Platform.runLater(() -> {
                    ui.greet(name);
                    ui.snapshot(snapshotFuture);
                });
    
                Image snapshotImage = snapshotFuture.get();
    
                exchange.getResponseHeaders().add("Content-type", "image/png");
                exchange.sendResponseHeaders(200, 0);
    
                ImageIO.write(
                        SwingFXUtils.fromFXImage(
                                snapshotImage,
                                null
                        ),
                        "png",
                        exchange.getResponseBody()
                );
    
                System.out.println("Greeted " + name);
            } catch (ExecutionException e) {
                throw new IOException(e);
            } catch (InterruptedException e) {
                // ignore interrupts.
            }
        }
    }
    

    ShutdownHandler.java

    package org.example.greeting.server;
    
    import com.sun.net.httpserver.HttpExchange;
    import com.sun.net.httpserver.HttpHandler;
    
    import java.io.IOException;
    import java.nio.charset.StandardCharsets;
    
    class ShutdownHandler implements HttpHandler {
        private static final String BYE = "bye";
    
        public final GreetingServer server;
    
        public ShutdownHandler(GreetingServer server) {
            this.server = server;
        }
    
        @Override
        public void handle(HttpExchange exchange) throws IOException {
            try (exchange) {
                byte[] responseBytes = BYE.getBytes(StandardCharsets.UTF_8);
    
                exchange.getResponseHeaders().add("Content-type", "text/plain");
                exchange.sendResponseHeaders(200, responseBytes.length);
                exchange.getResponseBody().write(
                        responseBytes
                );
            }
    
            server.stop();
            System.out.println("Handled bye");
        }
    }
    

    pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>org.example</groupId>
        <artifactId>greeting</artifactId>
        <version>1.0-SNAPSHOT</version>
        <name>greeting</name>
    
        <dependencies>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-controls</artifactId>
                <version>22.0.1</version>
            </dependency>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-swing</artifactId>
                <version>22.0.1</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.11.0</version>
                    <configuration>
                        <source>22</source>
                        <target>22</target>
                    </configuration>
                </plugin>
            </plugins>
        </build>
        <repositories>
            <repository>
                <id>maven_central</id>
                <name>Maven Central</name>
                <url>https://repo.maven.apache.org/maven2/</url>
            </repository>
        </repositories>
    </project>
    

    FAQ

    So the better aproach would be to run the loop in a seperate Task and from there call the other Tasks?

    No. On the server you don’t need any JavaFX tasks.

    A JavaFX task is used to offload work from the JavaFX thread. With a server you want to do things the other way round, i.e. offload work from a server thread to the JavaFX thread.

    Have a set of server threads in a thread pool that accept incoming requests and process them. Unless you write your own server, you won’t write that code.

    You write the handlers that the server delegates to and runs on the threads it creates. When the handler is called, it can do some processing, calling Platform.runLater to perform short work on the JavaFX thread as needed.

    If needed, a CompleteableFuture can wait for any results that need to be returned from the JavaFX thread (for example a snapshot image).

    By using a Java 21+ virtual thread pool, you don’t need to worry about blocking the server threads or the server running out of threads.

    A JavaFX client however might choose to use a Task to make an asynchronous call to the server. The client can be notified in the task success handler when the call is complete, to allow the client to take action when the server has completed its work. The client I have provided demonstrates using tasks to make asynchronous calls to the server.

    how would a possible solution with Platform startup look like?

    Explicit JavaFX Platform startup and shutdown is demonstrated in the supplied GreetingServer example code.