com.nike.riposte.server.http.NonblockingEndpoint Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of riposte-spi Show documentation
Show all versions of riposte-spi Show documentation
Riposte module riposte-spi
package com.nike.riposte.server.http;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Supplier;
import io.netty.channel.ChannelHandlerContext;
/**
* Represents a non-blocking resource endpoint.
*
* Since this is non-blocking it will be initially executed on a Netty worker I/O thread - DO *NOT* perform any
* blocking actions on this thread (sleeping, calling downstream services, making a DB query, expensive calculations,
* etc) or you will likely cripple your application's throughput! Instead, make sure that all blocking and/or expensive
* actions are done in the returned {@link CompletableFuture}'s async functions. See {@link #execute(RequestInfo,
* Executor, ChannelHandlerContext)} for more details.
*
* @author Nic Munroe
*/
public interface NonblockingEndpoint extends Endpoint {
/**
* This is where the rubber hits the road - this is where the logic for the endpoint goes.
*
*
HOW TO AUTOMATICALLY DESERIALIZE THE REQUEST BODY'S CONTENT AND HAVE IT (OPTIONALLY) AUTOMATICALLY
* VALIDATED
*
* If you want the incoming request's {@link RequestInfo#getContent()} to be populated then make sure you return an
* appropriate non-null value in {@link #requestContentType()} (if you don't then {@link
* RequestInfo#getRawContent()} will still be available for you to use). Note that by default {@code
* StandardEndpoint} automagically determines an appropriate return value for {@link #requestContentType()} so most
* of the time it will just work and you don't need to override that method - but if you need to you can.
*
* Additionally, if you setup the request's {@link RequestInfo#getContent()} to be populated you can have it
* automatically validated for you before this method is called by making sure {@link
* #isValidateRequestContent(RequestInfo)} returns true (it returns true by default).
*
*
HOW TO CONSTRUCT A {@link CompletableFuture} IN A WAY THAT WON'T BREAK YOUR APPLICATION
*
* Since this endpoint is non-blocking this method will initially be called on a Netty worker I/O thread, so you
* should *NEVER* perform any blocking actions on this thread (sleeping, calling downstream services, making a DB
* query, expensive calculations, etc).
*
* Ideally you would use an async driver for any downstream database or HTTP calls, and when the async driver call
* finishes it calls {@link CompletableFuture#complete(Object)} or {@link CompletableFuture#completeExceptionally(Throwable)}
* to complete the {@link CompletableFuture} you return from this method. If you are able to stick to this pattern
* and have no expensive computation that needs to be performed in the application itself then your thread count
* will never grow no matter how much concurrent traffic goes through the endpoint.
*
* Otherwise, if there is no async driver for your downstream calls or you have expensive computations you need to
* perform in the endpoint itself then all blocking/expensive actions should be done in an async portion of the
* returned {@link CompletableFuture}. Use the various factory methods to create and compose your {@link
* CompletableFuture}, but most of the time you should use the versions of the {@link CompletableFuture} methods
* that let you pass in an {@link Executor} for performing the action, and pass in the {@code
* longRunningTaskExecutor} argument. Why? By default {@link CompletableFuture} will use {@link
* java.util.concurrent.ForkJoinPool#commonPool()}, which is (1) limited to a handful of threads (not sufficient for
* high throughput HTTP request handling), and (2) is a work-stealing pool that queues up tasks (not good if tasks
* get stuck behind slow blocking ones in the queue). So for an application to support high volumes of HTTP requests
* this common forkjoin pool is not sufficient - you would run out of threads very quickly and see your throughput
* and performance drop through the floor except in the rare cases where the async tasks are short enough to not
* clog up the forkjoin pool, but long enough that you need to do an async operation rather than returning a trivial
* {@link CompletableFuture#completedFuture(Object)}.
*
*
* Therefore this {@code execute(...)} method is passed the {@code longRunningTaskExecutor} which should be used
* instead of the common forkjoin pool (in most cases), and one main drawback is that you have to remember to tell
* your {@link CompletableFuture} that you want to use it - e.g. use {@link CompletableFuture#supplyAsync(Supplier,
* Executor)}, *NOT* {@link CompletableFuture#supplyAsync(Supplier)}. If you fail to do this and your endpoints use
* the common forkjoin pool or you perform blocking actions outside of the {@link CompletableFuture} then you'll get
* miserable application performance at higher loads. The other main drawback is that your application's thread
* count will grow based on the number of concurrent executions of the endpoint. This may be unavoidable, especially
* if you have expensive computations to do in the endpoint (the work has to be done *somewhere* and you can't do it
* on the Netty I/O thread), but it's something to keep in mind. If you can break the work up into small chunks and
* have the ForkJoin pool do the work over time then that may be a better solution. Or you may be able to come up
* with some other creative solution; there's no limitation on your creativity to keep the thread count low, the
* only limitation is that the object returned by this method is a {@link CompletableFuture} that is eventually
* completed *somehow*.
*
*
HOW TO HAVE YOUR ASYNC {@link CompletableFuture} SUPPORT THE DISTRIBUTED TRACING AND LOGGING MDC
* ASSOCIATED WITH THE REQUEST
*
* Once the {@link CompletableFuture}'s async tasks start they will be on one or more different threads that are
* missing the distributed tracing and logging MDC values associated with the request. In order to have the tracing
* and MDC info "hop" threads we've provided some helpers in {@code com.nike.riposte.util.AsyncNettyHelper} that you
* can use in conjunction with the {@link CompletableFuture}'s methods to provide the {@link CompletableFuture} with
* {@link java.lang.Runnable}s, {@link Supplier}s, {@link java.util.function.Function}s, and {@link
* java.util.function.Consumer}s that know how to make the tracing and MDC info hop threads correctly. For example,
* if you wanted to use the {@link CompletableFuture#supplyAsync(Supplier, Executor)} method, then create your
* supplier using {@code com.nike.riposte.util.AsyncNettyHelper#supplierWithTracingAndMdc(java.util.function.Supplier,
* ChannelHandlerContext)} (passing in the raw Supplier that performs the async work along with the {@link
* ChannelHandlerContext} ctx argument given to you by this {@code execute(...)} method). That Supplier-wrapper
* returned by {@code com.nike.riposte.util.AsyncNettyHelper} will register the correct tracing and MDC info with
* the thread before performing the async action of the original Supplier, and will unregister them when the async
* action is done.
*
* The {@link ChannelHandlerContext} ctx argument is usually only necessary for use with {@code
* com.nike.riposte.util.AsyncNettyHelper} to create tracing and MDC supported async processing objects, but it's
* also here in case you need to access the {@code
* com.nike.riposte.server.channelpipeline.ChannelAttributes#getHttpProcessingStateForChannel(ChannelHandlerContext)}
* state for the channel, schedule a job to be performed at some arbitrary time in the future, or do some other
* pipeline manipulation. Just make sure you know *EXACTLY* what you're doing when you use it, otherwise you could
* break your application in subtle ways without even realizing it.
*/
CompletableFuture> execute(RequestInfo request, Executor longRunningTaskExecutor,
ChannelHandlerContext ctx);
/**
* @param request
* The request that was passed into the execute method.
* @param ctx
* The channel handler context that was passed into the execute method.
*
* @return The error to throw when the {@link java.util.concurrent.CompletableFuture} times out, or null if you want
* to use the default {@link com.nike.riposte.server.error.exception.NonblockingEndpointCompletableFutureTimedOut}.
* Most of the time you can just return null, but if you want to throw (for example) a custom error that contains
* special info based on the request you can do so.
*/
@SuppressWarnings("UnusedParameters")
default Throwable getCustomTimeoutExceptionCause(RequestInfo request, ChannelHandlerContext ctx) {
return null;
}
}