Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8268294

Reusing HttpClient in a WebSocket.Listener hangs.

    XMLWordPrintable

Details

    • b03
    • Verified

    Backports

      Description

        ADDITIONAL SYSTEM INFORMATION :
        Macos/java 11,17 ( current state on github)
        Linux 11

        A DESCRIPTION OF THE PROBLEM :
        Performing an http request in a WebSocket.Listener onText method with the same httpclient used to build the websocket client itself results hangs.

        STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
        Please run the test attached. It performs the follwing steps:
        1. create an httpclient
        2. use the httpclient to create a websocket client
        3. has the httpclient send an http request when the websocket client receive data
        4. has the websocket server send data to the client

        EXPECTED VERSUS ACTUAL BEHAVIOR :
        EXPECTED -
        The httpclient successfully perform the http request.
        If an unbounded executor is provided to the httpclient, the websocket client should use it to process incoming data.
        ACTUAL -
        the httpclient cannot peform the http request as it is waiting for the thread processing the incoming websocket data to be free

        ---------- BEGIN SOURCE ----------
        /**
         * This test use two publicly available services;
         * 1. echo.websocket.org - which returns the data sent by the websocket client connected to it
         * 2. httpbin.org/get - return the data retated to the request itself ( parameter, etc )
         */

        import java.net.URI;
        import java.net.http.HttpClient;
        import java.net.http.HttpRequest;
        import java.net.http.HttpResponse;
        import java.net.http.WebSocket;
        import java.util.Optional;
        import java.util.concurrent.CompletionStage;
        import java.util.concurrent.ExecutorService;
        import java.util.concurrent.Executors;
        import java.util.concurrent.atomic.AtomicReference;
        import java.util.function.Consumer;
        import java.util.logging.Logger;

        public class TestClient {

            private static final Logger LOG = Logger.getLogger("test");

            public static void main(String[] args) throws InterruptedException {

                ExecutorService executorService = Executors.newCachedThreadPool();
                HttpClient httpClient = HttpClient.newBuilder().executor(executorService).build();

                WsApiClient wsApiClient = new WsApiClient(httpClient, "ws://echo.websocket.org");
                HttpApiClient httpApiClient = new HttpApiClient(httpClient, "http://httpbin.org/get");

                AtomicReference<String> result = new AtomicReference<>("failed");

                wsApiClient.listen(message -> httpApiClient.getData(message).map(s -> "succeeded").ifPresent(result::set));

                wsApiClient.sendData("TEST_DATA");

                LOG.info("Wait some time");
                Thread.sleep(3_000);

                executorService.shutdownNow();

                LOG.info("Result: test " + result.get());
            }

            static class WsApiClient {
                final HttpClient httpClient;
                final String server;
                WebSocket webSocket;

                WsApiClient(HttpClient httpClient, String server) {
                    this.httpClient = httpClient;
                    this.server = server;
                }

                public void listen(Consumer<String> consumer) {
                    LOG.info("WS API client - Start listening for incoming messages");
                    URI uri = URI.create(server);
                    webSocket = httpClient.newWebSocketBuilder()
                        .buildAsync(uri, new WebSocket.Listener() {
                            @Override
                            public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
                                LOG.info("WS API client - received data: " + data);
                                consumer.accept(data.toString());
                                return null;
                            }
                        }).join();
                }

                void sendData(String data) {
                    LOG.info("WS API client - sending data via WebSocket: {}" + data);
                    webSocket.sendText(data, true).join();
                }
            }

            static class HttpApiClient {
                final HttpClient httpClient;
                final String baseUrl;

                HttpApiClient(HttpClient httpClient, String baseUrl) {
                    this.httpClient = httpClient;
                    this.baseUrl = baseUrl;
                }

                private Optional<String> getData(String data) {
                    try {
                        URI uri = URI.create(baseUrl + "?param=" + data);
                        HttpRequest request = HttpRequest.newBuilder().GET().uri(uri).build();
                        LOG.info("Http API Client - send HTTP GET request with parameter {}" + data);
                        HttpResponse<String> send = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
                        Optional<String> responseData = Optional.ofNullable(send.body());
                        responseData.ifPresent(s -> LOG.info("Http API Client - response for HTTP GET request received"));
                        return responseData;
                    } catch (Exception e) {
                        LOG.warning("Http API Client - Error getting data: " + e.getMessage());
                    }
                    return Optional.empty();
                }
            }
        }

        ---------- END SOURCE ----------

        CUSTOMER SUBMITTED WORKAROUND :
        We have no control in our code over these low level components, so we introduce separate threads (executor) to handle these cases.

        Note that modifying the jdk to have the httpclient's executor passed to the WebSocketImpl and have it passed to the underlying task scheduler runOrSchedule method also works.

        https://github.com/openjdk/jdk/blob/b05c40ca3b5fd34cbbc7a9479b108a4ff2c099f1/src/java.net.http/share/classes/jdk/internal/net/http/websocket/WebSocketImpl.java#L705

        Not sure why the capability of the scheduler using an executor is left unused in the websocket implementation.

        FREQUENCY : always


        Attachments

          Issue Links

            Activity

              People

                michaelm Michael McMahon
                webbuggrp Webbug Group
                Votes:
                0 Vote for this issue
                Watchers:
                6 Start watching this issue

                Dates

                  Created:
                  Updated:
                  Resolved: