use of com.nike.riposte.server.http.ProxyRouterProcessingState in project riposte by Nike-Inc.
the class ProxyRouterEndpointExecutionHandler method doChannelRead.
@Override
public PipelineContinuationBehavior doChannelRead(ChannelHandlerContext ctx, Object msg) {
HttpProcessingState state = ChannelAttributes.getHttpProcessingStateForChannel(ctx).get();
Endpoint<?> endpoint = state.getEndpointForExecution();
if (shouldHandleDoChannelReadMessage(msg, endpoint)) {
ProxyRouterProcessingState proxyRouterState = getOrCreateProxyRouterProcessingState(ctx);
ProxyRouterEndpoint endpointProxyRouter = ((ProxyRouterEndpoint) endpoint);
RequestInfo<?> requestInfo = state.getRequestInfo();
if (msg instanceof HttpRequest) {
if (requestInfo instanceof RiposteInternalRequestInfo) {
// Tell this RequestInfo that we'll be managing the release of content chunks, so that when
// RequestInfo.releaseAllResources() is called we don't have extra reference count removals.
((RiposteInternalRequestInfo) requestInfo).contentChunksWillBeReleasedExternally();
}
// We're supposed to start streaming. There may be pre-endpoint-execution validation logic or other work
// that needs to happen before the endpoint is executed, so set up the CompletableFuture for the
// endpoint call to only execute if the pre-endpoint-execution validation/work chain is successful.
CompletableFuture<DownstreamRequestFirstChunkInfo> firstChunkFuture = state.getPreEndpointExecutionWorkChain().thenCompose(functionWithTracingAndMdc(aVoid -> endpointProxyRouter.getDownstreamRequestFirstChunkInfo(requestInfo, longRunningTaskExecutor, ctx), ctx));
Long endpointTimeoutOverride = endpointProxyRouter.completableFutureTimeoutOverrideMillis();
long callTimeoutValueToUse = (endpointTimeoutOverride == null) ? defaultCompletableFutureTimeoutMillis : endpointTimeoutOverride;
// When the first chunk is ready, stream it downstream and set up what happens afterward.
firstChunkFuture.whenComplete((downstreamRequestFirstChunkInfo, throwable) -> {
Optional<ManualModeTask<HttpResponse>> circuitBreakerManualTask = getCircuitBreaker(downstreamRequestFirstChunkInfo, ctx).map(CircuitBreaker::newManualModeTask);
StreamingCallback callback = new StreamingCallbackForCtx(ctx, circuitBreakerManualTask, endpointProxyRouter, requestInfo, proxyRouterState);
if (throwable != null) {
// Something blew up trying to determine the first chunk info.
callback.unrecoverableErrorOccurred(throwable, true);
} else if (!ctx.channel().isOpen()) {
// The channel was closed for some reason before we were able to start streaming.
String errorMsg = "The channel from the original caller was closed before we could begin the " + "downstream call.";
Exception channelClosedException = new RuntimeException(errorMsg);
runnableWithTracingAndMdc(() -> logger.warn(errorMsg), ctx).run();
callback.unrecoverableErrorOccurred(channelClosedException, true);
} else {
try {
// Ok we have the first chunk info. Start by setting the downstream call info in the request
// info (i.e. for access logs if desired)
requestInfo.addRequestAttribute(DOWNSTREAM_CALL_PATH_REQUEST_ATTR_KEY, HttpUtils.extractPath(downstreamRequestFirstChunkInfo.firstChunk.uri()));
// Try our circuit breaker (if we have one).
Throwable circuitBreakerException = null;
try {
circuitBreakerManualTask.ifPresent(ManualModeTask::throwExceptionIfCircuitBreakerIsOpen);
} catch (Throwable t) {
circuitBreakerException = t;
}
if (circuitBreakerException == null) {
// No circuit breaker, or the breaker is closed. We can now stream the first chunk info.
String downstreamHost = downstreamRequestFirstChunkInfo.host;
int downstreamPort = downstreamRequestFirstChunkInfo.port;
HttpRequest downstreamRequestFirstChunk = downstreamRequestFirstChunkInfo.firstChunk;
boolean isSecureHttpsCall = downstreamRequestFirstChunkInfo.isHttps;
boolean relaxedHttpsValidation = downstreamRequestFirstChunkInfo.relaxedHttpsValidation;
boolean performSubSpanAroundDownstreamCall = downstreamRequestFirstChunkInfo.performSubSpanAroundDownstreamCall;
boolean addTracingHeadersToDownstreamCall = downstreamRequestFirstChunkInfo.addTracingHeadersToDownstreamCall;
// Tell the proxyRouterState about the streaming callback so that
// callback.unrecoverableErrorOccurred(...) can be called in the case of an error
// on subsequent chunks.
proxyRouterState.setStreamingCallback(callback);
// Setup the streaming channel future with everything it needs to kick off the
// downstream request.
proxyRouterState.setStreamingStartTimeNanos(System.nanoTime());
CompletableFuture<StreamingChannel> streamingChannel = streamingAsyncHttpClient.streamDownstreamCall(downstreamHost, downstreamPort, downstreamRequestFirstChunk, isSecureHttpsCall, relaxedHttpsValidation, callback, callTimeoutValueToUse, performSubSpanAroundDownstreamCall, addTracingHeadersToDownstreamCall, proxyRouterState, requestInfo, ctx);
// Tell the streaming channel future what to do when it completes.
streamingChannel = streamingChannel.whenComplete((sc, cause) -> {
if (cause == null) {
// Successfully connected and sent the first chunk. We can now safely let
// the remaining content chunks through for streaming.
proxyRouterState.triggerChunkProcessing(sc);
} else {
// Something blew up while connecting to the downstream server.
callback.unrecoverableErrorOccurred(cause, true);
}
});
// Set the streaming channel future on the state so it can be connected to.
proxyRouterState.setStreamingChannelCompletableFuture(streamingChannel);
} else {
// Circuit breaker is tripped (or otherwise threw an unexpected exception). Immediately
// short circuit the error back to the client.
callback.unrecoverableErrorOccurred(circuitBreakerException, true);
}
} catch (Throwable t) {
callback.unrecoverableErrorOccurred(t, true);
}
}
});
} else if (msg instanceof HttpContent) {
HttpContent msgContent = (HttpContent) msg;
// chunk-streaming behavior and subsequent cleanup for the given HttpContent.
if (!releaseContentChunkIfStreamAlreadyFailed(msgContent, proxyRouterState)) {
registerChunkStreamingAction(proxyRouterState, msgContent, ctx);
}
}
return PipelineContinuationBehavior.DO_NOT_FIRE_CONTINUE_EVENT;
}
return PipelineContinuationBehavior.CONTINUE;
}
use of com.nike.riposte.server.http.ProxyRouterProcessingState in project riposte by Nike-Inc.
the class ProxyRouterEndpointExecutionHandler method getOrCreateProxyRouterProcessingState.
protected ProxyRouterProcessingState getOrCreateProxyRouterProcessingState(ChannelHandlerContext ctx) {
Attribute<ProxyRouterProcessingState> proxyRouterStateAttribute = ChannelAttributes.getProxyRouterProcessingStateForChannel(ctx);
ProxyRouterProcessingState proxyRouterState = proxyRouterStateAttribute.get();
if (proxyRouterState == null) {
proxyRouterState = new ProxyRouterProcessingState();
proxyRouterStateAttribute.set(proxyRouterState);
// noinspection deprecation
proxyRouterState.setDistributedTracingConfig(distributedTracingConfig);
}
return proxyRouterState;
}
use of com.nike.riposte.server.http.ProxyRouterProcessingState in project riposte by Nike-Inc.
the class RequestStateCleanerHandler method channelRead.
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof HttpRequest) {
// New request incoming - setup/clear *all* state objects for new requests
for (ProcessingStateClassAndKeyPair<? extends ProcessingState> stateClassAndKeyPair : PROCESSING_STATE_ATTRIBUTE_KEYS) {
// See if we have an existing state object for this channel for the given state type.
@SuppressWarnings("unchecked") AttributeKey<ProcessingState> attrKey = (AttributeKey<ProcessingState>) stateClassAndKeyPair.getRight();
Attribute<ProcessingState> processingStateAttr = ctx.channel().attr(attrKey);
ProcessingState processingState = processingStateAttr.get();
if (processingState == null) {
// We don't already have one for this channel, so create one and register it.
processingState = stateClassAndKeyPair.getLeft().newInstance();
processingStateAttr.set(processingState);
}
// Clean the state for the new request.
processingState.cleanStateForNewRequest();
}
HttpProcessingState httpProcessingState = ChannelAttributes.getHttpProcessingStateForChannel(ctx).get();
// Set the DistributedTracingConfig on the HttpProcessingState.
// noinspection deprecation - This is the only place that should actually be calling this method.
httpProcessingState.setDistributedTracingConfig(distributedTracingConfig);
// Send a request received event to the metricsListener.
if (metricsListener != null) {
metricsListener.onEvent(ServerMetricsEvent.REQUEST_RECEIVED, httpProcessingState);
}
// Remove the idle channel timeout handler (if there is one) so that it doesn't kill this new request if the
// endpoint takes longer to complete than the idle timeout value - the idle channel timeout is only for
// timing out channels that are idle *in-between* requests.
ChannelPipeline pipeline = ctx.pipeline();
ChannelHandler idleChannelTimeoutHandler = pipeline.get(HttpChannelInitializer.IDLE_CHANNEL_TIMEOUT_HANDLER_NAME);
if (idleChannelTimeoutHandler != null)
pipeline.remove(idleChannelTimeoutHandler);
// last chunk when the timeout hits.
if (incompleteHttpCallTimeoutMillis > 0 && !(msg instanceof LastHttpContent)) {
IncompleteHttpCallTimeoutHandler newHandler = new IncompleteHttpCallTimeoutHandler(incompleteHttpCallTimeoutMillis);
ChannelHandler existingHandler = pipeline.get(INCOMPLETE_HTTP_CALL_TIMEOUT_HANDLER_NAME);
if (existingHandler == null) {
pipeline.addFirst(INCOMPLETE_HTTP_CALL_TIMEOUT_HANDLER_NAME, newHandler);
} else {
logger.error("Handling HttpRequest for new request and found an IncompleteHttpCallTimeoutHandler " + "already in the pipeline. This should not be possible. A new " + "IncompleteHttpCallTimeoutHandler will replace the old one. worker_channel_id={}", ctx.channel().toString());
pipeline.replace(existingHandler, INCOMPLETE_HTTP_CALL_TIMEOUT_HANDLER_NAME, newHandler);
}
}
ProxyRouterProcessingState proxyRouterProcessingState = ChannelAttributes.getProxyRouterProcessingStateForChannel(ctx).get();
// Set the DistributedTracingConfig on the ProxyRouterProcessingState.
// noinspection deprecation - This is the only place that should actually be calling this method.
proxyRouterProcessingState.setDistributedTracingConfig(distributedTracingConfig);
} else if (msg instanceof LastHttpContent) {
// The HTTP call is complete, so we can remove the IncompleteHttpCallTimeoutHandler.
ChannelPipeline pipeline = ctx.pipeline();
ChannelHandler existingHandler = pipeline.get(INCOMPLETE_HTTP_CALL_TIMEOUT_HANDLER_NAME);
if (existingHandler != null)
pipeline.remove(INCOMPLETE_HTTP_CALL_TIMEOUT_HANDLER_NAME);
}
// Continue on the pipeline processing.
super.channelRead(ctx, msg);
}
use of com.nike.riposte.server.http.ProxyRouterProcessingState in project riposte by Nike-Inc.
the class ResponseSenderHandler method sendResponse.
protected void sendResponse(ChannelHandlerContext ctx, Object msg, boolean sendLastDitchResponseInline) throws JsonProcessingException {
try {
// Try to send the response.
doSendResponse(ctx, msg);
} catch (Exception origSendEx) {
boolean shouldRethrowOriginalSendEx = true;
// Something went wrong while trying to send the response. We want to create a generic service error and
// send that back to the caller if possible.
// The HttpProcessingState will never be null thanks to ExceptionHandlingHandler
HttpProcessingState state = ChannelAttributes.getHttpProcessingStateForChannel(ctx).get();
boolean alreadyTriedSendingLastDitchResponse = alreadyTriedSendingLastDitchResponse(state);
// Cancel any proxy router streaming that may be happening.
ProxyRouterProcessingState proxyRouterProcessingState = ChannelAttributes.getProxyRouterProcessingStateForChannel(ctx).get();
if (proxyRouterProcessingState != null) {
proxyRouterProcessingState.cancelRequestStreaming(origSendEx, ctx);
proxyRouterProcessingState.cancelDownstreamRequest(origSendEx);
}
// we can create a generic service error and try sending that.
if (state.isResponseSendingStarted()) {
runnableWithTracingAndMdc(() -> {
logger.error("An unexpected error occurred while sending the response. At least part of the " + "response was sent, so there's nothing we can do at this point but close the connection.", origSendEx);
// Add this error to the current span if possible, but only if no error tag already exists.
Span currentSpan = Tracer.getInstance().getCurrentSpan();
if (currentSpan != null && currentSpan.getTags().get(KnownZipkinTags.ERROR) == null) {
String errorTagValue = (origSendEx.getMessage() == null) ? origSendEx.getClass().getSimpleName() : origSendEx.getMessage();
currentSpan.putTag(KnownZipkinTags.ERROR, errorTagValue);
}
}, ctx).run();
ctx.channel().close();
} else if (!alreadyTriedSendingLastDitchResponse) {
// Mark that we've tried doing the last-ditch response so that we only ever attempt it once.
markTriedSendingLastDitchResponse(state);
// We haven't already started response sending, so we can try sending a last ditch error response
// instead that represents the response-sending exception.
String errorId = UUID.randomUUID().toString();
ResponseInfo<?> lastDitchErrorResponseInfo = ResponseInfo.newBuilder(new ErrorResponseBodyImpl(errorId, Collections.singleton(SampleCoreApiError.GENERIC_SERVICE_ERROR))).withHeaders(new DefaultHttpHeaders().set("error_uid", errorId)).withHttpStatusCode(500).build();
state.setResponseInfo(lastDitchErrorResponseInfo, origSendEx);
runnableWithTracingAndMdc(() -> logger.error("An unexpected error occurred while attempting to send a response. We'll attempt to send a " + "last-ditch error message. error_uid={}", errorId, origSendEx), ctx).run();
if (sendLastDitchResponseInline) {
doSendResponse(ctx, msg);
// The last ditch response was successfully sent - we don't need to rethrow the original exception.
shouldRethrowOriginalSendEx = false;
}
}
if (shouldRethrowOriginalSendEx) {
// will be allowed to propagate out of this class.
throw origSendEx;
}
}
}
use of com.nike.riposte.server.http.ProxyRouterProcessingState in project riposte by Nike-Inc.
the class ChannelPipelineFinalizerHandler method doChannelInactive.
/**
* 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.
*
* <p>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) {
try {
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);
httpState.handleTracingResponseTaggingAndFinalSpanNameIfNotAlreadyDone();
Span currentSpan = Tracer.getInstance().getCurrentSpan();
if (currentSpan != null && !currentSpan.isCompleted()) {
Tracer.getInstance().completeRequestSpan();
}
}
}, ctx).run();
} catch (Throwable t) {
logErrorWithTracing("An unexpected error occurred while trying to finalize distributed tracing. " + "This exception will be swallowed.", t, httpState);
}
}
// Make sure access logging is handled
try {
if (!httpState.isAccessLogCompletedOrScheduled() && accessLogger != null) {
httpState.setAccessLogCompletedOrScheduled(true);
RequestInfo<?> requestInfoToUse = (requestInfo == null) ? RequestInfoImpl.dummyInstanceForUnknownRequests() : requestInfo;
accessLogger.log(requestInfoToUse, httpState.getActualResponseObject(), responseInfo, httpState.calculateTotalRequestTimeMillis());
}
} catch (Throwable t) {
logErrorWithTracing("An unexpected error occurred while trying to finalize access logging. " + "This exception will be swallowed.", t, httpState);
}
// Make sure metrics is handled
handleMetricsForCompletedRequestIfNotAlreadyDone(httpState);
// Tell the RequestInfo it can release all its resources.
if (requestInfo != null) {
try {
requestInfo.releaseAllResources();
} catch (Throwable t) {
logErrorWithTracing("An unexpected error occurred while trying to release request resources. " + "This exception will be swallowed.", t, httpState);
}
}
try {
releaseProxyRouterStateResources(proxyRouterState, ctx);
} catch (Throwable t) {
logErrorWithTracing("An unexpected error occurred while trying to release ProxyRouter state resources. " + "This exception will be swallowed.", t, httpState);
}
} catch (Throwable t) {
logErrorWithTracing("An unexpected error occurred during ChannelPipelineFinalizerHandler.doChannelInactive() - this " + "should not happen and indicates a bug that needs to be fixed in Riposte.", t, ctx);
}
return PipelineContinuationBehavior.CONTINUE;
}
Aggregations