All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.nike.riposte.server.handler.ChannelPipelineFinalizerHandler Maven / Gradle / Ivy

There is a newer version: 0.20.0
Show newest version
package com.nike.riposte.server.handler;

import com.nike.riposte.metrics.MetricsListener;
import com.nike.riposte.server.channelpipeline.ChannelAttributes;
import com.nike.riposte.server.channelpipeline.message.LastOutboundMessage;
import com.nike.riposte.server.error.handler.ErrorResponseBody;
import com.nike.riposte.server.handler.base.BaseInboundHandlerWithTracingAndMdcSupport;
import com.nike.riposte.server.handler.base.PipelineContinuationBehavior;
import com.nike.riposte.server.http.HttpProcessingState;
import com.nike.riposte.server.http.ProxyRouterProcessingState;
import com.nike.riposte.server.http.RequestInfo;
import com.nike.riposte.server.http.ResponseInfo;
import com.nike.riposte.server.http.ResponseSender;
import com.nike.riposte.server.http.impl.RequestInfoImpl;
import com.nike.riposte.server.logging.AccessLogger;
import com.nike.riposte.server.metrics.ServerMetricsEvent;
import com.nike.wingtips.Span;
import com.nike.wingtips.Tracer;

import com.fasterxml.jackson.core.JsonProcessingException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandler;
import io.netty.channel.ChannelOutboundHandler;
import io.netty.channel.ChannelPromise;

import static com.nike.riposte.server.channelpipeline.HttpChannelInitializer.IDLE_CHANNEL_TIMEOUT_HANDLER_NAME;
import static com.nike.riposte.util.AsyncNettyHelper.runnableWithTracingAndMdc;

