StopServerWithExternalWorkerUtils.java

/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2021 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package io.undertow.testutils;

import java.net.BindException;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import io.undertow.Undertow;
import org.xnio.XnioWorker;

import static org.junit.Assert.fail;

/**
 * <p>
 * When an Undertow Server has an internal worker (i.e. a {@link org.xnio.XnioWorker}
 * that is created upon start if no worker is {@link io.undertow.Undertow.Builder#setWorker(XnioWorker)
 * provided} to the builder), the worker is automatically closed with the server when it
 * {@link Undertow#stop() stops}. This operation blocks until the worker is finished, and this is
 * usually enough to guarantee all ports are freed when the operation returns, making it possible
 * to start another server associated to the same address.
 * </p>
 *<p>
 * If that is not the case, we say the worker is external to the server, provided to the Builder via
 * {@link io.undertow.Undertow.Builder#setWorker(XnioWorker)}. In this case, there is no way to
 * wait on the sockets closing when stopping the server, and in a test environment this means that
 * a series of opening/closing servers could lead to a {@link BindException} if a starting server tries
 * to bind to the ports used by a closing server before its address is released.
 * </p>
 * <p>To prevent that error, we need to perform some extra actions, such as {@link
 * StopServerWithExternalWorkerUtils#stopServerAndWorker(Undertow) closing } both the server
 * and the worker whenever possible, or  {@link
 * StopServerWithExternalWorkerUtils#waitWorkerRunnableCycle(XnioWorker) waiting} for a full cycle
 * of tasks to be executed in the {@link XnioWorker} plus a short period of sleep to avoid the time
 * window in which the closing server is still bound to the socket address.
 * </p>
 *
 * @see io.undertow.Undertow.Builder#setWorker(XnioWorker)
 * @see #stopServerAndWorker(Undertow)
 * @see #stopServer(Undertow)
 *
 * @author Flavia Rainone
 */
public class StopServerWithExternalWorkerUtils {

    private StopServerWithExternalWorkerUtils() {}

    /**
     * Stops the server and the external worker associated with it. Blocks until
     * the worker has fully stopped.
     *
     * @param server the Undertow server
     */
    public static void stopServerAndWorker(Undertow server) {
        final XnioWorker worker = server.getWorker();
        server.stop();
        stopWorker(worker);
    }

    /**
     * Stops the worker and waits until it is shutdown. This operation is not
     * asynchronous and will block until the worker has fully stopped.
     *
     * @param worker the XnioWorker
     */
    public static void stopWorker(XnioWorker worker) {
        worker.shutdown();
        try {
            if (!worker.awaitTermination(10, TimeUnit.SECONDS)) {
                List<Runnable> tasks = worker.shutdownNow();
                for (Runnable task: tasks)
                    task.run();
                if (!worker.awaitTermination(10, TimeUnit.SECONDS))
                    throw new IllegalStateException("Worker failed to shutdown within ten seconds");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            fail(e.getMessage());
        }
    }

    /**
     * Stops only the server, keeping its external worker up and unchanged. After the server
     * is stopped, waits for a full worker runnable cycle to complete, plus a sleep time, to
     * prevent the time window in which the closing server is still bound to the associated
     * address.
     * This operation blocks until the worker finishes executing an empty Runnable task.
     *
     * @param server the Undertow server
     */
    public static void stopServer(Undertow server) {
        final XnioWorker worker = server.getWorker();
        server.stop();
        waitWorkerRunnableCycle(worker);
    }

    /**
     * Waits for a full worker runnable cycle to complete, plus a sleep time, to prevent the
     * time window in which any closing server could be still bound to its associated address.
     * This operation blocks until the worker finishes executing an empty Runnable task.
     *
     * @param worker the XnioWorker
     */
    public static void waitWorkerRunnableCycle(XnioWorker worker) {
        CountDownLatch serverShutdownLatch = new CountDownLatch(1);
        worker.getIoThread().execute(serverShutdownLatch::countDown);
        //some environments seem to need a small delay to re-bind the socket
        try {
            serverShutdownLatch.await();
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            //ignore
        }
    }
}