Latency can be a major cause of performance issues in distributed systems. Even a simple system with a single web server and a single database can fall victim to the N+1 problem (SQL or API Based), processing one record at a time vs batching, or serial vs parallel execution. To help highlight some of these issues in future posts we need way to add artificial latency to http requests.

Scenario

  • Create custom Undertow HttpHandler's that block for ~1 second
  • Web server will only have a single IO worker and five worker threads
  • Make five requests in parallel

Delay Handler and Sleep Handler

We will start out with a naive sleep implementation using Guava's Uninterruptibles.sleepUninterruptibly and then test out a non-blocking option utilizing XnioExecutor.executeAfter method exposed to us by Undertow. Both approaches will then be tested on just the IO thread followed by dispatching to worker threads. Our only intention is to show the differences between all the approaches, not to compare performance or suggest one method over the other. We will dive into the DelayedExecutionHandler implementation later on.

private static HttpHandler getRouter() {

    // Handler using Thread.sleep for a blocking delay
    HttpHandler sleepHandler = (exchange) -> {
        log.debug("In sleep handler");
        Uninterruptibles.sleepUninterruptibly(1L, TimeUnit.SECONDS);
        Exchange.body().sendText(exchange, "ok");
    };

    // Custom handler using XnioExecutor.executeAfter
    // internals for a non blocking delay
    DelayedExecutionHandler delayedHandler = DiagnosticHandlers.fixedDelay(
        (exchange) -> {
            log.debug("In delayed handler");
            Exchange.body().sendText(exchange, "ok");
        },
        1L, TimeUnit.SECONDS);

    HttpHandler routes = new RoutingHandler()
        .get("/sleep", sleepHandler)
        .get("/dispatch/sleep", new BlockingHandler(sleepHandler))
        .get("/delay", delayedHandler)
        .get("/dispatch/delay", new BlockingHandler(delayedHandler))
        .setFallbackHandler(RoutingHandlers::notFoundHandler);

    return CustomHandlers.accessLog(routes, LoggerFactory.getLogger("Access Log"));
}

Parallel Request Helper

Additionally we have a helper function for executing N GET requests in parallel using the OkHttpClient. This will spin up a new fixed size thread pool, execute the requests, and finally shutdown the executor using Guava's MoreExecutors.shutdownAndAwaitTermination.

public static void getInParallel(OkHttpClient client, String url, int count) {
    ExecutorService exec = Executors.newFixedThreadPool(count);
    for (int i = 0; i < count; i++) {
        exec.submit(() -> Http.get(client, url));
    }
    MoreExecutors.shutdownAndAwaitTermination(exec, 30, TimeUnit.SECONDS);
}

Main Method

Here we will be spinning up a simple embedded Undertow web server configured with a single IO worker and five worker threads. Then we make five parallel requests to each endpoint to see the results.

public static void main(String[] args) {
    SimpleServer server = SimpleServer.simpleServer(getRouter());
    server.getUndertow()
          .setIoThreads(1)
          .setWorkerThreads(5);
    Undertow undertow = server.start();

    OkHttpClient client = HttpClient.globalClient();

    Timers.time("---------- sleep ----------", () ->
        Http.getInParallel(client, "http://localhost:8080/sleep", 5));

    Timers.time("---------- dispatch sleep ----------", () ->
        Http.getInParallel(client, "http://localhost:8080/dispatch/sleep", 5));

    Timers.time("---------- delay ----------", () ->
        Http.getInParallel(client, "http://localhost:8080/delay", 5));

    Timers.time("---------- dispatch delay ----------", () ->
        Http.getInParallel(client, "http://localhost:8080/dispatch/delay", 5));
    undertow.stop();
}

Sleep Handler IO Thread Results

You should never be calling blocking operations on the IO threads but we did this purposefully here just as an example. It took a total of ~5 seconds to make our five concurrent requests. The first request took ~1 second and each additional request took a second longer than the one before it. This is expected as we were blocking the only IO thread (XNIO-1 I/O-1) so each request needed to process serially.

2019-03-13 21:56:33.951 [main] INFO  com.stubbornjava.common.Timers - ---------- sleep ----------
2019-03-13 21:56:33.954 [pool-5-thread-2] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/sleep http/1.1
2019-03-13 21:56:33.954 [pool-5-thread-1] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/sleep http/1.1
2019-03-13 21:56:33.957 [pool-5-thread-3] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/sleep http/1.1
2019-03-13 21:56:33.959 [XNIO-1 I/O-1] DEBUG c.s.e.u.h.DelayedHandlerExample - In sleep handler
2019-03-13 21:56:33.959 [pool-5-thread-5] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/sleep http/1.1
2019-03-13 21:56:33.959 [pool-5-thread-4] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/sleep http/1.1
2019-03-13 21:56:34.962 [XNIO-1 I/O-1] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /sleep HTTP/1.1" 200 1003ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:34.962 [pool-5-thread-1] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/sleep (1007ms, 2-byte body)
2019-03-13 21:56:34.963 [XNIO-1 I/O-1] DEBUG c.s.e.u.h.DelayedHandlerExample - In sleep handler
2019-03-13 21:56:35.968 [XNIO-1 I/O-1] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /sleep HTTP/1.1" 200 1004ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:35.968 [pool-5-thread-2] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/sleep (2013ms, 2-byte body)
2019-03-13 21:56:35.969 [XNIO-1 I/O-1] DEBUG c.s.e.u.h.DelayedHandlerExample - In sleep handler
2019-03-13 21:56:36.971 [XNIO-1 I/O-1] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /sleep HTTP/1.1" 200 1002ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:36.971 [pool-5-thread-3] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/sleep (3014ms, 2-byte body)
2019-03-13 21:56:36.972 [XNIO-1 I/O-1] DEBUG c.s.e.u.h.DelayedHandlerExample - In sleep handler
2019-03-13 21:56:37.975 [XNIO-1 I/O-1] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /sleep HTTP/1.1" 200 1002ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:37.975 [pool-5-thread-4] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/sleep (4015ms, 2-byte body)
2019-03-13 21:56:37.976 [XNIO-1 I/O-1] DEBUG c.s.e.u.h.DelayedHandlerExample - In sleep handler
2019-03-13 21:56:38.978 [pool-5-thread-5] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/sleep (5018ms, 2-byte body)
2019-03-13 21:56:38.978 [XNIO-1 I/O-1] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /sleep HTTP/1.1" 200 1001ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:38.979 [main] INFO  com.stubbornjava.common.Timers - ---------- sleep ---------- took 5028ms

Sleep Handler Dispatching to Worker Threads Results

Since you should never be blocking the IO threads this example will dispatch the sleep operation to our pool of five worker threads. Since we now have five worker threads (XNIO-1 task-{n}) that are able to handle blocking operations our total response time is ~1 second with each request taking about a second. If we had more requests than workers the requests would queue up.

2019-03-13 21:56:38.980 [main] INFO  com.stubbornjava.common.Timers - ---------- dispatch sleep ----------
2019-03-13 21:56:38.982 [pool-6-thread-2] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/dispatch/sleep http/1.1
2019-03-13 21:56:38.982 [pool-6-thread-4] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/dispatch/sleep http/1.1
2019-03-13 21:56:38.982 [pool-6-thread-1] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/dispatch/sleep http/1.1
2019-03-13 21:56:38.982 [pool-6-thread-3] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/dispatch/sleep http/1.1
2019-03-13 21:56:38.983 [XNIO-1 task-2] DEBUG c.s.e.u.h.DelayedHandlerExample - In sleep handler
2019-03-13 21:56:38.983 [XNIO-1 task-5] DEBUG c.s.e.u.h.DelayedHandlerExample - In sleep handler
2019-03-13 21:56:38.983 [XNIO-1 task-1] DEBUG c.s.e.u.h.DelayedHandlerExample - In sleep handler
2019-03-13 21:56:38.983 [pool-6-thread-5] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/dispatch/sleep http/1.1
2019-03-13 21:56:38.984 [XNIO-1 task-4] DEBUG c.s.e.u.h.DelayedHandlerExample - In sleep handler
2019-03-13 21:56:38.984 [XNIO-1 task-3] DEBUG c.s.e.u.h.DelayedHandlerExample - In sleep handler
2019-03-13 21:56:39.987 [XNIO-1 task-2] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /dispatch/sleep HTTP/1.1" 200 1004ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:39.987 [pool-6-thread-3] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/dispatch/sleep (1005ms, 2-byte body)
2019-03-13 21:56:39.987 [XNIO-1 task-5] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /dispatch/sleep HTTP/1.1" 200 1004ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:39.988 [pool-6-thread-2] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/dispatch/sleep (1005ms, 2-byte body)
2019-03-13 21:56:39.987 [pool-6-thread-1] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/dispatch/sleep (1005ms, 2-byte body)
2019-03-13 21:56:39.988 [XNIO-1 task-1] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /dispatch/sleep HTTP/1.1" 200 1004ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:39.989 [XNIO-1 task-3] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /dispatch/sleep HTTP/1.1" 200 1004ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:39.989 [XNIO-1 task-4] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /dispatch/sleep HTTP/1.1" 200 1005ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:39.990 [pool-6-thread-5] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/dispatch/sleep (1005ms, 2-byte body)
2019-03-13 21:56:39.990 [pool-6-thread-4] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/dispatch/sleep (1007ms, 2-byte body)
2019-03-13 21:56:39.990 [main] INFO  com.stubbornjava.common.Timers - ---------- dispatch sleep ---------- took 1010ms

