/** * @param asyncHttpClientHelper The async HTTP client you want this method to use to make the AWS metadata call. * * @return A {@link CompletableFuture} that will contain the AWS instance ID this app is running on (assuming it * completes successfully). If an error occurs retrieving the instance ID from AWS then the error will be logged and * {@link AppInfo#UNKNOWN_VALUE} returned as the value. */ public static CompletableFuture<String> getAwsInstanceId(AsyncHttpClientHelper asyncHttpClientHelper) { return asyncHttpClientHelper.executeAsyncHttpRequest( asyncHttpClientHelper.getRequestBuilder(AMAZON_METADATA_INSTANCE_ID_URL, HttpMethod.GET), Response::getResponseBody ).handle((instanceId, error) -> { if (error != null) { logger.error("Unable to get instance ID info from AWS metadata service.", error); return AppInfo.UNKNOWN_VALUE; } if (instanceId == null) { logger.error("AWS metadata service returned null for instance ID. Using 'unknown' as fallback."); return AppInfo.UNKNOWN_VALUE; } return instanceId; }); }
@Test public void fluent_setters_work_as_expected() { // when AsyncHttpClientHelper instance = new AsyncHttpClientHelper(false); assertThat(instance.performSubSpanAroundDownstreamCalls).isFalse(); AsyncHttpClientHelper result = instance.setPerformSubSpanAroundDownstreamCalls(true) .setDefaultSignatureCalculator(signatureCalculator) .setSpanNamingAndTaggingStrategy(tagAndNamingStrategy); // then assertThat(result).isSameAs(instance); assertThat(instance.performSubSpanAroundDownstreamCalls).isTrue(); assertThat(Whitebox.getInternalState(instance.asyncHttpClient, "signatureCalculator")).isEqualTo(signatureCalculator); assertThat(instance.spanNamingAndTaggingStrategy).isSameAs(tagAndNamingStrategy); }
/** * Call this before one of the {@code executeAsyncHttpRequest(...)} methods in order to get a request builder you * can populate with query params, headers, body, etc. Pass in a non-empty {@code customCircuitBreaker} argument to * specify the exact circuit breaker you want to use, pass in an empty {@code customCircuitBreaker} if you want the * HTTP client to use a default one based on the host being called, and pass in true for the {@code * disableCircuitBreaker} argument if you want to disable circuit breaking entirely for this call. */ public RequestBuilderWrapper getRequestBuilder(String url, HttpMethod method, Optional<CircuitBreaker<Response>> customCircuitBreaker, boolean disableCircuitBreaker) { RequestBuilderWrapper wrapper = generateRequestBuilderWrapper( url, method, customCircuitBreaker, disableCircuitBreaker ); // The AsyncHttpClient doesn't properly split traffic when a DNS has multiple IP addresses associated with it, // so we have to hack it ourselves. wrapper.requestBuilder.setNameResolver(getMultiIpAwareNameResolver()); return wrapper; }
@Test public void setSpanNamingAndTaggingStrategy_throws_IllegalArgumentException_if_passed_null() { // given AsyncHttpClientHelper instance = new AsyncHttpClientHelper(false); // when Throwable ex = catchThrowable(() -> instance.setSpanNamingAndTaggingStrategy(null)); // then assertThat(ex) .isInstanceOf(IllegalArgumentException.class) .hasMessage("spanNamingAndTaggingStrategy cannot be null"); }
public void verify_basic_functionality(boolean surroundWithSubspan, boolean parentSpanExists) throws Exception { AsyncHttpClientHelper asyncClient = new AsyncHttpClientHelper(surroundWithSubspan); RequestBuilderWrapper rbw = asyncClient.getRequestBuilder(fullUrl, HttpMethod.GET); rbw.requestBuilder.setHeader(TestEndpoint.EXPECTED_HEADER_KEY, TestEndpoint.EXPECTED_HEADER_VAL); rbw.requestBuilder.setBody(TestEndpoint.EXPECTED_REQUEST_PAYLOAD); Response result = asyncClient.executeAsyncHttpRequest( rbw, response -> response, distributedTraceStackForCall, mdcContextForCall ).join();
@DataProvider(value = { "true", "false" }, splitBy = "\\|") @Test public void executeAsyncHttpRequest_completes_future_if_exception_happens_during_setup( boolean throwCircuitBreakerOpenException) { // given RuntimeException exToThrow = (throwCircuitBreakerOpenException) ? new CircuitBreakerOpenException("foo", "kaboom") : new RuntimeException("kaboom"); doThrow(exToThrow).when(helperSpy).getCircuitBreaker(any(RequestBuilderWrapper.class)); Logger loggerMock = mock(Logger.class); Whitebox.setInternalState(helperSpy, "logger", loggerMock); // when CompletableFuture result = helperSpy .executeAsyncHttpRequest(mock(RequestBuilderWrapper.class), mock(AsyncResponseHandler.class), null, null); // then assertThat(result).isCompletedExceptionally(); Throwable ex = catchThrowable(result::get); assertThat(ex) .isInstanceOf(ExecutionException.class) .hasCause(exToThrow); if (throwCircuitBreakerOpenException) verifyZeroInteractions(loggerMock); else verify(loggerMock).error(anyString(), anyString(), anyString(), eq(exToThrow)); }
@Test public void executeAsyncHttpRequest_with_ctx_throws_IllegalStateException_if_state_is_null() { // given RequestBuilderWrapper rbwMock = mock(RequestBuilderWrapper.class); AsyncResponseHandler responseHandlerMock = mock(AsyncResponseHandler.class); CompletableFuture cfMock = mock(CompletableFuture.class); doReturn(cfMock).when(helperSpy).executeAsyncHttpRequest( any(RequestBuilderWrapper.class), any(AsyncResponseHandler.class), any(Deque.class), any(Map.class) ); doReturn(null).when(stateAttributeMock).get(); // when Throwable ex = catchThrowable(() -> helperSpy.executeAsyncHttpRequest(rbwMock, responseHandlerMock, ctxMock)); // then assertThat(ex).isInstanceOf(IllegalStateException.class); }
@Test public void getCircuitBreaker_returns_empty_if_disableCircuitBreaker_is_true() { // given RequestBuilderWrapper rbw = new RequestBuilderWrapper( "foo", "bar", mock(AsyncHttpClient.BoundRequestBuilder.class), Optional.of(mock(CircuitBreaker.class)), true); // when Optional<CircuitBreaker<Response>> result = helperSpy.getCircuitBreaker(rbw); // then assertThat(result).isEmpty(); }
/** * Call this before one of the {@code executeAsyncHttpRequest(...)} methods in order to get a request builder you * can populate with query params, headers, body, etc. If you want to specify a custom circuit breaker (or disable * circuit breaking entirely) for this call then use {@link #getRequestBuilder(String, HttpMethod, Optional, * boolean)} instead. This method tells the HTTP client to use a default circuit breaker based on the host being * called. */ public RequestBuilderWrapper getRequestBuilder(String url, HttpMethod method) { return getRequestBuilder(url, method, Optional.empty(), false); }
@Test public void default_constructor_creates_instance_with_default_values() { // when AsyncHttpClientHelper instance = new AsyncHttpClientHelper(); // then assertThat(instance.performSubSpanAroundDownstreamCalls).isTrue(); verifyDefaultUnderlyingClientConfig(instance); }
) { AsyncHttpClientHelper asyncClient = new AsyncHttpClientHelper(surroundWithSubspan); RequestBuilderWrapper rbw = asyncClient.getRequestBuilder(fullUrl, HttpMethod.GET); rbw.requestBuilder.setHeader(TestEndpoint.EXPECTED_HEADER_KEY, TestEndpoint.EXPECTED_HEADER_VAL); rbw.requestBuilder.setBody(TestEndpoint.EXPECTED_REQUEST_PAYLOAD); () -> asyncClient.executeAsyncHttpRequest( rbw, response -> response, finalDistributedTraceStackForCall, finalMdcContextForCall ).join()
doReturn(Optional.of(circuitBreakerMock)).when(helperSpy).getCircuitBreaker(any(RequestBuilderWrapper.class)); ManualModeTask<Response> cbManualTaskMock = mock(ManualModeTask.class); doReturn(cbManualTaskMock).when(circuitBreakerMock).newManualModeTask(); CompletableFuture resultFuture = helperSpy.executeAsyncHttpRequest( rbw, responseHandlerMock, initialSpanStack, initialMdc ); verify(helperSpy).getCircuitBreaker(rbw); verify(cbManualTaskMock).throwExceptionIfCircuitBreakerIsOpen();
/** * Executes the given request asynchronously, handling the response with the given responseHandlerFunction, and * returns a {@link CompletableFuture} that represents the result of executing the * responseHandlerFunction on the downstream response. Any error anywhere along the way will cause the returned * future to be completed with {@link CompletableFuture#completeExceptionally(Throwable)}. * <p/> * NOTE: This is a helper method for calling {@link #executeAsyncHttpRequest(RequestBuilderWrapper, * AsyncResponseHandler, java.util.Deque, java.util.Map)} that uses {@link * ChannelAttributes#getHttpProcessingStateForChannel(ChannelHandlerContext)} to extract the {@link * HttpProcessingState} from the given ctx argument, and then grabs {@link * HttpProcessingState#getDistributedTraceStack()} and {@link HttpProcessingState#getLoggerMdcContextMap()} to use * as the distributed trace stack and MDC info for the downstream call. */ public <O> CompletableFuture<O> executeAsyncHttpRequest(RequestBuilderWrapper requestBuilderWrapper, AsyncResponseHandler<O> responseHandlerFunction, ChannelHandlerContext ctx) { HttpProcessingState state = ChannelAttributes.getHttpProcessingStateForChannel(ctx).get(); if (state == null) throw new IllegalStateException("state cannot be null"); Map<String, String> mdcContextMap = state.getLoggerMdcContextMap(); Deque<Span> distributedTraceStack = state.getDistributedTraceStack(); requestBuilderWrapper.setCtx(ctx); return executeAsyncHttpRequest(requestBuilderWrapper, responseHandlerFunction, distributedTraceStack, mdcContextMap); }
@Test public void getCircuitBreaker_returns_custom_circuit_breaker_if_disableCircuitBreaker_is_false_and_customCircuitBreaker_exists() { // given Optional<CircuitBreaker<Response>> customCb = Optional.of(mock(CircuitBreaker.class)); RequestBuilderWrapper rbw = new RequestBuilderWrapper( "foo", "bar", mock(AsyncHttpClient.BoundRequestBuilder.class), customCb, false); // when Optional<CircuitBreaker<Response>> result = helperSpy.getCircuitBreaker(rbw); // then assertThat(result).isSameAs(customCb); }
@Test public void getRequestBuilder_delegates_to_helper_with_default_circuit_breaker_args() { // given String url = UUID.randomUUID().toString(); HttpMethod method = HttpMethod.valueOf(UUID.randomUUID().toString()); RequestBuilderWrapper rbwMock = mock(RequestBuilderWrapper.class); doReturn(rbwMock).when(helperSpy) .getRequestBuilder(anyString(), any(HttpMethod.class), any(Optional.class), anyBoolean()); // when RequestBuilderWrapper rbw = helperSpy.getRequestBuilder(url, method); // then verify(helperSpy).getRequestBuilder(url, method, Optional.empty(), false); assertThat(rbw).isSameAs(rbwMock); }
); helperSpy = spy(new AsyncHttpClientHelper().setSpanNamingAndTaggingStrategy(tagAndNamingStrategy)); channelMock = mock(Channel.class); ctxMock = mock(ChannelHandlerContext.class);
@DataProvider(value = { "true", "false" }, splitBy = "\\|") @Test public void constructor_with_subspan_opt_works_as_expected(boolean performSubspan) { // when AsyncHttpClientHelper instance = new AsyncHttpClientHelper(performSubspan); // then assertThat(instance.performSubSpanAroundDownstreamCalls).isEqualTo(performSubspan); verifyDefaultUnderlyingClientConfig(instance); }
return asyncHttpClientHelper.executeAsyncHttpRequest( asyncHttpClientHelper.getRequestBuilder(AMAZON_METADATA_DOCUMENT_URL, HttpMethod.GET), response -> { String region = null;
/** * Executes the given request asynchronously, handling the response with the given responseHandlerFunction, and * returns a {@link CompletableFuture} that represents the result of executing the * responseHandlerFunction on the downstream response. Any error anywhere along the way will cause the returned * future to be completed with {@link CompletableFuture#completeExceptionally(Throwable)}. * <p/> * NOTE: This is a helper method for calling {@link #executeAsyncHttpRequest(RequestBuilderWrapper, * AsyncResponseHandler, java.util.Deque, java.util.Map)} that uses the current thread's {@link * Tracer#getCurrentSpanStackCopy()} and {@link org.slf4j.MDC#getCopyOfContextMap()} for the for the distributed * trace stack and MDC info for the downstream call. */ public <O> CompletableFuture<O> executeAsyncHttpRequest(RequestBuilderWrapper requestBuilderWrapper, AsyncResponseHandler<O> responseHandlerFunction) { Map<String, String> mdcContextMap = MDC.getCopyOfContextMap(); Deque<Span> distributedTraceStack = Tracer.getInstance().getCurrentSpanStackCopy(); return executeAsyncHttpRequest(requestBuilderWrapper, responseHandlerFunction, distributedTraceStack, mdcContextMap); }
getCircuitBreaker(requestBuilderWrapper).map(CircuitBreaker::newManualModeTask);