/**
 * Finalizes incoming messages so that the pipeline considers the message handled and won't throw an error. This first
 * checks to see if a response was sent to the user - if not then a generic error will be thrown. After that it cleans
 * out the {@link HttpProcessingState} for this channel in preparation for a new incoming request, and sets up a new
 * {@link IdleChannelTimeoutHandler} that will kill the channel if it sits idle for longer than the timeout value before
 * the next request comes in.
 * 

* This handler should be the last handler in the pipeline. * * @author Nic Munroe */ @SuppressWarnings("WeakerAccess") public class ChannelPipelineFinalizerHandler extends BaseInboundHandlerWithTracingAndMdcSupport { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final ExceptionHandlingHandler exceptionHandlingHandler; private final ResponseSender responseSender; private final MetricsListener metricsListener; private final AccessLogger accessLogger; private final long workerChannelIdleTimeoutMillis; private static final Throwable ARTIFICIAL_SERVER_WORKER_CHANNEL_CLOSED_EXCEPTION = new RuntimeException("Server worker channel closed"); /** * @param exceptionHandlingHandler * The {@link ExceptionHandlingHandler} that is used by the pipeline where this class is registered for handling * errors/exceptions. * @param responseSender * The {@link ResponseSender} that is used by the pipeline where this class is registered for sending data to * the user. * @param accessLogger The {@link AccessLogger} that is used by the pipeline where this class is registered for * access logging (i.e. the same access logger set on {@link AccessLogEndHandler}). * @param workerChannelIdleTimeoutMillis * The time in millis that should be given to {@link IdleChannelTimeoutHandler}s when they are created for * detecting idle channels that need to be closed. */ public ChannelPipelineFinalizerHandler(ExceptionHandlingHandler exceptionHandlingHandler, ResponseSender responseSender, MetricsListener metricsListener, AccessLogger accessLogger, long workerChannelIdleTimeoutMillis) { if (exceptionHandlingHandler == null) throw new IllegalArgumentException("exceptionHandlingHandler cannot be null"); if (responseSender == null) throw new IllegalArgumentException("responseSender cannot be null"); this.exceptionHandlingHandler = exceptionHandlingHandler; this.responseSender = responseSender; this.metricsListener = metricsListener; this.accessLogger = accessLogger; this.workerChannelIdleTimeoutMillis = workerChannelIdleTimeoutMillis; } @Override public PipelineContinuationBehavior doChannelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof LastOutboundMessage) { Exception ex = new Exception("Manually created exception to be used for diagnostic stack trace"); HttpProcessingState state = getStateAndCreateIfNeeded(ctx, ex); finalizeChannelPipeline(ctx, msg, state, ex); } return PipelineContinuationBehavior.DO_NOT_FIRE_CONTINUE_EVENT; } @Override public PipelineContinuationBehavior doExceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { HttpProcessingState state = getStateAndCreateIfNeeded(ctx, cause); finalizeChannelPipeline(ctx, null, state, cause); return PipelineContinuationBehavior.DO_NOT_FIRE_CONTINUE_EVENT; } @Override protected boolean argsAreEligibleForLinkingAndUnlinkingDistributedTracingInfo( HandlerMethodToExecute methodToExecute, ChannelHandlerContext ctx, Object msgOrEvt, Throwable cause ) { // At this point in the pipeline DTraceEndHandler has already run, // so there is no tracing information to link/unlink. return false; } /** * Returns the given context's channel's state. We expect the state to exist, so if it doesn't this method will log * an error (using the passed-in cause as the stack trace) and setup a new blank state. */ protected HttpProcessingState getStateAndCreateIfNeeded(ChannelHandlerContext ctx, Throwable cause) { HttpProcessingState state = ChannelAttributes.getHttpProcessingStateForChannel(ctx).get(); if (state == null) { // The error must have occurred before RequestStateCleanerHandler could even execute. Create a new state and // put it into the channel so that we can process without worrying about null states. logger.error( "Found an empty state for this request in the finalizer - this should not be possible and indicates a " + "major problem in the channel pipeline.", new Exception("Wrapper exception", cause) ); state = new HttpProcessingState(); ctx.channel().attr(ChannelAttributes.HTTP_PROCESSING_STATE_ATTRIBUTE_KEY).set(state); } return state; } /** * This will first check the given state to see if a response was sent to the user. If not then this method will * send a generic error to the user so they get some response (so this method is kind of a backstop in case requests * somehow slip through our pipeline without being handled, which should never happen, but we have to have this just * in case). Then it will clean out the state so that it is ready for the next request. *

* If the state indicates that a response was already sent then this method will only clean out the state for the * next request and will not send an error. */ protected void finalizeChannelPipeline(ChannelHandlerContext ctx, Object msg, HttpProcessingState state, Throwable cause) throws JsonProcessingException { RequestInfo requestInfo = exceptionHandlingHandler.getRequestInfo(state, msg); // Send a generic error response to the client if no response has already been sent. NOTE: In the case of // multiple chunked messages, this block will only be executed once because when the generic error response // is sent it will update the state.isResponseSent() so that further calls will return true. if (!state.isResponseSendingStarted()) { String errorMsg = "Discovered a request that snuck through without a response being sent. This should not " + "be possible and indicates a major problem in the channel pipeline."; logger.error(errorMsg, new Exception("Wrapper exception", cause)); // Send a generic unhandled error response with a wrapper exception so that the logging info output by the // exceptionHandlingHandler will have the overview of what went wrong. Exception exceptionToUse = new Exception(errorMsg, cause); ResponseInfo responseInfo = exceptionHandlingHandler.processUnhandledError(state, msg, exceptionToUse); responseSender.sendErrorResponse(ctx, requestInfo, responseInfo); } ctx.flush(); handleMetricsForCompletedRequestIfNotAlreadyDone(state); // Make sure to clear out request info chunks, multipart data, and any other resources to prevent reference // counting memory leaks (or any other kind of memory leaks). requestInfo.releaseAllResources(); // Add an IdleChannelTimeoutHandler (if desired) to the start of the pipeline in order to auto-close this // channel if it sits unused longer than the timeout value before the next request arrives. if (workerChannelIdleTimeoutMillis > 0 && ctx.pipeline().get(IDLE_CHANNEL_TIMEOUT_HANDLER_NAME) == null) { ctx.pipeline().addFirst(IDLE_CHANNEL_TIMEOUT_HANDLER_NAME, new IdleChannelTimeoutHandler(workerChannelIdleTimeoutMillis, "ServerWorkerChannel")); } // If we're in an error case (cause != null) and the response sending has started but not completed, then this // request is broken. We can't do anything except kill the channel. if ((cause != null) && state.isResponseSendingStarted() && !state.isResponseSendingLastChunkSent()) { runnableWithTracingAndMdc( () -> logger.error( "Received an error in ChannelPipelineFinalizerHandler after response sending was started, but " + "before it finished. Closing the channel. unexpected_error={}", cause.toString() ), ctx ).run(); ctx.channel().close(); } } protected void handleMetricsForCompletedRequestIfNotAlreadyDone(HttpProcessingState state) { // Send response-sent event for metrics purposes now that we handled all possible cases. // Due to multiple messages and exception possibilities/interactions it's possible we've already dealt with // the metrics for this request, so make sure we only do it if appropriate. if (metricsListener != null && !state.isRequestMetricsRecordedOrScheduled()) { state.setRequestMetricsRecordedOrScheduled(true); // If there was no response sent then do the metrics event now (should only happen under rare error // conditions), otherwise do it when the response finishes. if (!state.isResponseSendingLastChunkSent()) { // TODO: Somehow mark the state as a failed request and update the metrics listener to handle it metricsListener.onEvent(ServerMetricsEvent.RESPONSE_WRITE_FAILED, state); } else { // We need to use a copy of the state in case the original state gets cleaned. HttpProcessingState stateCopy = new HttpProcessingState(state); stateCopy.getResponseWriterFinalChunkChannelFuture() .addListener((ChannelFutureListener) channelFuture -> { if (channelFuture.isSuccess()) metricsListener.onEvent(ServerMetricsEvent.RESPONSE_SENT, stateCopy); else { // TODO: Somehow mark the state as a failed request and update the metrics listener to handle it metricsListener.onEvent(ServerMetricsEvent.RESPONSE_WRITE_FAILED, null); } }); } } } /** * This method is used as the final cleanup safety net for when a channel is closed. It guarantees that any * {@link ByteBuf}s being held by {@link RequestInfo} or {@link ProxyRouterProcessingState} are {@link * ByteBuf#release()}d so that we don't end up with a memory leak. * *

Note that we can't use {@link ChannelOutboundHandler#close(ChannelHandlerContext, ChannelPromise)} for this * purpose as it is only called if we close the connection in our application code. It won't be triggered if * (for example) the caller closes the connection, and we need it to *always* run for *every* closed connection, * no matter the source of the close. {@link ChannelInboundHandler#channelInactive(ChannelHandlerContext)} is always * called, so we're using that. */ @Override public PipelineContinuationBehavior doChannelInactive(ChannelHandlerContext ctx) throws Exception { try { // Grab hold of the things we may need when cleaning up. HttpProcessingState httpState = ChannelAttributes.getHttpProcessingStateForChannel(ctx).get(); ProxyRouterProcessingState proxyRouterState = ChannelAttributes.getProxyRouterProcessingStateForChannel(ctx).get(); if (httpState == null) { if (proxyRouterState == null) { logger.debug("This channel closed before it processed any requests. Nothing to cleanup. " + "current_span={}", Tracer.getInstance().getCurrentSpan()); } else { // This should never happen, but if it does we'll try to release what we can and then return. logger.error("Found a channel where HttpProcessingState was null, but ProxyRouterProcessingState " + "was not null. This should not be possible! " + "current_span={}", Tracer.getInstance().getCurrentSpan()); releaseProxyRouterStateResources(proxyRouterState, ctx); } // With httpState null, there's nothing left for us to do. return PipelineContinuationBehavior.CONTINUE; } RequestInfo requestInfo = httpState.getRequestInfo(); ResponseInfo responseInfo = httpState.getResponseInfo(); if (logger.isDebugEnabled()) { runnableWithTracingAndMdc( () -> logger.debug("Cleaning up channel after it was closed. closed_channel_id={}", ctx.channel().toString()), ctx ).run(); } // The request/response is definitely done at this point since the channel is closing. Set the response end // time if it hasn't already been done. httpState.setResponseEndTimeNanosToNowIfNotAlreadySet(); // Handle the case where the response wasn't fully sent or tracing wasn't completed for some reason. // We want to finish the distributed tracing span for this request since there's no other place it // might be done, and if the request wasn't fully sent then we should spit out a log message so // debug investigations can find out what happened. @SuppressWarnings("SimplifiableConditionalExpression") boolean tracingAlreadyCompleted = httpState.isTraceCompletedOrScheduled(); boolean responseNotFullySent = responseInfo == null || !responseInfo.isResponseSendingLastChunkSent(); if (responseNotFullySent || !tracingAlreadyCompleted) { runnableWithTracingAndMdc( () -> { if (responseNotFullySent) { logger.warn( "The caller's channel was closed before a response could be sent. Distributed tracing " + "will be completed now if it wasn't already done, and we will attempt to output an " + "access log if needed. Any dangling resources will be released. " + "response_info_is_null={}", (responseInfo == null) ); } if (!tracingAlreadyCompleted) { httpState.setTraceCompletedOrScheduled(true); Span currentSpan = Tracer.getInstance().getCurrentSpan(); if (currentSpan != null && !currentSpan.isCompleted()) Tracer.getInstance().completeRequestSpan(); } }, ctx ).run(); } // Make sure access logging is handled if (!httpState.isAccessLogCompletedOrScheduled() && accessLogger != null) { httpState.setAccessLogCompletedOrScheduled(true); RequestInfo requestInfoToUse = (requestInfo == null) ? RequestInfoImpl.dummyInstanceForUnknownRequests() : requestInfo; accessLogger.log( requestInfoToUse, httpState.getActualResponseObject(), responseInfo, httpState.calculateTotalRequestTimeMillis() ); } // Make sure metrics is handled handleMetricsForCompletedRequestIfNotAlreadyDone(httpState); // Tell the RequestInfo it can release all its resources. if (requestInfo != null) requestInfo.releaseAllResources(); releaseProxyRouterStateResources(proxyRouterState, ctx); } catch(Throwable t) { runnableWithTracingAndMdc( () -> logger.error( "An unexpected error occurred during ChannelPipelineFinalizerHandler.doChannelInactive() - this " + "should not happen and indicates a bug that needs to be fixed in Riposte.", t), ctx ).run(); } return PipelineContinuationBehavior.CONTINUE; } /** * Tell the ProxyRouterProcessingState that the request streaming is cancelled so that it will trigger its chunk * streaming error handling with an artificial exception. If the call had already succeeded previously then this * will do nothing, but if it hasn't already succeeded then it's not going to (since the connection is closing) and * doing this will cause any resources it's holding onto to be released. * *

Also tell ProxyRouterProcessingState to cancel any downstream call. If the downstream call had already * finished this would do nothing. * * @param proxyRouterState The state to cleanup. */ protected void releaseProxyRouterStateResources(ProxyRouterProcessingState proxyRouterState, ChannelHandlerContext ctx) { if (proxyRouterState != null) { proxyRouterState.cancelRequestStreaming( ARTIFICIAL_SERVER_WORKER_CHANNEL_CLOSED_EXCEPTION, ctx ); proxyRouterState.cancelDownstreamRequest(ARTIFICIAL_SERVER_WORKER_CHANNEL_CLOSED_EXCEPTION); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy