JettyHttpContainerDuringShutdownTest.java

/*
 * Copyright (c) 2026 Contributors to the Eclipse Foundation.
 * Copyright (c) 2026 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.jersey.tests.jettyresponseclose;

import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.AsyncInvoker;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.core.Response;

import java.lang.System.Logger;
import java.net.ConnectException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static java.lang.System.Logger.Level.INFO;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class JettyHttpContainerDuringShutdownTest {

    private static final Logger LOG = System.getLogger(JettyHttpContainerDuringShutdownTest.class.getName());
    private static Server server;
    private static final String URL = "http://localhost:9080/test/get-me-204";

    @BeforeAll
    public static void setup() throws Exception {
        ResourceConfig resourceConfig = new ResourceConfig(Collections.singleton(Resource204.class));
        ServletContextHandler ctx = new ServletContextHandler();
        ServletHolder servlet = new ServletHolder(new ServletContainer(resourceConfig));
        ctx.addServlet(servlet, "/test/*");
        ContextHandlerCollection handlers = new ContextHandlerCollection();
        server = new Server();

        handlers.addHandler(ctx);
        StatisticsHandler statisticsHandler = new StatisticsHandler();
        statisticsHandler.setHandler(handlers);
        server.setHandler(statisticsHandler);

        ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(new HttpConfiguration()));
        http.setPort(9080);
        server.addConnector(http);
        server.setStopTimeout(50);
        server.start();

        waitStartUp();
    }

    private static void waitStartUp() {
        for (int i = 0; i < 10; i++) {
            try (Client client = ClientBuilder.newClient();
                Response response = client.target(URL).request().get()) {
                if (response.getStatus() == 204) {
                    return;
                }
                LOG.log(INFO, "Waiting for the startup ... response status: " + response.getStatus());
            } catch (Exception e) {
                LOG.log(INFO, "Waiting for the startup ... response status: ", e);
            }
        }
    }

    @AfterAll
    public static void shutdown() throws Exception {
        if (server != null) {
            shutdownServer(10_000);
        }
    }


    /**
     * Tests that all responses are closed properly when the server is shutting down. The test sends
     * a large number of requests to the server and then initiates the shutdown process. It verifies
     * that all responses are either successfully received with a 204 status code or result in a
     * ProcessingException caused by a ConnectException, indicating that the connection was closed
     * during shutdown.
     *
     * @throws Exception
     */
    @Test
    public void testResponseClose() throws Exception {
        AtomicInteger countOf204 = new AtomicInteger();
        AtomicInteger countOfConnException = new AtomicInteger();
        try (Client client = ClientBuilder.newClient()) {
            // create requests but don't invoke them
            List<AsyncInvoker> invokers = new ArrayList<>();
            for (int i = 0; i < 1_000; i++) {
                invokers.add(client.target(URL).request().async());
            }
            // invoke them fast as possible.
            List<Future<Response>> waitingResponses = invokers.stream().map(AsyncInvoker::get).collect(Collectors.toList());
            assertThrows(TimeoutException.class, () -> shutdownServer(100));
            for (Future<Response> responseFuture : waitingResponses) {
                try (Response response = responseFuture.get(30, TimeUnit.SECONDS)) {
                    assertThat("Unexpected response code", response.getStatus(), equalTo(204));
                    countOf204.incrementAndGet();
                } catch (ExecutionException e) {
                    Throwable cause = e.getCause();
                    assertThat(cause, instanceOf(ProcessingException.class));
                    assertThat(cause.getCause(), instanceOf(ConnectException.class));
                    countOfConnException.incrementAndGet();
                }
            }
        }
        // There is no guarantee that shutdown would not make it before requests.
        // But we should be able to process at least some of HTTP requests.
        LOG.log(INFO, "Count of results:\n  HTTP 204: {0}\n  ConnectException: {1}", countOf204, countOfConnException);
        assertAll(
            () -> assertThat("Count of HTTP 204", countOf204.get(), greaterThan(0)),
            () -> assertThat("Count of ConnectException", countOfConnException.get(), greaterThanOrEqualTo(0))
        );
    }


    private static void shutdownServer(int timeoutInMillis) throws Exception {
        LOG.log(INFO, "Stopping the server: " + server);
        server.setStopTimeout(timeoutInMillis);
        server.stop();
        server = null;
    }
}