public void cancelDownstreamRequest(Throwable reason) { if (streamingCallback != null) streamingCallback.cancelStreamingToOriginalCaller(); // Do nothing else if a StreamingChannel was never started. if (streamingChannelCompletableFuture == null) return; streamingChannelCompletableFuture.whenComplete((sc, error) -> { if (sc != null) { // A StreamingChannel was created. Tell it to stop if it's still going. if (sc.isDownstreamCallActive()) sc.closeChannelDueToUnrecoverableError(reason); } }); }
protected ChannelPool getPooledChannelFuture(String downstreamHost, int downstreamPort) { return getPoolMap().get( resolveHostnameToInetSocketAddressWithMultiIpSupport(downstreamHost, downstreamPort) ); }
@Override protected SimpleChannelPool newPool(InetSocketAddress key) { return new SimpleChannelPool( generateClientBootstrap(eventLoopGroup, channelClass).remoteAddress(key), new ChannelPoolHandlerImpl(), CHANNEL_HEALTH_CHECK_INSTANCE ) {
protected static void markChannelBrokenAndLogInfoIfHttpClientCodecStateIsNotZero(Channel ch, String callContextForLogs) { HttpClientCodec currentCodec = (HttpClientCodec) ch.pipeline().get(HTTP_CLIENT_CODEC_HANDLER_NAME); if (currentCodec != null) { int currentHttpClientCodecInboundState = determineHttpClientCodecInboundState(currentCodec); if (currentHttpClientCodecInboundState != 0) { boolean channelAlreadyBroken = channelIsMarkedAsBeingBroken(ch); logger.warn( "HttpClientCodec inbound state was not 0. The channel will be marked as broken so it won't be " + "used. bad_httpclientcodec_inbound_state={}, channel_already_broken={}, channel_id={}, " + "call_context=\"{}\"", currentHttpClientCodecInboundState, channelAlreadyBroken, ch.toString(), callContextForLogs ); markChannelAsBroken(ch); } else { int currentHttpClientCodecOutboundState = determineHttpClientCodecOutboundState(currentCodec); if (currentHttpClientCodecOutboundState != 0) { boolean channelAlreadyBroken = channelIsMarkedAsBeingBroken(ch); logger.warn( "HttpClientCodec outbound state was not 0. The channel will be marked as broken so it won't be " + "used. bad_httpclientcodec_outbound_state={}, channel_already_broken={}, channel_id={}, " + "call_context=\"{}\"", currentHttpClientCodecOutboundState, channelAlreadyBroken, ch.toString(), callContextForLogs ); markChannelAsBroken(ch); } } } }
@DataProvider(value = { "80 | false | localhost | localhost", "80 | true | localhost | localhost:80", "8080 | false | localhost | localhost:8080", "443 | true | localhost | localhost", "443 | false | localhost | localhost:443", "8080 | true | localhost | localhost:8080", }, splitBy = "\\|") @Test public void streamDownstreamCall_setsHostHeaderCorrectly(int downstreamPort, boolean isSecure, String downstreamHost, String expectedHostHeader) { // given DefaultHttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, ""); ChannelHandlerContext ctx = mockChannelHandlerContext(); StreamingCallback streamingCallback = mock(StreamingCallback.class); // when new StreamingAsyncHttpClient( 200, 200, true, mock(DistributedTracingConfig.class) ).streamDownstreamCall( downstreamHost, downstreamPort, request, isSecure, false, streamingCallback, 200, true, true, ctx ); // then assertThat(request.headers().get(HOST)).isEqualTo(expectedHostHeader); }
@Before public void beforeMethod() { channelMock = mock(Channel.class); channelPoolMock = mock(ChannelPool.class); eventLoopMock = mock(EventLoop.class); contentChunkMock = mock(HttpContent.class); callActiveHolder = new ObjectHolder<>(); callActiveHolder.heldObject = true; downstreamLastChunkSentHolder = new ObjectHolder<>(); downstreamLastChunkSentHolder.heldObject = false; spanForDownstreamCallMock = mock(Span.class); proxySpanTaggingStrategyMock = mock(ProxyRouterSpanNamingAndTaggingStrategy.class); streamingChannelSpy = spy(new StreamingChannel( channelMock, channelPoolMock, callActiveHolder, downstreamLastChunkSentHolder, null, null, spanForDownstreamCallMock, proxySpanTaggingStrategyMock )); writeAndFlushChannelFutureMock = mock(ChannelFuture.class); doReturn(eventLoopMock).when(channelMock).eventLoop(); doReturn(writeAndFlushChannelFutureMock).when(channelMock).writeAndFlush(contentChunkMock); channelIsBrokenAttrMock = mock(Attribute.class); doReturn(channelIsBrokenAttrMock).when(channelMock).attr(CHANNEL_IS_BROKEN_ATTR); streamChunkChannelPromiseMock = mock(ChannelPromise.class); doReturn(streamChunkChannelPromiseMock).when(channelMock).newPromise(); failedFutureMock = mock(ChannelFuture.class); doReturn(failedFutureMock).when(channelMock).newFailedFuture(any(Throwable.class)); }
@Test public void StreamingChannel_doStreamChunk_works_as_expected_when_closeChannelDueToUnrecoverableError_was_called_previously() { // given streamingChannelSpy.channelClosedDueToUnrecoverableError = true; // when ChannelFuture result = streamingChannelSpy.doStreamChunk(contentChunkMock); // then verify(channelMock, never()).writeAndFlush(any(Object.class)); verify(contentChunkMock).release(); verifyFailedChannelFuture( result, "Unable to stream chunks downstream - the channel was closed previously due to an unrecoverable error", null ); }
@Test public void StreamingChannel_streamChunk_fails_promise_with_unexpected_exception() { // given Throwable crazyEx = new RuntimeException("kaboom"); doThrow(crazyEx).when(eventLoopMock).execute(any(Runnable.class)); // when ChannelFuture result = streamingChannelSpy.streamChunk(contentChunkMock); // then verifyFailedChannelFuture(result, "StreamingChannel.streamChunk() threw an exception", crazyEx); }
@DataProvider(value = { "true", "false" }) @Test public void StreamingChannel_doCloseChannelDueToUnrecoverableError_works_as_expected(boolean callActive) { // given streamingChannelSpy.callActiveHolder.heldObject = callActive; Throwable unrecoverableError = new RuntimeException("kaboom"); // when streamingChannelSpy.doCloseChannelDueToUnrecoverableError(unrecoverableError); // then if (callActive) { verify(channelIsBrokenAttrMock).set(true); verifyChannelReleasedBackToPool(streamingChannelSpy.callActiveHolder, channelPoolMock, channelMock); verify(channelMock).close(); } else { verify(channelIsBrokenAttrMock, never()).set(anyBoolean()); verify(channelMock, never()).close(); } }
public void closeChannelDueToUnrecoverableError(Throwable cause) { try { // Ignore subsequent calls to this method, and only try to do something if the call is still active. // If the call is *not* active, then everything has already been cleaned up and we shouldn't // do anything because the channel might have already been handed out for a different call. if (!channelClosedDueToUnrecoverableError && callActiveHolder.heldObject) { // Schedule the close on the channel's event loop. channel.eventLoop().execute(() -> doCloseChannelDueToUnrecoverableError(cause)); return; } if (!alreadyLoggedMessageAboutIgnoringCloseDueToError && logger.isDebugEnabled()) { runnableWithTracingAndMdc( () -> logger.debug( "Ignoring calls to StreamingChannel.closeChannelDueToUnrecoverableError() because it " + "has already been called, or the call is no longer active. " + "previously_called={}, call_is_active={}", channelClosedDueToUnrecoverableError, callActiveHolder.heldObject ), distributedTracingSpanStack, distributedTracingMdcInfo ).run(); } alreadyLoggedMessageAboutIgnoringCloseDueToError = true; } finally { channelClosedDueToUnrecoverableError = true; } }
@Override public Future<Boolean> isHealthy(Channel channel) { // See if we've marked the channel as being non-usable first. if (channelIsMarkedAsBeingBroken(channel)) return channel.eventLoop().newSucceededFuture(Boolean.FALSE); // We haven't marked it broken, so fallback to the default channel health checker. return ChannelHealthChecker.ACTIVE.isHealthy(channel); } }
/** * Returns the name that should be used for the span surrounding the downstream call. Defaults to whatever {@link * ProxyRouterSpanNamingAndTaggingStrategy#getInitialSpanName(Object)} returns, with a fallback * of {@link HttpRequestTracingUtils#getFallbackSpanNameForHttpRequest(String, String)} if the naming strategy * returned null or blank string. * * @param downstreamRequest The Netty {@link HttpRequest} for the downstream call. * @param namingStrategy The {@link ProxyRouterSpanNamingAndTaggingStrategy} being used. * @return The name that should be used for the span surrounding the downstream call. */ protected @NotNull String getSubspanSpanName( @NotNull HttpRequest downstreamRequest, @NotNull ProxyRouterSpanNamingAndTaggingStrategy<Span> namingStrategy ) { String spanNameFromStrategy = namingStrategy.getInitialSpanName(downstreamRequest); if (StringUtils.isNotBlank(spanNameFromStrategy)) { return spanNameFromStrategy; } // The naming strategy didn't have anything for us. Fall back to something reasonable. return getFallbackSpanName(downstreamRequest); }
protected static void markChannelBrokenAndLogInfoIfHttpClientCodecStateIsNotZero(Channel ch, String callContextForLogs) { HttpClientCodec currentCodec = (HttpClientCodec) ch.pipeline().get(HTTP_CLIENT_CODEC_HANDLER_NAME); if (currentCodec != null) { int currentHttpClientCodecInboundState = determineHttpClientCodecInboundState(currentCodec); if (currentHttpClientCodecInboundState != 0) { boolean channelAlreadyBroken = channelIsMarkedAsBeingBroken(ch); logger.warn( "HttpClientCodec inbound state was not 0. The channel will be marked as broken so it won't be " + "used. bad_httpclientcodec_inbound_state={}, channel_already_broken={}, channel_id={}, " + "call_context=\"{}\"", currentHttpClientCodecInboundState, channelAlreadyBroken, ch.toString(), callContextForLogs ); markChannelAsBroken(ch); } else { int currentHttpClientCodecOutboundState = determineHttpClientCodecOutboundState(currentCodec); if (currentHttpClientCodecOutboundState != 0) { boolean channelAlreadyBroken = channelIsMarkedAsBeingBroken(ch); logger.warn( "HttpClientCodec outbound state was not 0. The channel will be marked as broken so it won't be " + "used. bad_httpclientcodec_outbound_state={}, channel_already_broken={}, channel_id={}, " + "call_context=\"{}\"", currentHttpClientCodecOutboundState, channelAlreadyBroken, ch.toString(), callContextForLogs ); markChannelAsBroken(ch); } } } }
public void cancelDownstreamRequest(Throwable reason) { if (streamingCallback != null) streamingCallback.cancelStreamingToOriginalCaller(); // Do nothing else if a StreamingChannel was never started. if (streamingChannelCompletableFuture == null) return; streamingChannelCompletableFuture.whenComplete((sc, error) -> { if (sc != null) { // A StreamingChannel was created. Tell it to stop if it's still going. if (sc.isDownstreamCallActive()) sc.closeChannelDueToUnrecoverableError(reason); } }); }
@Test public void StreamingChannel_doStreamChunk_works_as_expected_when_downstream_call_is_not_active() { // given streamingChannelSpy.callActiveHolder.heldObject = false; // when ChannelFuture result = streamingChannelSpy.doStreamChunk(contentChunkMock); // then verify(channelMock, never()).writeAndFlush(any(Object.class)); verify(contentChunkMock).release(); verifyFailedChannelFuture( result, "Unable to stream chunk - downstream call is no longer active.", null ); }
protected ChannelPool getPooledChannelFuture(String downstreamHost, int downstreamPort) { return getPoolMap().get( resolveHostnameToInetSocketAddressWithMultiIpSupport(downstreamHost, downstreamPort) ); }
@Override protected SimpleChannelPool newPool(InetSocketAddress key) { return new SimpleChannelPool( generateClientBootstrap(eventLoopGroup, channelClass).remoteAddress(key), new ChannelPoolHandlerImpl(), CHANNEL_HEALTH_CHECK_INSTANCE ) {
@Override public Future<Boolean> isHealthy(Channel channel) { // See if we've marked the channel as being non-usable first. if (channelIsMarkedAsBeingBroken(channel)) return channel.eventLoop().newSucceededFuture(Boolean.FALSE); // We haven't marked it broken, so fallback to the default channel health checker. return ChannelHealthChecker.ACTIVE.isHealthy(channel); } }
/** * Returns the name that should be used for the span surrounding the downstream call. Defaults to whatever {@link * ProxyRouterSpanNamingAndTaggingStrategy#getInitialSpanName(Object)} returns, with a fallback * of {@link HttpRequestTracingUtils#getFallbackSpanNameForHttpRequest(String, String)} if the naming strategy * returned null or blank string. * * @param downstreamRequest The Netty {@link HttpRequest} for the downstream call. * @param namingStrategy The {@link ProxyRouterSpanNamingAndTaggingStrategy} being used. * @return The name that should be used for the span surrounding the downstream call. */ protected @NotNull String getSubspanSpanName( @NotNull HttpRequest downstreamRequest, @NotNull ProxyRouterSpanNamingAndTaggingStrategy<Span> namingStrategy ) { String spanNameFromStrategy = namingStrategy.getInitialSpanName(downstreamRequest); if (StringUtils.isNotBlank(spanNameFromStrategy)) { return spanNameFromStrategy; } // The naming strategy didn't have anything for us. Fall back to something reasonable. return getFallbackSpanName(downstreamRequest); }
@Test public void StreamingChannel_doStreamChunk_works_as_expected_when_crazy_exception_is_thrown() { // given Throwable crazyEx = new RuntimeException("kaboom"); doThrow(crazyEx).when(channelMock).writeAndFlush(any(Object.class)); // when ChannelFuture result = streamingChannelSpy.doStreamChunk(contentChunkMock); // then verify(channelMock).writeAndFlush(any(Object.class)); verify(contentChunkMock, never()).release(); verifyFailedChannelFuture( result, "StreamingChannel.doStreamChunk() threw an exception", crazyEx ); }