Delay Handler Implementation

Here we have a non blocking HttpHandler that utilizes XnioExecutor.executeAfter to achieve our delay. We need to undispatch the HttpServerExchange if it has already been dispatched so we can spin on the IO thread.

/**
 * A non blocking handler to add a time delay before the next handler
 * is executed. If the exchange has already been dispatched this will
 * un-dispatch the exchange and re-dispatch it before next is called.
 */
public class DelayedExecutionHandler implements HttpHandler {

    private final HttpHandler next;
    private final Function<HttpServerExchange, Duration> durationFunc;

    DelayedExecutionHandler(HttpHandler next,
                            Function<HttpServerExchange, Duration> durationFunc) {
        this.next = next;
        this.durationFunc = durationFunc;
    }

    @Override
    public void handleRequest(HttpServerExchange exchange) throws Exception {
        Duration duration = durationFunc.apply(exchange);

        final HttpHandler delegate;
        if (exchange.isBlocking()) {
            // We want to undispatch here so that we are not blocking
            // a worker thread. We will spin on the IO thread using the
            // built in executeAfter.
            exchange.unDispatch();
            delegate = new BlockingHandler(next);
        } else {
            delegate = next;
        }

        exchange.dispatch(exchange.getIoThread(), () -> {
            exchange.getIoThread().executeAfter(() ->
                Connectors.executeRootHandler(delegate, exchange),
                duration.toMillis(),
                TimeUnit.MILLISECONDS);
        });
    }
}
/**
 * Add a fixed delay before execution of the next handler
 * @param next
 * @param duration
 * @param unit
 * @return
 */
public static DelayedExecutionHandler fixedDelay(HttpHandler next,
                                                 long duration,
                                                 TimeUnit unit) {
    return new DelayedExecutionHandler(
        next, (exchange) -> Duration.ofMillis(unit.toMillis(duration)));
}

/**
 * Add a random delay between minDuration (inclusive) and
 * maxDuration (exclusive) before execution of the next handler.
 * This can be used to add artificial latency for requests.
 *
 * @param next
 * @param minDuration inclusive
 * @param maxDuration exclusive
 * @param unit
 * @return
 */
public static DelayedExecutionHandler randomDelay(HttpHandler next,
                                                 long minDuration,
                                                 long maxDuration,
                                                 TimeUnit unit) {
return new DelayedExecutionHandler(
    next, (exchange) ->  {
        long duration = ThreadLocalRandom.current()
                                         .nextLong(minDuration, maxDuration);
        return Duration.ofMillis(unit.toMillis(duration));
    });
}

Delay Handler IO Thread Results

Utilizing XnioExecutor.executeAfter we are able to achieve our same one second delay across all five requests with only the IO thread (XNIO-1 I/O-1). This is an option that could allow us to delay many parallel requests with fewer threads than the blocking approach.

2019-03-13 21:56:39.991 [main] INFO  com.stubbornjava.common.Timers - ---------- delay ----------
2019-03-13 21:56:39.992 [pool-7-thread-1] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/delay http/1.1
2019-03-13 21:56:39.992 [pool-7-thread-3] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/delay http/1.1
2019-03-13 21:56:39.992 [pool-7-thread-2] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/delay http/1.1
2019-03-13 21:56:39.993 [pool-7-thread-4] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/delay http/1.1
2019-03-13 21:56:39.993 [pool-7-thread-5] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/delay http/1.1
2019-03-13 21:56:40.998 [XNIO-1 I/O-1] DEBUG c.s.e.u.h.DelayedHandlerExample - In delayed handler
2019-03-13 21:56:40.999 [XNIO-1 I/O-1] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /delay HTTP/1.1" 200 1005ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:40.999 [pool-7-thread-1] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/delay (1006ms, 2-byte body)
2019-03-13 21:56:40.999 [XNIO-1 I/O-1] DEBUG c.s.e.u.h.DelayedHandlerExample - In delayed handler
2019-03-13 21:56:41.000 [XNIO-1 I/O-1] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /delay HTTP/1.1" 200 1006ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:41.000 [pool-7-thread-2] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/delay (1007ms, 2-byte body)
2019-03-13 21:56:41.000 [XNIO-1 I/O-1] DEBUG c.s.e.u.h.DelayedHandlerExample - In delayed handler
2019-03-13 21:56:41.001 [XNIO-1 I/O-1] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /delay HTTP/1.1" 200 1007ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:41.001 [pool-7-thread-3] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/delay (1008ms, 2-byte body)
2019-03-13 21:56:41.001 [XNIO-1 I/O-1] DEBUG c.s.e.u.h.DelayedHandlerExample - In delayed handler
2019-03-13 21:56:41.002 [XNIO-1 I/O-1] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /delay HTTP/1.1" 200 1008ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:41.002 [pool-7-thread-4] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/delay (1009ms, 2-byte body)
2019-03-13 21:56:41.002 [XNIO-1 I/O-1] DEBUG c.s.e.u.h.DelayedHandlerExample - In delayed handler
2019-03-13 21:56:41.003 [XNIO-1 I/O-1] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /delay HTTP/1.1" 200 1008ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:41.003 [pool-7-thread-5] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/delay (1009ms, 2-byte body)
2019-03-13 21:56:41.003 [main] INFO  com.stubbornjava.common.Timers - ---------- delay ---------- took 1012ms

Delay Handler Dispatching to Worker Threads Results

The delayed handler operating on the dispatched threads has similar results as the sleeping approach dispatching to worker threads. This approach probably has slightly higher overhead since we are bouncing from the IO thread to the worker thread, then back to the IO thread, and finally completing in the worker again.

2019-03-13 21:56:41.003 [main] INFO  com.stubbornjava.common.Timers - ---------- dispatch delay ----------
2019-03-13 21:56:41.004 [pool-8-thread-1] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/dispatch/delay http/1.1
2019-03-13 21:56:41.005 [pool-8-thread-2] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/dispatch/delay http/1.1
2019-03-13 21:56:41.005 [pool-8-thread-3] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/dispatch/delay http/1.1
2019-03-13 21:56:41.005 [pool-8-thread-4] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/dispatch/delay http/1.1
2019-03-13 21:56:41.005 [pool-8-thread-5] DEBUG com.stubbornjava.common.HttpClient - --> GET http://localhost:8080/dispatch/delay http/1.1
2019-03-13 21:56:42.007 [XNIO-1 task-2] DEBUG c.s.e.u.h.DelayedHandlerExample - In delayed handler
2019-03-13 21:56:42.007 [XNIO-1 task-1] DEBUG c.s.e.u.h.DelayedHandlerExample - In delayed handler
2019-03-13 21:56:42.008 [XNIO-1 task-3] DEBUG c.s.e.u.h.DelayedHandlerExample - In delayed handler
2019-03-13 21:56:42.008 [XNIO-1 task-1] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /dispatch/delay HTTP/1.1" 200 1001ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:42.007 [XNIO-1 task-5] DEBUG c.s.e.u.h.DelayedHandlerExample - In delayed handler
2019-03-13 21:56:42.008 [pool-8-thread-4] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/dispatch/delay (1002ms, 2-byte body)
2019-03-13 21:56:42.008 [XNIO-1 task-3] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /dispatch/delay HTTP/1.1" 200 1001ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:42.008 [XNIO-1 task-5] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /dispatch/delay HTTP/1.1" 200 1002ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:42.008 [XNIO-1 task-4] DEBUG c.s.e.u.h.DelayedHandlerExample - In delayed handler
2019-03-13 21:56:42.008 [XNIO-1 task-2] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /dispatch/delay HTTP/1.1" 200 1002ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:42.008 [pool-8-thread-2] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/dispatch/delay (1003ms, 2-byte body)
2019-03-13 21:56:42.008 [pool-8-thread-1] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/dispatch/delay (1003ms, 2-byte body)
2019-03-13 21:56:42.008 [pool-8-thread-3] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/dispatch/delay (1002ms, 2-byte body)
2019-03-13 21:56:42.009 [pool-8-thread-5] DEBUG com.stubbornjava.common.HttpClient - <-- 200 OK http://localhost:8080/dispatch/delay (1003ms, 2-byte body)
2019-03-13 21:56:42.009 [XNIO-1 task-4] INFO  Access Log - HTTP/1.1 127.0.0.1 - "GET /dispatch/delay HTTP/1.1" 200 1002ms 2 bytes "-" "okhttp/3.11.0"
2019-03-13 21:56:42.010 [main] INFO  com.stubbornjava.common.Timers - ---------- dispatch delay ---------- took 1006ms