From dc260f48693e3f86c0773a41a19f073c9f257855 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Fri, 21 Jan 2022 12:23:33 -0800 Subject: [PATCH 01/54] `MultiAddressClientBuilder` execution strategy control Motivation: The `MultiAddressClientBuilder` does not currently always respect overrides of execution strategy made by the single address initializer Modification: The computed execution strategy is a combination of the multiaddress client builder strategy, the api strategy and the client strategy. Result: More predictable and controllable behavior for `MultiAddressClientBuilder` --- .../client/api/DefaultClientGroup.java | 8 +- .../http/api}/FilterableClientToClient.java | 58 ++- .../http/api/HttpApiConversions.java | 32 ++ .../http/api/HttpClientBuilder.java | 2 +- .../servicetalk/http/api/HttpContextKeys.java | 6 + ...reamingHttpClientToBlockingHttpClient.java | 9 +- ...tpClientToBlockingStreamingHttpClient.java | 9 +- .../api/StreamingHttpClientToHttpClient.java | 15 +- ...reamingHttpConnectionToHttpConnection.java | 4 +- ...faultMultiAddressUrlHttpClientBuilder.java | 178 +++++++++- .../DefaultPartitionedHttpClientBuilder.java | 3 +- ...DefaultSingleAddressHttpClientBuilder.java | 3 +- .../netty/ClientEffectiveStrategyTest.java | 331 ++++++++++-------- 13 files changed, 470 insertions(+), 188 deletions(-) rename {servicetalk-http-netty/src/main/java/io/servicetalk/http/netty => servicetalk-http-api/src/main/java/io/servicetalk/http/api}/FilterableClientToClient.java (77%) diff --git a/servicetalk-client-api/src/main/java/io/servicetalk/client/api/DefaultClientGroup.java b/servicetalk-client-api/src/main/java/io/servicetalk/client/api/DefaultClientGroup.java index 430f9499d9..ffdbbdf366 100644 --- a/servicetalk-client-api/src/main/java/io/servicetalk/client/api/DefaultClientGroup.java +++ b/servicetalk-client-api/src/main/java/io/servicetalk/client/api/DefaultClientGroup.java @@ -123,17 +123,19 @@ public Client get(final Key key) { try { client = requireNonNull(clientFactory.apply(key), "Newly created client can not be null"); } catch (Throwable t) { - clientMap.remove(key); // PLACEHOLDER_CLIENT + final boolean removed = clientMap.remove(key, PLACEHOLDER_CLIENT); + assert removed : "Expected to remove PLACEHOLDER_CLIENT"; throw new IllegalArgumentException("Failed to create new client", t); } - clientMap.put(key, client); // Overwrite PLACEHOLDER_CLIENT + final boolean replaced = clientMap.replace(key, PLACEHOLDER_CLIENT, client); + assert replaced : "Expected to replace PLACEHOLDER_CLIENT"; toSource(client.onClose()).subscribe(new RemoveClientOnClose(key, client)); LOGGER.debug("A new client {} was created", client); if (closed) { // group has been closed after a new client was created - if (clientMap.remove(key) != null) { // not closed by closing thread + if (clientMap.remove(key, client)) { // not closed by closing thread client.closeAsync().subscribe(); LOGGER.debug("Recently created client {} was removed and closed, group {} closed", client, this); } diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/FilterableClientToClient.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/FilterableClientToClient.java similarity index 77% rename from servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/FilterableClientToClient.java rename to servicetalk-http-api/src/main/java/io/servicetalk/http/api/FilterableClientToClient.java index 7d9afd68fc..6d60997bea 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/FilterableClientToClient.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/FilterableClientToClient.java @@ -13,29 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.servicetalk.http.netty; +package io.servicetalk.http.api; import io.servicetalk.concurrent.api.Completable; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; -import io.servicetalk.http.api.BlockingHttpClient; -import io.servicetalk.http.api.BlockingStreamingHttpClient; -import io.servicetalk.http.api.FilterableStreamingHttpClient; -import io.servicetalk.http.api.HttpClient; -import io.servicetalk.http.api.HttpConnectionContext; -import io.servicetalk.http.api.HttpEventKey; -import io.servicetalk.http.api.HttpExecutionContext; -import io.servicetalk.http.api.HttpExecutionStrategy; -import io.servicetalk.http.api.HttpRequestMetaData; -import io.servicetalk.http.api.HttpRequestMethod; -import io.servicetalk.http.api.ReservedBlockingHttpConnection; -import io.servicetalk.http.api.ReservedBlockingStreamingHttpConnection; -import io.servicetalk.http.api.ReservedHttpConnection; -import io.servicetalk.http.api.ReservedStreamingHttpConnection; -import io.servicetalk.http.api.StreamingHttpClient; -import io.servicetalk.http.api.StreamingHttpRequest; -import io.servicetalk.http.api.StreamingHttpResponse; -import io.servicetalk.http.api.StreamingHttpResponseFactory; import static io.servicetalk.http.api.HttpApiConversions.toBlockingClient; import static io.servicetalk.http.api.HttpApiConversions.toBlockingStreamingClient; @@ -47,32 +29,46 @@ final class FilterableClientToClient implements StreamingHttpClient { private final FilterableStreamingHttpClient client; - private final HttpExecutionStrategy strategy; + private final DelegatingHttpExecutionContext executionContext; FilterableClientToClient(FilterableStreamingHttpClient filteredClient, HttpExecutionStrategy strategy) { client = filteredClient; - this.strategy = strategy; + this.executionContext = new DelegatingHttpExecutionContext(client.executionContext()) { + @Override + public HttpExecutionStrategy executionStrategy() { + return strategy; + } + }; } @Override public HttpClient asClient() { - return toClient(this, strategy); + return toClient(this, executionContext.executionStrategy()); } @Override public BlockingStreamingHttpClient asBlockingStreamingClient() { - return toBlockingStreamingClient(this, strategy); + return toBlockingStreamingClient(this, executionContext.executionStrategy()); } @Override public BlockingHttpClient asBlockingClient() { - return toBlockingClient(this, strategy); + return toBlockingClient(this, executionContext.executionStrategy()); } @Override public Single request(final StreamingHttpRequest request) { return Single.defer(() -> { - request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy); + request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, executionContext.executionStrategy()); + // HttpExecutionStrategy currentStrategy = request.context().getOrDefault( + // HTTP_EXECUTION_STRATEGY_KEY, defaultStrategy()); + // if (defaultStrategy() == currentStrategy) { + // HttpApiConversions.ClientAPI clientApi = request.context().getOrDefault( + // HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.ASYNC_STREAMING); + // request.context().put(HTTP_EXECUTION_STRATEGY_KEY, clientApi.defaultStrategy().merge(strategy)); + // } else { + // request.context().put(HTTP_EXECUTION_STRATEGY_KEY, strategy); + // } return client.request(request).shareContextOnSubscribe(); }); } @@ -80,21 +76,21 @@ public Single request(final StreamingHttpRequest request) @Override public Single reserveConnection(final HttpRequestMetaData metaData) { return Single.defer(() -> { - metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy); + metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, executionContext.executionStrategy()); return client.reserveConnection(metaData).map(rc -> new ReservedStreamingHttpConnection() { @Override public ReservedHttpConnection asConnection() { - return toReservedConnection(this, strategy); + return toReservedConnection(this, executionContext.executionStrategy()); } @Override public ReservedBlockingStreamingHttpConnection asBlockingStreamingConnection() { - return toReservedBlockingStreamingConnection(this, strategy); + return toReservedBlockingStreamingConnection(this, executionContext.executionStrategy()); } @Override public ReservedBlockingHttpConnection asBlockingConnection() { - return toReservedBlockingConnection(this, strategy); + return toReservedBlockingConnection(this, executionContext.executionStrategy()); } @Override @@ -109,7 +105,7 @@ public Single request(final StreamingHttpRequest request) // the method without strategy just as we do for the regular connection. return Single.defer(() -> { request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, - FilterableClientToClient.this.strategy); + FilterableClientToClient.this.executionContext.executionStrategy()); return rc.request(request).shareContextOnSubscribe(); }); } @@ -159,7 +155,7 @@ public StreamingHttpRequest newRequest(final HttpRequestMethod method, final Str @Override public HttpExecutionContext executionContext() { - return client.executionContext(); + return executionContext; } @Override diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java index 8d3b4119de..ffc9c8f8d7 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java @@ -20,12 +20,32 @@ import io.servicetalk.http.api.StreamingHttpClientToHttpClient.ReservedStreamingHttpConnectionToReservedHttpConnection; import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; +import static io.servicetalk.http.api.StreamingHttpConnectionToBlockingHttpConnection.DEFAULT_BLOCKING_CONNECTION_STRATEGY; +import static io.servicetalk.http.api.StreamingHttpConnectionToBlockingStreamingHttpConnection.DEFAULT_BLOCKING_STREAMING_CONNECTION_STRATEGY; +import static io.servicetalk.http.api.StreamingHttpConnectionToHttpConnection.DEFAULT_ASYNC_CONNECTION_STRATEGY; /** * Conversion routines to {@link StreamingHttpService}. */ public final class HttpApiConversions { + enum ClientAPI { + BLOCKING_AGGREGATED(DEFAULT_BLOCKING_CONNECTION_STRATEGY), + BLOCKING_STREAMING(DEFAULT_BLOCKING_STREAMING_CONNECTION_STRATEGY), + ASYNC_AGGREGATED(DEFAULT_ASYNC_CONNECTION_STRATEGY), + ASYNC_STREAMING(DefaultHttpExecutionStrategy.OFFLOADD_ALL_REQRESP_EVENT_STRATEGY); + + private final HttpExecutionStrategy defaultApiStrategy; + + ClientAPI(HttpExecutionStrategy defaultApiStrategy) { + this.defaultApiStrategy = defaultApiStrategy; + } + + public HttpExecutionStrategy defaultStrategy() { + return defaultApiStrategy; + } + } + private HttpApiConversions() { // no instances } @@ -219,6 +239,18 @@ public static HttpClient toClient(StreamingHttpClient original, HttpExecutionStr return new StreamingHttpClientToHttpClient(original, strategy); } + /** + * Convert from {@link FilterableStreamingHttpClient} to {@link StreamingHttpClient}. + * + * @param client orignal {@link FilterableClientToClient} to convert. + * @param strategy required strategy for the service when invoking the resulting {@link HttpClient} + * @return The conversion result. + */ + public static StreamingHttpClient toStreamingClient(final FilterableStreamingHttpClient client, + final HttpExecutionStrategy strategy) { + return new FilterableClientToClient(client, strategy); + } + /** * Convert from {@link StreamingHttpClient} to {@link BlockingHttpClient}. * diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpClientBuilder.java index 9f5b9a7693..e3fbace7db 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpClientBuilder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpClientBuilder.java @@ -27,7 +27,7 @@ * @param the type of address after resolution (resolved address) * @param the type of {@link ServiceDiscovererEvent} */ -interface HttpClientBuilder> { +public interface HttpClientBuilder> { /** * Sets the {@link Executor} for all connections created from this builder. diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpContextKeys.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpContextKeys.java index ff33d3282b..0470ed9837 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpContextKeys.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpContextKeys.java @@ -33,6 +33,12 @@ public final class HttpContextKeys { public static final Key HTTP_EXECUTION_STRATEGY_KEY = newKey("HTTP_EXECUTION_STRATEGY_KEY", HttpExecutionStrategy.class); + /** + * Tracks the original API mode used for client API conversions + */ + public static final Key HTTP_CLIENT_API_KEY = + newKey("HTTP_CLIENT_API_KEY", HttpApiConversions.ClientAPI.class); + private HttpContextKeys() { // No instances } diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingHttpClient.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingHttpClient.java index b670ed8c8a..772105b583 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingHttpClient.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingHttpClient.java @@ -18,6 +18,7 @@ import io.servicetalk.concurrent.BlockingIterable; import static io.servicetalk.http.api.BlockingUtils.blockingInvocation; +import static io.servicetalk.http.api.HttpContextKeys.HTTP_CLIENT_API_KEY; import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.RequestResponseFactories.toAggregated; @@ -45,7 +46,9 @@ public HttpExecutionStrategy executionStrategy() { @Override public ReservedBlockingHttpConnection reserveConnection(final HttpRequestMetaData metaData) throws Exception { - metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy); + if (null == metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy)) { + metaData.context().put(HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.BLOCKING_AGGREGATED); + } return blockingInvocation(client.reserveConnection(metaData) .map(c -> new ReservedStreamingHttpConnectionToReservedBlockingHttpConnection(c, this.strategy, reqRespFactory))); @@ -58,7 +61,9 @@ public StreamingHttpClient asStreamingClient() { @Override public HttpResponse request(final HttpRequest request) throws Exception { - request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy); + if (null == request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy)) { + request.context().put(HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.BLOCKING_AGGREGATED); + } return BlockingUtils.request(client, request); } diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingStreamingHttpClient.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingStreamingHttpClient.java index a55b971893..89cae24967 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingStreamingHttpClient.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingStreamingHttpClient.java @@ -18,6 +18,7 @@ import io.servicetalk.concurrent.BlockingIterable; import static io.servicetalk.http.api.BlockingUtils.blockingInvocation; +import static io.servicetalk.http.api.HttpContextKeys.HTTP_CLIENT_API_KEY; import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.RequestResponseFactories.toBlockingStreaming; @@ -48,7 +49,9 @@ public ReservedBlockingStreamingHttpConnection reserveConnection(final HttpReque throws Exception { // It is assumed that users will always apply timeouts at the StreamingHttpService layer (e.g. via filter). // So we don't apply any explicit timeout here and just wait forever. - metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy); + if (null == metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy)) { + metaData.context().put(HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.BLOCKING_STREAMING); + } return blockingInvocation(client.reserveConnection(metaData) .map(c -> new ReservedStreamingHttpConnectionToBlockingStreaming(c, this.strategy, reqRespFactory))); } @@ -60,7 +63,9 @@ public StreamingHttpClient asStreamingClient() { @Override public BlockingStreamingHttpResponse request(final BlockingStreamingHttpRequest request) throws Exception { - request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy); + if (null == request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy)) { + request.context().put(HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.BLOCKING_STREAMING); + } return blockingInvocation(client.request(request.toStreamingRequest())).toBlockingStreamingResponse(); } diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToHttpClient.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToHttpClient.java index cb32c3fcd8..e0eee3b8d1 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToHttpClient.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToHttpClient.java @@ -19,10 +19,11 @@ import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; +import static io.servicetalk.http.api.HttpContextKeys.HTTP_CLIENT_API_KEY; import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.RequestResponseFactories.toAggregated; -import static io.servicetalk.http.api.StreamingHttpConnectionToHttpConnection.DEFAULT_CONNECTION_STRATEGY; +import static io.servicetalk.http.api.StreamingHttpConnectionToHttpConnection.DEFAULT_ASYNC_CONNECTION_STRATEGY; import static java.util.Objects.requireNonNull; final class StreamingHttpClientToHttpClient implements HttpClient { @@ -32,7 +33,7 @@ final class StreamingHttpClientToHttpClient implements HttpClient { private final HttpRequestResponseFactory reqRespFactory; StreamingHttpClientToHttpClient(final StreamingHttpClient client, final HttpExecutionStrategy strategy) { - this.strategy = defaultStrategy() == strategy ? DEFAULT_CONNECTION_STRATEGY : strategy; + this.strategy = defaultStrategy() == strategy ? DEFAULT_ASYNC_CONNECTION_STRATEGY : strategy; this.client = client; context = new DelegatingHttpExecutionContext(client.executionContext()) { @Override @@ -46,7 +47,9 @@ public HttpExecutionStrategy executionStrategy() { @Override public Single request(final HttpRequest request) { return Single.defer(() -> { - request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy); + if (null == request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy)) { + request.context().put(HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.ASYNC_AGGREGATED); + } return client.request(request.toStreamingRequest()) .flatMap(response -> response.toResponse().shareContextOnSubscribe()) .shareContextOnSubscribe(); @@ -56,7 +59,9 @@ public Single request(final HttpRequest request) { @Override public Single reserveConnection(final HttpRequestMetaData metaData) { return Single.defer(() -> { - metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy); + if (null == metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy)) { + metaData.context().put(HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.ASYNC_AGGREGATED); + } return client.reserveConnection(metaData) .map(c -> new ReservedStreamingHttpConnectionToReservedHttpConnection(c, this.strategy, reqRespFactory)) @@ -118,7 +123,7 @@ static final class ReservedStreamingHttpConnectionToReservedHttpConnection imple ReservedStreamingHttpConnectionToReservedHttpConnection(final ReservedStreamingHttpConnection connection, final HttpExecutionStrategy strategy) { - this(connection, defaultStrategy() == strategy ? DEFAULT_CONNECTION_STRATEGY : strategy, + this(connection, defaultStrategy() == strategy ? DEFAULT_ASYNC_CONNECTION_STRATEGY : strategy, toAggregated(connection)); } diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpConnectionToHttpConnection.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpConnectionToHttpConnection.java index f0b70a43ed..4c80a8d035 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpConnectionToHttpConnection.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpConnectionToHttpConnection.java @@ -28,7 +28,7 @@ final class StreamingHttpConnectionToHttpConnection implements HttpConnection { * For aggregation, we invoke the user callback (Single from client#request()) after the payload is completed, * hence we need to offload data. */ - static final HttpExecutionStrategy DEFAULT_CONNECTION_STRATEGY = OFFLOAD_RECEIVE_DATA_EVENT_STRATEGY; + static final HttpExecutionStrategy DEFAULT_ASYNC_CONNECTION_STRATEGY = OFFLOAD_RECEIVE_DATA_EVENT_STRATEGY; private final StreamingHttpConnection connection; private final HttpExecutionStrategy strategy; private final HttpConnectionContext context; @@ -37,7 +37,7 @@ final class StreamingHttpConnectionToHttpConnection implements HttpConnection { StreamingHttpConnectionToHttpConnection(final StreamingHttpConnection connection, final HttpExecutionStrategy strategy) { - this.strategy = defaultStrategy() == strategy ? DEFAULT_CONNECTION_STRATEGY : strategy; + this.strategy = defaultStrategy() == strategy ? DEFAULT_ASYNC_CONNECTION_STRATEGY : strategy; this.connection = connection; final HttpConnectionContext originalCtx = connection.connectionContext(); executionContext = new DelegatingHttpExecutionContext(connection.executionContext()) { diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index 2bfdd2cab9..074d08fee0 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -22,12 +22,19 @@ import io.servicetalk.concurrent.api.CompositeCloseable; import io.servicetalk.concurrent.api.Executor; import io.servicetalk.concurrent.api.ListenableAsyncCloseable; +import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; import io.servicetalk.concurrent.api.internal.SubscribableCompletable; +import io.servicetalk.context.api.ContextMap; +import io.servicetalk.http.api.BlockingHttpClient; +import io.servicetalk.http.api.BlockingStreamingHttpClient; import io.servicetalk.http.api.DefaultHttpHeadersFactory; import io.servicetalk.http.api.DefaultStreamingHttpRequestResponseFactory; import io.servicetalk.http.api.FilterableReservedStreamingHttpConnection; import io.servicetalk.http.api.FilterableStreamingHttpClient; +import io.servicetalk.http.api.HttpClient; +import io.servicetalk.http.api.HttpConnectionContext; +import io.servicetalk.http.api.HttpEventKey; import io.servicetalk.http.api.HttpExecutionContext; import io.servicetalk.http.api.HttpExecutionStrategy; import io.servicetalk.http.api.HttpHeadersFactory; @@ -35,6 +42,10 @@ import io.servicetalk.http.api.HttpRequestMethod; import io.servicetalk.http.api.MultiAddressHttpClientBuilder; import io.servicetalk.http.api.RedirectConfig; +import io.servicetalk.http.api.ReservedBlockingHttpConnection; +import io.servicetalk.http.api.ReservedBlockingStreamingHttpConnection; +import io.servicetalk.http.api.ReservedHttpConnection; +import io.servicetalk.http.api.ReservedStreamingHttpConnection; import io.servicetalk.http.api.SingleAddressHttpClientBuilder; import io.servicetalk.http.api.StreamingHttpClient; import io.servicetalk.http.api.StreamingHttpRequest; @@ -63,6 +74,16 @@ import static io.servicetalk.concurrent.api.AsyncCloseables.toListenableAsyncCloseable; import static io.servicetalk.concurrent.api.Single.defer; import static io.servicetalk.concurrent.internal.SubscriberUtils.deliverCompleteFromSource; +import static io.servicetalk.http.api.HttpApiConversions.toBlockingClient; +import static io.servicetalk.http.api.HttpApiConversions.toBlockingStreamingClient; +import static io.servicetalk.http.api.HttpApiConversions.toClient; +import static io.servicetalk.http.api.HttpApiConversions.toReservedBlockingConnection; +import static io.servicetalk.http.api.HttpApiConversions.toReservedBlockingStreamingConnection; +import static io.servicetalk.http.api.HttpApiConversions.toReservedConnection; +import static io.servicetalk.http.api.HttpApiConversions.toStreamingClient; +import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; +import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; +import static io.servicetalk.http.api.HttpExecutionStrategies.offloadAll; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static io.servicetalk.http.netty.DefaultSingleAddressHttpClientBuilder.setExecutionContext; import static java.util.Objects.requireNonNull; @@ -106,7 +127,8 @@ public StreamingHttpClient buildStreaming() { final CompositeCloseable closeables = newCompositeCloseable(); try { final HttpExecutionContext executionContext = executionContextBuilder.build(); - final ClientFactory clientFactory = new ClientFactory(builderFactory, executionContext, + final ClientFactory clientFactory = new ClientFactory(builderFactory, + executionContext, singleAddressInitializer); final CachingKeyFactory keyFactory = closeables.prepend(new CachingKeyFactory()); final HttpHeadersFactory headersFactory = this.headersFactory; @@ -121,7 +143,7 @@ public StreamingHttpClient buildStreaming() { new RedirectingHttpRequesterFilter(redirectConfig).create(urlClient); LOGGER.debug("Multi-address client created with base strategy {}", executionContext.executionStrategy()); - return new FilterableClientToClient(urlClient, executionContext.executionStrategy()); + return toStreamingClient(urlClient, executionContext.executionStrategy()); } catch (final Throwable t) { closeables.closeAsync().subscribe(); throw t; @@ -235,6 +257,7 @@ public StreamingHttpClient apply(final UrlKey urlKey) { final SingleAddressHttpClientBuilder builder = requireNonNull(builderFactory.apply(urlKey.hostAndPort)); + // XXX This makes executionContextSet() always true. setExecutionContext(builder, executionContext); if (HTTPS_SCHEME.equalsIgnoreCase(urlKey.scheme)) { builder.sslConfig(DEFAULT_CLIENT_SSL_CONFIG); @@ -244,7 +267,156 @@ public StreamingHttpClient apply(final UrlKey urlKey) { singleAddressInitializer.initialize(urlKey.scheme, urlKey.hostAndPort, builder); } - return builder.buildStreaming(); + StreamingHttpClient client = builder.buildStreaming(); + // if executionStrategySet then wrap client with wrapper that sets execution strategy on request context. + return new StreamingHttpClientExecutionStrategy(client); + } + } + + private static final class StreamingHttpClientExecutionStrategy implements StreamingHttpClient { + + final StreamingHttpClient original; + + StreamingHttpClientExecutionStrategy(final StreamingHttpClient original) { + this.original = original; + } + + @Override + public Completable closeAsync() { + return original.closeAsync(); + } + + @Override + public Completable onClose() { + return original.onClose(); + } + + @Override + public Single reserveConnection(final HttpRequestMetaData metaData) { + HttpExecutionStrategy ourStrategy = original.executionContext().executionStrategy(); + return Single.defer(() -> { + metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, ourStrategy); + return original.reserveConnection(metaData).map(rc -> new ReservedStreamingHttpConnection() { + @Override + public ReservedHttpConnection asConnection() { + return toReservedConnection(this, ourStrategy); + } + + @Override + public ReservedBlockingStreamingHttpConnection asBlockingStreamingConnection() { + return toReservedBlockingStreamingConnection(this, ourStrategy); + } + + @Override + public ReservedBlockingHttpConnection asBlockingConnection() { + return toReservedBlockingConnection(this, ourStrategy); + } + + @Override + public Completable releaseAsync() { + return rc.releaseAsync(); + } + + @Override + public Single request(final StreamingHttpRequest request) { + // Use the strategy from the client as the underlying ReservedStreamingHttpConnection may be user + // created and hence could have an incorrect default strategy. Doing this makes sure we never call + // the method without strategy just as we do for the regular connection. + return Single.defer(() -> { + updateStrategy(request.context(), ourStrategy); + return rc.request(request).shareContextOnSubscribe(); + }); + } + + @Override + public HttpConnectionContext connectionContext() { + return rc.connectionContext(); + } + + @Override + public Publisher transportEventStream(final HttpEventKey eventKey) { + return rc.transportEventStream(eventKey); + } + + @Override + public HttpExecutionContext executionContext() { + return rc.executionContext(); + } + + @Override + public StreamingHttpResponseFactory httpResponseFactory() { + return rc.httpResponseFactory(); + } + + @Override + public Completable onClose() { + return rc.onClose(); + } + + @Override + public Completable closeAsync() { + return rc.closeAsync(); + } + + @Override + public Completable closeAsyncGracefully() { + return rc.closeAsyncGracefully(); + } + + @Override + public StreamingHttpRequest newRequest(final HttpRequestMethod method, final String requestTarget) { + return rc.newRequest(method, requestTarget); + } + }).shareContextOnSubscribe(); + }); + } + + @Override + public HttpClient asClient() { + return toClient(this, original.executionContext().executionStrategy()); + } + + @Override + public BlockingStreamingHttpClient asBlockingStreamingClient() { + return toBlockingStreamingClient(this, original.executionContext().executionStrategy()); + } + + @Override + public BlockingHttpClient asBlockingClient() { + return toBlockingClient(this, original.executionContext().executionStrategy()); + } + + @Override + public StreamingHttpRequest newRequest(final HttpRequestMethod method, final String requestTarget) { + return original.newRequest(method, requestTarget); + } + + @Override + public Single request(final StreamingHttpRequest request) { + return defer(() -> { + updateStrategy(request.context(), original.executionContext().executionStrategy()); + return original.request(request); + }); + } + + private HttpExecutionStrategy updateStrategy(ContextMap contextMap, HttpExecutionStrategy ourStrategy) { + HttpExecutionStrategy multiAddressStrategy = + contextMap.getOrDefault(HTTP_EXECUTION_STRATEGY_KEY, defaultStrategy()); + HttpExecutionStrategy combinedStrategy = defaultStrategy() == ourStrategy ? + defaultStrategy() == multiAddressStrategy ? offloadAll() : multiAddressStrategy : + defaultStrategy() == multiAddressStrategy ? offloadAll() : ourStrategy.merge(multiAddressStrategy); + contextMap.put(HTTP_EXECUTION_STRATEGY_KEY, combinedStrategy); + return combinedStrategy; + } + + @Override + public HttpExecutionContext executionContext() { + return original.executionContext(); + } + + @Override + public StreamingHttpResponseFactory httpResponseFactory() { + return original.httpResponseFactory(); } } diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultPartitionedHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultPartitionedHttpClientBuilder.java index 0fba847dca..376d5b8eb6 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultPartitionedHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultPartitionedHttpClientBuilder.java @@ -63,6 +63,7 @@ import static io.servicetalk.concurrent.api.RetryStrategies.retryWithConstantBackoffDeltaJitter; import static io.servicetalk.concurrent.api.Single.defer; import static io.servicetalk.concurrent.api.Single.failed; +import static io.servicetalk.http.api.HttpApiConversions.toStreamingClient; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static io.servicetalk.http.netty.DefaultSingleAddressHttpClientBuilder.SD_RETRY_STRATEGY_INIT_DURATION; import static io.servicetalk.http.netty.DefaultSingleAddressHttpClientBuilder.SD_RETRY_STRATEGY_JITTER; @@ -132,7 +133,7 @@ public StreamingHttpClient buildStreaming() { executionContext, partitionMapFactory); LOGGER.debug("Partitioned client created with base strategy {}", executionContext.executionStrategy()); - return new FilterableClientToClient(partitionedClient, executionContext.executionStrategy()); + return toStreamingClient(partitionedClient, executionContext.executionStrategy()); } private static final class DefaultPartitionedStreamingHttpClientFilter implements diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java index ea1f4e2aca..76b3f78965 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java @@ -68,6 +68,7 @@ import static io.servicetalk.concurrent.api.AsyncCloseables.newCompositeCloseable; import static io.servicetalk.concurrent.api.Processors.newCompletableProcessor; import static io.servicetalk.concurrent.api.RetryStrategies.retryWithConstantBackoffDeltaJitter; +import static io.servicetalk.http.api.HttpApiConversions.toStreamingClient; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_2_0; import static io.servicetalk.http.netty.AlpnIds.HTTP_2; @@ -293,7 +294,7 @@ ctx.builder.connectionFilterFactory, new AlpnReqRespFactoryFunc( HttpExecutionStrategy computedStrategy = ctx.builder.strategyComputation.buildForClient(executionStrategy); LOGGER.debug("Client for {} created with base strategy {} → computed strategy {}", targetAddress(ctx), executionStrategy, computedStrategy); - return new FilterableClientToClient(currClientFilterFactory != null ? + return toStreamingClient(currClientFilterFactory != null ? currClientFilterFactory.create(lbClient, lb.eventStream(), ctx.sdStatus) : lbClient, computedStrategy); } catch (final Throwable t) { diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 37d7901d6a..2aab536941 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -29,10 +29,12 @@ import io.servicetalk.http.api.FilterableStreamingHttpConnection; import io.servicetalk.http.api.FilterableStreamingHttpLoadBalancedConnection; import io.servicetalk.http.api.HttpClient; +import io.servicetalk.http.api.HttpClientBuilder; import io.servicetalk.http.api.HttpExecutionStrategies; import io.servicetalk.http.api.HttpExecutionStrategy; import io.servicetalk.http.api.HttpLoadBalancerFactory; import io.servicetalk.http.api.HttpServerBuilder; +import io.servicetalk.http.api.MultiAddressHttpClientBuilder; import io.servicetalk.http.api.SingleAddressHttpClientBuilder; import io.servicetalk.http.api.StreamingHttpClient; import io.servicetalk.http.api.StreamingHttpClientFilter; @@ -83,11 +85,69 @@ import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.fail; -@Execution(ExecutionMode.CONCURRENT) +@Execution(ExecutionMode.SAME_THREAD) class ClientEffectiveStrategyTest { + private static final String SCHEME = "http"; + private static final String PATH = "/"; private static final String GREETING = "Hello"; + private static final HttpExecutionStrategy CUSTOM_OFFLOAD_ALL = new HttpExecutionStrategy() { + @Override + public boolean isMetadataReceiveOffloaded() { + return true; + } + + @Override + public boolean isDataReceiveOffloaded() { + return true; + } + + @Override + public boolean isSendOffloaded() { + return true; + } + + @Override + public boolean isEventOffloaded() { + return true; + } + + @Override + public HttpExecutionStrategy merge(final HttpExecutionStrategy other) { + return HttpExecutionStrategies.offloadAll(); + } + + @Override + public boolean isCloseOffloaded() { + return true; + } + }; + + /** + * Which builder API will be used and where will ExecutionStrategy be initialized. + */ + private enum BuilderType { + Single, + Multi_ExecutionStrategy_On_Builder, + Multi_ExecutionStrategy_In_Initializer, + Multi_ExecutionStrategy_Override_In_Initializer + } + + /** + * Which API flavor will be used. + */ + private enum ClientApi { + AsyncStreaming, + BlockingStreaming, + BlockingAggregate(), + AsyncAggregate + } + + enum ClientOffloadPoint { + RequestPayloadSubscription, + ResponseMeta, + ResponseData + } private static final HttpExecutionStrategy[] BUILDER_STRATEGIES = { null, // unspecified @@ -97,7 +157,6 @@ class ClientEffectiveStrategyTest { HttpExecutionStrategies.customStrategyBuilder().offloadSend().build(), offloadAll(), }; - private static final HttpExecutionStrategy[] FILTER_STRATEGIES = { null, // absent offloadNever(), // treated as "offloadNone" @@ -106,7 +165,6 @@ class ClientEffectiveStrategyTest { defaultStrategy(), // treated as "offloadAll" offloadAll(), }; - private static final HttpExecutionStrategy[] LB_STRATEGIES = { null, // absent offloadNever(), // treated as "offloadNone" @@ -115,7 +173,6 @@ class ClientEffectiveStrategyTest { defaultStrategy(), // treated as "offloadAll" offloadAll(), }; - private static final HttpExecutionStrategy[] CF_STRATEGIES = { null, // absent offloadNever(), // treated as "offloadNone" @@ -125,7 +182,7 @@ class ClientEffectiveStrategyTest { offloadAll(), }; - static final ServerContext context; + private static final ServerContext context; static { try { @@ -138,21 +195,22 @@ class ClientEffectiveStrategyTest { } } - @AfterAll - static void shutdown() throws Exception { - context.closeGracefully(); - } - @SuppressWarnings("unused") static Stream casesSupplier() { List arguments = new ArrayList<>(); - for (ClientType clientType : ClientType.values()) { - for (HttpExecutionStrategy builderStrategy : BUILDER_STRATEGIES) { - for (HttpExecutionStrategy filterStrategy : FILTER_STRATEGIES) { - for (HttpExecutionStrategy lbStrategy : LB_STRATEGIES) { - for (HttpExecutionStrategy cfStrategy : CF_STRATEGIES) { - arguments.add(Arguments.of(clientType, - builderStrategy, filterStrategy, lbStrategy, cfStrategy)); + for (BuilderType builderType : BuilderType.values()) { + // if (BuilderType.Multi_ExecutionStrategy_In_Initializer == builderType || + // BuilderType.Multi_ExecutionStrategy_Override_In_Initializer == builderType) { + // continue; + // } + for (ClientApi clientApi : ClientApi.values()) { + for (HttpExecutionStrategy builderStrategy : BUILDER_STRATEGIES) { + for (HttpExecutionStrategy filterStrategy : FILTER_STRATEGIES) { + for (HttpExecutionStrategy lbStrategy : LB_STRATEGIES) { + for (HttpExecutionStrategy cfStrategy : CF_STRATEGIES) { + arguments.add(Arguments.of(builderType, clientApi, + builderStrategy, filterStrategy, lbStrategy, cfStrategy)); + } } } } @@ -161,70 +219,104 @@ static Stream casesSupplier() { return arguments.stream(); } - @ParameterizedTest(name = "API={0} builder={1} filter={2} LB={3} CF={4}") + @AfterAll + static void shutdown() throws Exception { + context.closeGracefully(); + } + + @ParameterizedTest(name = "Type={0} API={1} builder={2} filter={3} LB={4} CF={5}") @MethodSource("casesSupplier") - void clientStrategy(ClientType clientType, - @Nullable final HttpExecutionStrategy builderStrategy, - @Nullable final HttpExecutionStrategy filterStrategy, - @Nullable final HttpExecutionStrategy lbStrategy, - @Nullable final HttpExecutionStrategy cfStrategy) throws Exception { + void clientStrategy(final BuilderType builderType, final ClientApi clientApi, + @Nullable final HttpExecutionStrategy builderStrategy, + @Nullable final HttpExecutionStrategy filterStrategy, + @Nullable final HttpExecutionStrategy lbStrategy, + @Nullable final HttpExecutionStrategy cfStrategy) throws Exception { HttpExecutionStrategy effectiveStrategy = computeClientExecutionStrategy( builderStrategy, filterStrategy, lbStrategy, cfStrategy); - SingleAddressHttpClientBuilder clientBuilder = - HttpClients.forSingleAddress(serverHostAndPort(context)); - - if (builderStrategy != null) { - clientBuilder.executionStrategy(builderStrategy); - } - ClientInvokingThreadRecorder invokingThreadsRecorder = - new ClientInvokingThreadRecorder(clientType, effectiveStrategy); - - clientBuilder.appendClientFilter(invokingThreadsRecorder); - if (null != filterStrategy) { - clientBuilder.appendClientFilter(new StreamingHttpClientFilterFactory() { - @Override - public StreamingHttpClientFilter create(final FilterableStreamingHttpClient client) { - return new StreamingHttpClientFilter(client) { - }; - } + new ClientInvokingThreadRecorder(clientApi, effectiveStrategy); + + MultiAddressHttpClientBuilder.SingleAddressInitializer initializer = + (scheme, address, clientBuilder) -> { + clientBuilder.appendClientFilter(invokingThreadsRecorder); + if (null != filterStrategy) { + clientBuilder.appendClientFilter(new StreamingHttpClientFilterFactory() { + @Override + public StreamingHttpClientFilter create(final FilterableStreamingHttpClient client) { + return new StreamingHttpClientFilter(client) { + }; + } + + @Override + public HttpExecutionStrategy requiredOffloads() { + return filterStrategy; + } + }); + } + if (null != lbStrategy) { + HttpLoadBalancerFactory lfFactory = + DefaultHttpLoadBalancerFactory.Builder.from( + new LoadBalancerFactoryImpl() { + @Override + public ExecutionStrategy requiredOffloads() { + return lbStrategy; + } + }).build(); + clientBuilder.loadBalancerFactory(lfFactory); + } + if (null != cfStrategy) { + clientBuilder.appendConnectionFilter(new StreamingHttpConnectionFilterFactory() { + @Override + public StreamingHttpConnectionFilter create( + final FilterableStreamingHttpConnection connection) { + return new StreamingHttpConnectionFilter(connection) { + }; + } + + @Override + public HttpExecutionStrategy requiredOffloads() { + return cfStrategy; + } + }); + } - @Override - public HttpExecutionStrategy requiredOffloads() { - return filterStrategy; - } - }); + if (builderType != BuilderType.Multi_ExecutionStrategy_On_Builder && null != builderStrategy) { + clientBuilder.executionStrategy(builderStrategy); + } + }; + String requestTarget; + HttpClientBuilder> clientBuilder; + switch (builderType) { + case Single: + requestTarget = PATH; + clientBuilder = HttpClients.forSingleAddress(serverHostAndPort(context)); + // apply initializer immediately + initializer.initialize(SCHEME, serverHostAndPort(context), + (SingleAddressHttpClientBuilder) clientBuilder); + break; + case Multi_ExecutionStrategy_On_Builder: + case Multi_ExecutionStrategy_In_Initializer: + case Multi_ExecutionStrategy_Override_In_Initializer: + requestTarget = SCHEME + "://" + serverHostAndPort(context) + PATH; + clientBuilder = HttpClients.forMultiAddressUrl().initializer(initializer); + break; + default: + throw new AssertionError("Unexpected clientType"); } - if (null != lbStrategy) { - HttpLoadBalancerFactory lfFactory = - DefaultHttpLoadBalancerFactory.Builder.from(new LoadBalancerFactoryImpl() { - @Override - public ExecutionStrategy requiredOffloads() { - return lbStrategy; - } - }).build(); - clientBuilder.loadBalancerFactory(lfFactory); + + if (BuilderType.Multi_ExecutionStrategy_On_Builder == builderType && null != builderStrategy) { + clientBuilder.executionStrategy(builderStrategy); } - if (null != cfStrategy) { - clientBuilder.appendConnectionFilter(new StreamingHttpConnectionFilterFactory() { - @Override - public StreamingHttpConnectionFilter create( - final FilterableStreamingHttpConnection connection) { - return new StreamingHttpConnectionFilter(connection) { - }; - } - @Override - public HttpExecutionStrategy requiredOffloads() { - return cfStrategy; - } - }); + if (BuilderType.Multi_ExecutionStrategy_Override_In_Initializer == builderType && null != builderStrategy) { + // This is expected to ALWAYS be overridden in initializer. + clientBuilder.executionStrategy(CUSTOM_OFFLOAD_ALL); } // Exercise the client try (StreamingHttpClient client = clientBuilder.buildStreaming()) { - String responseBody = getResponse(clientType, client); + String responseBody = getResponse(clientApi, client, requestTarget); assertThat(responseBody, is(GREETING)); invokingThreadsRecorder.verifyOffloads(); } @@ -247,7 +339,7 @@ private HttpExecutionStrategy computeClientExecutionStrategy(@Nullable final Htt @Nullable final HttpExecutionStrategy filter, @Nullable final HttpExecutionStrategy lb, @Nullable final HttpExecutionStrategy cf) { - HttpExecutionStrategy chain = mergeStrategies(cf, mergeStrategies(lb, filter)); + @Nullable HttpExecutionStrategy chain = mergeStrategies(cf, mergeStrategies(lb, filter)); HttpExecutionStrategy merged = null == chain || !chain.hasOffloads() ? null == builder ? defaultStrategy() : builder : @@ -264,36 +356,34 @@ private HttpExecutionStrategy computeClientExecutionStrategy(@Nullable final Htt return null == first ? second : null == second ? first : first.merge(second); } - private String getResponse(ClientType clientType, StreamingHttpClient client) throws Exception { - switch (clientType) { - case Blocking: + private String getResponse(ClientApi clientApi, StreamingHttpClient client, String requestTarget) throws Exception { + switch (clientApi) { + case BlockingAggregate: BlockingHttpClient blockingClient = client.asBlockingClient(); - return blockingClient.request(blockingClient.get("/")).payloadBody() + return blockingClient.request(blockingClient.get(requestTarget)).payloadBody() .toString(StandardCharsets.US_ASCII); case BlockingStreaming: BlockingStreamingHttpClient blockingStreamingClient = client.asBlockingStreamingClient(); Supplier supplier = client.executionContext().bufferAllocator()::newCompositeBuffer; return StreamSupport.stream( - blockingStreamingClient.request(blockingStreamingClient.get("/")) + blockingStreamingClient.request(blockingStreamingClient.get(requestTarget)) .payloadBody().spliterator(), false) .reduce((Buffer base, Buffer buffer) -> (base instanceof CompositeBuffer ? - ((CompositeBuffer) base) : supplier.get().addBuffer(base)).addBuffer(buffer)) + ((CompositeBuffer) base) : supplier.get().addBuffer(base)).addBuffer(buffer)) .map(buffer -> buffer.toString(StandardCharsets.US_ASCII)) .orElse(""); case AsyncStreaming: - return client.request(client.get("/")).flatMap(resp -> resp.payloadBody().collect(() -> + return client.request(client.get(requestTarget)).flatMap(resp -> resp.payloadBody().collect(() -> client.executionContext().bufferAllocator().newCompositeBuffer(), CompositeBuffer::addBuffer)) .toFuture().get() .toString(StandardCharsets.US_ASCII); - case Async: + case AsyncAggregate: HttpClient httpClient = client.asClient(); - return httpClient.request(httpClient.get("/")).toFuture().get().payloadBody() + return httpClient.request(httpClient.get(requestTarget)).toFuture().get().payloadBody() .toString(StandardCharsets.US_ASCII); default: - fail("Unexpected client type " + clientType); - /* NOTREACHED */ - return "failed"; + throw new AssertionError("Unexpected client api " + clientApi); } } @@ -303,19 +393,33 @@ private static final class ClientInvokingThreadRecorder implements StreamingHttp private final ConcurrentMap invokingThreads = new ConcurrentHashMap<>(); private final Queue errors = new LinkedBlockingQueue<>(); - ClientInvokingThreadRecorder(ClientType clientType, HttpExecutionStrategy effectiveStrategy) { - if (defaultStrategy() == effectiveStrategy) { - offloadPoints = Offloads.DEFAULT.expected(clientType); - } else { - offloadPoints = EnumSet.noneOf(ClientOffloadPoint.class); + ClientInvokingThreadRecorder(ClientApi clientApi, HttpExecutionStrategy streamingAsyncStrategy) { + switch (clientApi) { + case BlockingAggregate: + offloadPoints = EnumSet.noneOf(ClientOffloadPoint.class); + break; + case BlockingStreaming: + offloadPoints = EnumSet.of(RequestPayloadSubscription); + break; + case AsyncAggregate: + offloadPoints = EnumSet.of(ResponseData); + break; + case AsyncStreaming: + offloadPoints = EnumSet.allOf(ClientOffloadPoint.class); + break; + default: + throw new AssertionError("unexpected case " + clientApi); + } + + if (defaultStrategy() != streamingAsyncStrategy) { // adjust expected offloads for specific execution strategy - if (effectiveStrategy.isSendOffloaded()) { + if (streamingAsyncStrategy.isSendOffloaded()) { offloadPoints.add(RequestPayloadSubscription); } - if (effectiveStrategy.isMetadataReceiveOffloaded()) { + if (streamingAsyncStrategy.isMetadataReceiveOffloaded()) { offloadPoints.add(ResponseMeta); } - if (effectiveStrategy.isDataReceiveOffloaded()) { + if (streamingAsyncStrategy.isDataReceiveOffloaded()) { offloadPoints.add(ResponseData); } } @@ -386,51 +490,4 @@ public ExecutionStrategy requiredOffloads() { return ExecutionStrategy.offloadNone(); } } - - private enum Offloads { - NONE() { - @Override - EnumSet expected(ClientType clientType) { - return EnumSet.noneOf(ClientOffloadPoint.class); - } - }, - DEFAULT() { - @Override - EnumSet expected(ClientType clientType) { - switch (clientType) { - case Blocking: - return EnumSet.noneOf(ClientOffloadPoint.class); - case BlockingStreaming: - return EnumSet.of(RequestPayloadSubscription); - case Async: - return EnumSet.of(ResponseData); - case AsyncStreaming: - return EnumSet.allOf(ClientOffloadPoint.class); - default: - throw new IllegalStateException("unexpected case " + clientType); - } - } - }, - ALL() { - @Override - EnumSet expected(ClientType clientType) { - return EnumSet.allOf(ClientOffloadPoint.class); - } - }; - - abstract EnumSet expected(ClientType clientType); - } - - private enum ClientType { - AsyncStreaming, - BlockingStreaming, - Blocking, - Async - } - - enum ClientOffloadPoint { - RequestPayloadSubscription, - ResponseMeta, - ResponseData - } } From 20cec2a87524e8322d497df5717056960b548586 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Thu, 31 Mar 2022 18:07:53 -0700 Subject: [PATCH 02/54] fixes ClientEffectiveStrategyTest for basic Single and Multi builders --- .../api/DefaultHttpExecutionStrategy.java | 2 +- .../http/api/FilterableClientToClient.java | 27 +-- .../http/api/HttpApiConversions.java | 19 +- .../http/api/HttpClientBuilder.java | 10 +- .../api/MultiAddressHttpClientBuilder.java | 8 + .../api/SpecialHttpExecutionStrategy.java | 2 +- .../ClientStrategyInfluencerChainBuilder.java | 53 ++--- ...faultMultiAddressUrlHttpClientBuilder.java | 63 ++--- .../netty/ClientEffectiveStrategyTest.java | 216 ++++++++++++------ 9 files changed, 256 insertions(+), 144 deletions(-) diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpExecutionStrategy.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpExecutionStrategy.java index 70b62119fc..0668a38f92 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpExecutionStrategy.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpExecutionStrategy.java @@ -49,7 +49,7 @@ enum DefaultHttpExecutionStrategy implements HttpExecutionStrategy { OFFLOAD_SEND_EVENT_STRATEGY(EnumSet.of(OFFLOAD_SEND, OFFLOAD_EVENT)), OFFLOAD_RECEIVE_META_AND_SEND_EVENT_STRATEGY(EnumSet.of(OFFLOAD_RECEIVE_META, OFFLOAD_SEND, OFFLOAD_EVENT)), OFFLOAD_RECEIVE_DATA_AND_SEND_EVENT_STRATEGY(EnumSet.of(OFFLOAD_RECEIVE_DATA, OFFLOAD_SEND, OFFLOAD_EVENT)), - OFFLOADD_ALL_REQRESP_EVENT_STRATEGY( + OFFLOAD_ALL_REQRESP_EVENT_STRATEGY( EnumSet.of(OFFLOAD_RECEIVE_META, OFFLOAD_RECEIVE_DATA, OFFLOAD_SEND, OFFLOAD_EVENT)), OFFLOAD_CLOSE_STRATEGY(EnumSet.of(OFFLOAD_CLOSE)), diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/FilterableClientToClient.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/FilterableClientToClient.java index 6d60997bea..575112a217 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/FilterableClientToClient.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/FilterableClientToClient.java @@ -25,6 +25,7 @@ import static io.servicetalk.http.api.HttpApiConversions.toReservedBlockingConnection; import static io.servicetalk.http.api.HttpApiConversions.toReservedBlockingStreamingConnection; import static io.servicetalk.http.api.HttpApiConversions.toReservedConnection; +import static io.servicetalk.http.api.HttpContextKeys.HTTP_CLIENT_API_KEY; import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; final class FilterableClientToClient implements StreamingHttpClient { @@ -59,16 +60,10 @@ public BlockingHttpClient asBlockingClient() { @Override public Single request(final StreamingHttpRequest request) { return Single.defer(() -> { - request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, executionContext.executionStrategy()); - // HttpExecutionStrategy currentStrategy = request.context().getOrDefault( - // HTTP_EXECUTION_STRATEGY_KEY, defaultStrategy()); - // if (defaultStrategy() == currentStrategy) { - // HttpApiConversions.ClientAPI clientApi = request.context().getOrDefault( - // HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.ASYNC_STREAMING); - // request.context().put(HTTP_EXECUTION_STRATEGY_KEY, clientApi.defaultStrategy().merge(strategy)); - // } else { - // request.context().put(HTTP_EXECUTION_STRATEGY_KEY, strategy); - // } + if (null == request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, + executionContext().executionStrategy())) { + request.context().put(HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.ASYNC_STREAMING); + } return client.request(request).shareContextOnSubscribe(); }); } @@ -76,7 +71,11 @@ public Single request(final StreamingHttpRequest request) @Override public Single reserveConnection(final HttpRequestMetaData metaData) { return Single.defer(() -> { - metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, executionContext.executionStrategy()); + if (null == metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, + executionContext().executionStrategy())) { + metaData.context().put(HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.ASYNC_STREAMING); + } + return client.reserveConnection(metaData).map(rc -> new ReservedStreamingHttpConnection() { @Override public ReservedHttpConnection asConnection() { @@ -104,8 +103,10 @@ public Single request(final StreamingHttpRequest request) // created and hence could have an incorrect default strategy. Doing this makes sure we never call // the method without strategy just as we do for the regular connection. return Single.defer(() -> { - request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, - FilterableClientToClient.this.executionContext.executionStrategy()); + if (null == request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, + FilterableClientToClient.this.executionContext().executionStrategy())) { + request.context().put(HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.ASYNC_STREAMING); + } return rc.request(request).shareContextOnSubscribe(); }); } diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java index ffc9c8f8d7..db3beea0fa 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java @@ -29,11 +29,14 @@ */ public final class HttpApiConversions { - enum ClientAPI { + /** + * The "flavors" of client API available + */ + public enum ClientAPI { BLOCKING_AGGREGATED(DEFAULT_BLOCKING_CONNECTION_STRATEGY), BLOCKING_STREAMING(DEFAULT_BLOCKING_STREAMING_CONNECTION_STRATEGY), ASYNC_AGGREGATED(DEFAULT_ASYNC_CONNECTION_STRATEGY), - ASYNC_STREAMING(DefaultHttpExecutionStrategy.OFFLOADD_ALL_REQRESP_EVENT_STRATEGY); + ASYNC_STREAMING(DefaultHttpExecutionStrategy.OFFLOAD_ALL_REQRESP_EVENT_STRATEGY); private final HttpExecutionStrategy defaultApiStrategy; @@ -59,6 +62,7 @@ private HttpApiConversions() { * @return The conversion result. * @deprecated Use overload with {@link HttpExecutionStrategy} rather than {@link HttpExecutionStrategyInfluencer} */ + // FIXME: 0.43 - remove deprecated method @Deprecated public static ReservedHttpConnection toReservedConnection(ReservedStreamingHttpConnection original, HttpExecutionStrategyInfluencer influencer) { @@ -86,6 +90,7 @@ public static ReservedHttpConnection toReservedConnection(ReservedStreamingHttpC * @return The conversion result. * @deprecated Use overload with {@link HttpExecutionStrategy} rather than {@link HttpExecutionStrategyInfluencer} */ + // FIXME: 0.43 - remove deprecated method @Deprecated public static ReservedBlockingHttpConnection toReservedBlockingConnection( ReservedStreamingHttpConnection original, HttpExecutionStrategyInfluencer influencer) { @@ -114,6 +119,7 @@ public static ReservedBlockingHttpConnection toReservedBlockingConnection( * @return The conversion result. * @deprecated Use overload with {@link HttpExecutionStrategy} rather than {@link HttpExecutionStrategyInfluencer} */ + // FIXME: 0.43 - remove deprecated method @Deprecated public static ReservedBlockingStreamingHttpConnection toReservedBlockingStreamingConnection( ReservedStreamingHttpConnection original, HttpExecutionStrategyInfluencer influencer) { @@ -142,6 +148,7 @@ public static ReservedBlockingStreamingHttpConnection toReservedBlockingStreamin * @return The conversion result. * @deprecated Use overload with {@link HttpExecutionStrategy} rather than {@link HttpExecutionStrategyInfluencer} */ + // FIXME: 0.43 - remove deprecated method @Deprecated public static HttpConnection toConnection(StreamingHttpConnection original, HttpExecutionStrategyInfluencer influencer) { @@ -168,6 +175,7 @@ public static HttpConnection toConnection(StreamingHttpConnection original, Http * @return The conversion result. * @deprecated Use overload with {@link HttpExecutionStrategy} rather than {@link HttpExecutionStrategyInfluencer} */ + // FIXME: 0.43 - remove deprecated method @Deprecated public static BlockingHttpConnection toBlockingConnection(StreamingHttpConnection original, HttpExecutionStrategyInfluencer influencer) { @@ -195,6 +203,7 @@ public static BlockingHttpConnection toBlockingConnection(StreamingHttpConnectio * @return The conversion result. * @deprecated Use overload with {@link HttpExecutionStrategy} rather than {@link HttpExecutionStrategyInfluencer} */ + // FIXME: 0.43 - remove deprecated method @Deprecated public static BlockingStreamingHttpConnection toBlockingStreamingConnection( StreamingHttpConnection original, HttpExecutionStrategyInfluencer influencer) { @@ -223,6 +232,7 @@ public static BlockingStreamingHttpConnection toBlockingStreamingConnection( * @return The conversion result. * @deprecated Use overload with {@link HttpExecutionStrategy} rather than {@link HttpExecutionStrategyInfluencer} */ + // FIXME: 0.43 - remove deprecated method @Deprecated public static HttpClient toClient(StreamingHttpClient original, HttpExecutionStrategyInfluencer influencer) { return toClient(original, influencer.requiredOffloads()); @@ -260,6 +270,7 @@ public static StreamingHttpClient toStreamingClient(final FilterableStreamingHtt * @return The conversion result. * @deprecated Use overload with {@link HttpExecutionStrategy} rather than {@link HttpExecutionStrategyInfluencer} */ + // FIXME: 0.43 - remove deprecated method @Deprecated public static BlockingHttpClient toBlockingClient(StreamingHttpClient original, HttpExecutionStrategyInfluencer influencer) { @@ -286,6 +297,7 @@ public static BlockingHttpClient toBlockingClient(StreamingHttpClient original, * @return The conversion result. * @deprecated Use overload with {@link HttpExecutionStrategy} rather than {@link HttpExecutionStrategyInfluencer} */ + // FIXME: 0.43 - remove deprecated method @Deprecated public static BlockingStreamingHttpClient toBlockingStreamingClient(StreamingHttpClient original, HttpExecutionStrategyInfluencer influencer) { @@ -313,6 +325,7 @@ public static BlockingStreamingHttpClient toBlockingStreamingClient(StreamingHtt * @return {@link ServiceAdapterHolder} containing the service adapted to the streaming programming model. * @deprecated Use overload with {@link HttpExecutionStrategy} rather than {@link HttpExecutionStrategyInfluencer} */ + // FIXME: 0.43 - remove deprecated method @Deprecated public static ServiceAdapterHolder toStreamingHttpService(HttpService service, HttpExecutionStrategyInfluencer influencer) { @@ -339,6 +352,7 @@ public static ServiceAdapterHolder toStreamingHttpService(HttpService service, H * @return {@link ServiceAdapterHolder} containing the service adapted to the streaming programming model. * @deprecated Use overload with {@link HttpExecutionStrategy} rather than {@link HttpExecutionStrategyInfluencer} */ + // FIXME: 0.43 - remove deprecated method @Deprecated public static ServiceAdapterHolder toStreamingHttpService(BlockingStreamingHttpService service, HttpExecutionStrategyInfluencer influencer) { @@ -366,6 +380,7 @@ public static ServiceAdapterHolder toStreamingHttpService(BlockingStreamingHttpS * @return {@link ServiceAdapterHolder} containing the service adapted to the streaming programming model. * @deprecated Use overload with {@link HttpExecutionStrategy} rather than {@link HttpExecutionStrategyInfluencer} */ + // FIXME: 0.43 - remove deprecated method @Deprecated public static ServiceAdapterHolder toStreamingHttpService(BlockingHttpService service, HttpExecutionStrategyInfluencer influencer) { diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpClientBuilder.java index e3fbace7db..0647b6c3a3 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpClientBuilder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpClientBuilder.java @@ -27,10 +27,10 @@ * @param the type of address after resolution (resolved address) * @param the type of {@link ServiceDiscovererEvent} */ -public interface HttpClientBuilder> { +interface HttpClientBuilder> { /** - * Sets the {@link Executor} for all connections created from this builder. + * Sets the {@link Executor} for all clients created from this builder. * * @param executor {@link IoExecutor} to use. * @return {@code this}. @@ -38,7 +38,7 @@ public interface HttpClientBuilder> HttpClientBuilder executor(Executor executor); /** - * Sets the {@link IoExecutor} for all connections created from this builder. + * Sets the {@link IoExecutor} for all clients created from this builder. * * @param ioExecutor {@link IoExecutor} to use. * @return {@code this}. @@ -46,7 +46,7 @@ public interface HttpClientBuilder> HttpClientBuilder ioExecutor(IoExecutor ioExecutor); /** - * Sets the {@link BufferAllocator} for all connections created from this builder. + * Sets the {@link BufferAllocator} for all clients created from this builder. * * @param allocator {@link BufferAllocator} to use. * @return {@code this}. @@ -54,7 +54,7 @@ public interface HttpClientBuilder> HttpClientBuilder bufferAllocator(BufferAllocator allocator); /** - * Sets the {@link HttpExecutionStrategy} for all connections created from this builder. + * Sets the {@link HttpExecutionStrategy} for all clients created from this builder. * * @param strategy {@link HttpExecutionStrategy} to use. * @return {@code this}. diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java index 89b49d7ebf..42eca4d9b3 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java @@ -69,6 +69,14 @@ default SingleAddressInitializer append(SingleAddressInitializer toA @Override MultiAddressHttpClientBuilder executor(Executor executor); + /** + * {@inheritDoc} + *

Provides the default strategy for the {@link SingleAddressHttpClientBuilder} used to construct client + * instances. The {@link #initializer(SingleAddressInitializer)} may override the default execution strategy.

+ * + * @param strategy {@inheritDoc} + * @return {@inheritDoc} + */ @Override MultiAddressHttpClientBuilder executionStrategy(HttpExecutionStrategy strategy); diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SpecialHttpExecutionStrategy.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SpecialHttpExecutionStrategy.java index bd0ae48c97..7e5da6adfe 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SpecialHttpExecutionStrategy.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SpecialHttpExecutionStrategy.java @@ -129,7 +129,7 @@ public HttpExecutionStrategy merge(final HttpExecutionStrategy other) { // assert false : "merging defaultStrategy() with other strategies is deprecated"; if (!mergeWarning) { mergeWarning = true; - LOGGER.warn("merging defaultStrategy() with other strategies is deprecated"); + LOGGER.warn("merging defaultStrategy() with other strategies is deprecated", new Throwable("culprit")); } return other; } diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ClientStrategyInfluencerChainBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ClientStrategyInfluencerChainBuilder.java index 783b302f47..ec9891a6af 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ClientStrategyInfluencerChainBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ClientStrategyInfluencerChainBuilder.java @@ -69,17 +69,17 @@ void add(HttpLoadBalancerFactory lb) { private void add(String purpose, ExecutionStrategyInfluencer influencer, HttpExecutionStrategy strategy) { if (offloadNever() == strategy) { - LOGGER.warn("{}#requiredOffloads() returns offloadNever(), which is unexpected. " + - "offloadNone() should be used instead. " + - "Making automatic adjustment, update the {} to avoid this warning.", - influencer, purpose); + // LOGGER.warn("{}#requiredOffloads() returns offloadNever(), which is unexpected. " + + // "offloadNone() should be used instead. " + + // "Making automatic adjustment, update the {} to avoid this warning.", + // influencer, purpose); strategy = offloadNone(); } if (defaultStrategy() == strategy) { - LOGGER.warn("{}#requiredOffloads() returns defaultStrategy(), which is unexpected. " + - "offloadAll() (safe default) or more appropriate custom strategy should be used instead." + - "Making automatic adjustment, update the {} to avoid this warning.", - influencer, purpose); + // LOGGER.warn("{}#requiredOffloads() returns defaultStrategy(), which is unexpected. " + + // "offloadAll() (safe default) or more appropriate custom strategy should be used instead." + + // "Making automatic adjustment, update the {} to avoid this warning.", + // influencer, purpose); strategy = offloadAll(); } clientChain = null != clientChain ? clientChain.merge(strategy) : strategy; @@ -88,17 +88,17 @@ private void add(String purpose, ExecutionStrategyInfluencer influencer, Http void add(ConnectionFactoryFilter connectionFactoryFilter) { ExecutionStrategy filterOffloads = connectionFactoryFilter.requiredOffloads(); if (offloadNever() == filterOffloads) { - LOGGER.warn("{}#requiredOffloads() returns offloadNever(), which is unexpected. " + - "offloadNone() should be used instead. " + - "Making automatic adjustment, update the filter.", - connectionFactoryFilter); + // LOGGER.warn("{}#requiredOffloads() returns offloadNever(), which is unexpected. " + + // "offloadNone() should be used instead. " + + // "Making automatic adjustment, update the filter.", + // connectionFactoryFilter); filterOffloads = offloadNone(); } if (defaultStrategy() == filterOffloads) { - LOGGER.warn("{}#requiredOffloads() returns defaultStrategy(), which is unexpected. " + - "offloadAll() (safe default) or more appropriate custom strategy should be used instead." + - "Making automatic adjustment, consider updating the filter.", - connectionFactoryFilter); + // LOGGER.warn("{}#requiredOffloads() returns defaultStrategy(), which is unexpected. " + + // "offloadAll() (safe default) or more appropriate custom strategy should be used instead." + + // "Making automatic adjustment, consider updating the filter.", + // connectionFactoryFilter); filterOffloads = offloadAll(); } connFactoryChain = null != connFactoryChain ? @@ -108,17 +108,17 @@ void add(ConnectionFactoryFilter connectio void add(StreamingHttpConnectionFilterFactory connectionFilter) { HttpExecutionStrategy filterOffloads = connectionFilter.requiredOffloads(); if (offloadNever() == filterOffloads) { - LOGGER.warn("{}#requiredOffloads() returns offloadNever(), which is unexpected. " + - "offloadNone() should be used instead. " + - "Making automatic adjustment, consider updating the filter.", - connectionFilter); + // LOGGER.warn("{}#requiredOffloads() returns offloadNever(), which is unexpected. " + + // "offloadNone() should be used instead. " + + // "Making automatic adjustment, consider updating the filter.", + // connectionFilter); filterOffloads = offloadNone(); } if (defaultStrategy() == filterOffloads) { - LOGGER.warn("{}#requiredOffloads() returns defaultStrategy(), which is unexpected. " + - "offloadAll() (safe default) or more appropriate custom strategy should be used instead." + - "Making automatic adjustment, consider updating the filter.", - connectionFilter); + // LOGGER.warn("{}#requiredOffloads() returns defaultStrategy(), which is unexpected. " + + // "offloadAll() (safe default) or more appropriate custom strategy should be used instead." + + // "Making automatic adjustment, consider updating the filter.", + // connectionFilter); filterOffloads = offloadAll(); } if (filterOffloads.hasOffloads()) { @@ -139,8 +139,9 @@ HttpExecutionStrategy buildForClient(HttpExecutionStrategy transportStrategy) { return (null == chainStrategy || !chainStrategy.hasOffloads()) ? transportStrategy : - defaultStrategy() == transportStrategy || !transportStrategy.hasOffloads() ? - chainStrategy : chainStrategy.merge(transportStrategy); + defaultStrategy() == transportStrategy ? + chainStrategy : transportStrategy.hasOffloads() ? + chainStrategy.merge(transportStrategy) : offloadNone(); } ExecutionStrategy buildForConnectionFactory() { diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index 074d08fee0..a3cec4368f 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -32,6 +32,7 @@ import io.servicetalk.http.api.DefaultStreamingHttpRequestResponseFactory; import io.servicetalk.http.api.FilterableReservedStreamingHttpConnection; import io.servicetalk.http.api.FilterableStreamingHttpClient; +import io.servicetalk.http.api.HttpApiConversions; import io.servicetalk.http.api.HttpClient; import io.servicetalk.http.api.HttpConnectionContext; import io.servicetalk.http.api.HttpEventKey; @@ -81,9 +82,11 @@ import static io.servicetalk.http.api.HttpApiConversions.toReservedBlockingStreamingConnection; import static io.servicetalk.http.api.HttpApiConversions.toReservedConnection; import static io.servicetalk.http.api.HttpApiConversions.toStreamingClient; +import static io.servicetalk.http.api.HttpContextKeys.HTTP_CLIENT_API_KEY; import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.HttpExecutionStrategies.offloadAll; +import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNone; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static io.servicetalk.http.netty.DefaultSingleAddressHttpClientBuilder.setExecutionContext; import static java.util.Objects.requireNonNull; @@ -257,7 +260,6 @@ public StreamingHttpClient apply(final UrlKey urlKey) { final SingleAddressHttpClientBuilder builder = requireNonNull(builderFactory.apply(urlKey.hostAndPort)); - // XXX This makes executionContextSet() always true. setExecutionContext(builder, executionContext); if (HTTPS_SCHEME.equalsIgnoreCase(urlKey.scheme)) { builder.sslConfig(DEFAULT_CLIENT_SSL_CONFIG); @@ -267,36 +269,39 @@ public StreamingHttpClient apply(final UrlKey urlKey) { singleAddressInitializer.initialize(urlKey.scheme, urlKey.hostAndPort, builder); } - StreamingHttpClient client = builder.buildStreaming(); - // if executionStrategySet then wrap client with wrapper that sets execution strategy on request context. - return new StreamingHttpClientExecutionStrategy(client); + StreamingHttpClient singleClient = builder.buildStreaming(); + + return new StreamingHttpClientExecutionStrategy(singleClient, executionContext.executionStrategy()); } } private static final class StreamingHttpClientExecutionStrategy implements StreamingHttpClient { - final StreamingHttpClient original; + final StreamingHttpClient singleClient; + final HttpExecutionStrategy multiClientStrategy; - StreamingHttpClientExecutionStrategy(final StreamingHttpClient original) { - this.original = original; + StreamingHttpClientExecutionStrategy(final StreamingHttpClient singleClient, + final HttpExecutionStrategy multiClientStrategy) { + this.singleClient = singleClient; + this.multiClientStrategy = multiClientStrategy; } @Override public Completable closeAsync() { - return original.closeAsync(); + return singleClient.closeAsync(); } @Override public Completable onClose() { - return original.onClose(); + return singleClient.onClose(); } @Override public Single reserveConnection(final HttpRequestMetaData metaData) { - HttpExecutionStrategy ourStrategy = original.executionContext().executionStrategy(); + HttpExecutionStrategy ourStrategy = singleClient.executionContext().executionStrategy(); return Single.defer(() -> { metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, ourStrategy); - return original.reserveConnection(metaData).map(rc -> new ReservedStreamingHttpConnection() { + return singleClient.reserveConnection(metaData).map(rc -> new ReservedStreamingHttpConnection() { @Override public ReservedHttpConnection asConnection() { return toReservedConnection(this, ourStrategy); @@ -373,50 +378,54 @@ public StreamingHttpRequest newRequest(final HttpRequestMethod method, final Str @Override public HttpClient asClient() { - return toClient(this, original.executionContext().executionStrategy()); + return toClient(this, singleClient.executionContext().executionStrategy()); } @Override public BlockingStreamingHttpClient asBlockingStreamingClient() { - return toBlockingStreamingClient(this, original.executionContext().executionStrategy()); + return toBlockingStreamingClient(this, singleClient.executionContext().executionStrategy()); } @Override public BlockingHttpClient asBlockingClient() { - return toBlockingClient(this, original.executionContext().executionStrategy()); + return toBlockingClient(this, singleClient.executionContext().executionStrategy()); } @Override public StreamingHttpRequest newRequest(final HttpRequestMethod method, final String requestTarget) { - return original.newRequest(method, requestTarget); + return singleClient.newRequest(method, requestTarget); } @Override public Single request(final StreamingHttpRequest request) { return defer(() -> { - updateStrategy(request.context(), original.executionContext().executionStrategy()); - return original.request(request); + updateStrategy(request.context(), singleClient.executionContext().executionStrategy()); + return singleClient.request(request); }); } - private HttpExecutionStrategy updateStrategy(ContextMap contextMap, HttpExecutionStrategy ourStrategy) { - HttpExecutionStrategy multiAddressStrategy = - contextMap.getOrDefault(HTTP_EXECUTION_STRATEGY_KEY, defaultStrategy()); - HttpExecutionStrategy combinedStrategy = defaultStrategy() == ourStrategy ? - defaultStrategy() == multiAddressStrategy ? offloadAll() : multiAddressStrategy : - defaultStrategy() == multiAddressStrategy ? offloadAll() : ourStrategy.merge(multiAddressStrategy); - contextMap.put(HTTP_EXECUTION_STRATEGY_KEY, combinedStrategy); - return combinedStrategy; + private HttpExecutionStrategy updateStrategy(final ContextMap contextMap, + final HttpExecutionStrategy singleClientStrategy) { + HttpExecutionStrategy requestStrategy = contextMap.get(HTTP_EXECUTION_STRATEGY_KEY); + HttpApiConversions.ClientAPI clientAPI = contextMap.get(HTTP_CLIENT_API_KEY); + HttpExecutionStrategy useStrategy = requestStrategy.hasOffloads() ? + defaultStrategy() == singleClientStrategy ? + defaultStrategy() == requestStrategy ? offloadAll() : requestStrategy : + singleClientStrategy.merge(defaultStrategy() == requestStrategy ? + clientAPI.defaultStrategy() : requestStrategy) : + offloadNone(); + contextMap.put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); + return useStrategy; } @Override public HttpExecutionContext executionContext() { - return original.executionContext(); + return singleClient.executionContext(); } @Override public StreamingHttpResponseFactory httpResponseFactory() { - return original.httpResponseFactory(); + return singleClient.httpResponseFactory(); } } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 2aab536941..6f7def4826 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -29,7 +29,6 @@ import io.servicetalk.http.api.FilterableStreamingHttpConnection; import io.servicetalk.http.api.FilterableStreamingHttpLoadBalancedConnection; import io.servicetalk.http.api.HttpClient; -import io.servicetalk.http.api.HttpClientBuilder; import io.servicetalk.http.api.HttpExecutionStrategies; import io.servicetalk.http.api.HttpExecutionStrategy; import io.servicetalk.http.api.HttpLoadBalancerFactory; @@ -64,6 +63,7 @@ import java.util.Collection; import java.util.EnumSet; import java.util.List; +import java.util.Objects; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -77,9 +77,9 @@ import static io.servicetalk.http.api.HttpExecutionStrategies.offloadAll; import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNever; import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNone; -import static io.servicetalk.http.netty.ClientEffectiveStrategyTest.ClientOffloadPoint.RequestPayloadSubscription; -import static io.servicetalk.http.netty.ClientEffectiveStrategyTest.ClientOffloadPoint.ResponseData; -import static io.servicetalk.http.netty.ClientEffectiveStrategyTest.ClientOffloadPoint.ResponseMeta; +import static io.servicetalk.http.netty.ClientEffectiveStrategyTest.ClientOffloadPoint.ReceiveData; +import static io.servicetalk.http.netty.ClientEffectiveStrategyTest.ClientOffloadPoint.ReceiveMeta; +import static io.servicetalk.http.netty.ClientEffectiveStrategyTest.ClientOffloadPoint.Send; import static io.servicetalk.test.resources.TestUtils.assertNoAsyncErrors; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; @@ -130,23 +130,26 @@ private enum BuilderType { Single, Multi_ExecutionStrategy_On_Builder, Multi_ExecutionStrategy_In_Initializer, - Multi_ExecutionStrategy_Override_In_Initializer + Multi_ExecutionStrategy_Initializer_Override } /** * Which API flavor will be used. */ private enum ClientApi { - AsyncStreaming, + BlockingAggregate, BlockingStreaming, - BlockingAggregate(), - AsyncAggregate + AsyncAggregate, + AsyncStreaming; } + /** + * Execution points at which the client will sample the executing thread + */ enum ClientOffloadPoint { - RequestPayloadSubscription, - ResponseMeta, - ResponseData + Send, + ReceiveMeta, + ReceiveData } private static final HttpExecutionStrategy[] BUILDER_STRATEGIES = { @@ -199,17 +202,21 @@ enum ClientOffloadPoint { static Stream casesSupplier() { List arguments = new ArrayList<>(); for (BuilderType builderType : BuilderType.values()) { - // if (BuilderType.Multi_ExecutionStrategy_In_Initializer == builderType || - // BuilderType.Multi_ExecutionStrategy_Override_In_Initializer == builderType) { - // continue; - // } + if (BuilderType.Multi_ExecutionStrategy_Initializer_Override == builderType) { + continue; + } for (ClientApi clientApi : ClientApi.values()) { for (HttpExecutionStrategy builderStrategy : BUILDER_STRATEGIES) { + if (BuilderType.Multi_ExecutionStrategy_Initializer_Override == builderType && + null == builderStrategy) { + // null builderStrategy won't actually override, so skip. + continue; + } for (HttpExecutionStrategy filterStrategy : FILTER_STRATEGIES) { for (HttpExecutionStrategy lbStrategy : LB_STRATEGIES) { for (HttpExecutionStrategy cfStrategy : CF_STRATEGIES) { - arguments.add(Arguments.of(builderType, clientApi, - builderStrategy, filterStrategy, lbStrategy, cfStrategy)); + arguments.add(Arguments.of(builderType, clientApi, builderStrategy, + filterStrategy, lbStrategy, cfStrategy)); } } } @@ -226,13 +233,13 @@ static void shutdown() throws Exception { @ParameterizedTest(name = "Type={0} API={1} builder={2} filter={3} LB={4} CF={5}") @MethodSource("casesSupplier") - void clientStrategy(final BuilderType builderType, final ClientApi clientApi, + void clientStrategy(final BuilderType builderType, ClientApi clientApi, @Nullable final HttpExecutionStrategy builderStrategy, @Nullable final HttpExecutionStrategy filterStrategy, @Nullable final HttpExecutionStrategy lbStrategy, @Nullable final HttpExecutionStrategy cfStrategy) throws Exception { HttpExecutionStrategy effectiveStrategy = computeClientExecutionStrategy( - builderStrategy, filterStrategy, lbStrategy, cfStrategy); + builderType, builderStrategy, filterStrategy, lbStrategy, cfStrategy, clientApi); ClientInvokingThreadRecorder invokingThreadsRecorder = new ClientInvokingThreadRecorder(clientApi, effectiveStrategy); @@ -286,36 +293,39 @@ public HttpExecutionStrategy requiredOffloads() { } }; String requestTarget; - HttpClientBuilder> clientBuilder; + Supplier clientBuilder; switch (builderType) { case Single: requestTarget = PATH; - clientBuilder = HttpClients.forSingleAddress(serverHostAndPort(context)); + SingleAddressHttpClientBuilder singleClientBuilder = + HttpClients.forSingleAddress(serverHostAndPort(context)); // apply initializer immediately - initializer.initialize(SCHEME, serverHostAndPort(context), - (SingleAddressHttpClientBuilder) clientBuilder); + initializer.initialize(SCHEME, serverHostAndPort(context), singleClientBuilder); + clientBuilder = singleClientBuilder::buildStreaming; break; case Multi_ExecutionStrategy_On_Builder: case Multi_ExecutionStrategy_In_Initializer: - case Multi_ExecutionStrategy_Override_In_Initializer: + case Multi_ExecutionStrategy_Initializer_Override: requestTarget = SCHEME + "://" + serverHostAndPort(context) + PATH; - clientBuilder = HttpClients.forMultiAddressUrl().initializer(initializer); + MultiAddressHttpClientBuilder multiClientBuilder = + HttpClients.forMultiAddressUrl().initializer(initializer); + if (BuilderType.Multi_ExecutionStrategy_On_Builder == builderType && null != builderStrategy) { + multiClientBuilder.executionStrategy(builderStrategy); + } + if (BuilderType.Multi_ExecutionStrategy_Initializer_Override == builderType && + null != builderStrategy) { + // This is expected to ALWAYS be overridden in initializer. + multiClientBuilder.executionStrategy(CUSTOM_OFFLOAD_ALL); + } + clientBuilder = multiClientBuilder::buildStreaming; break; default: throw new AssertionError("Unexpected clientType"); } - if (BuilderType.Multi_ExecutionStrategy_On_Builder == builderType && null != builderStrategy) { - clientBuilder.executionStrategy(builderStrategy); - } - - if (BuilderType.Multi_ExecutionStrategy_Override_In_Initializer == builderType && null != builderStrategy) { - // This is expected to ALWAYS be overridden in initializer. - clientBuilder.executionStrategy(CUSTOM_OFFLOAD_ALL); - } - + ClientApi[] clientApis = {ClientApi.BlockingStreaming}; // Exercise the client - try (StreamingHttpClient client = clientBuilder.buildStreaming()) { + try (StreamingHttpClient client = Objects.requireNonNull(clientBuilder.get())) { String responseBody = getResponse(clientApi, client, requestTarget); assertThat(responseBody, is(GREETING)); invokingThreadsRecorder.verifyOffloads(); @@ -335,18 +345,83 @@ public HttpExecutionStrategy requiredOffloads() { * connection filter will be added. * @return The strategy as computed */ - private HttpExecutionStrategy computeClientExecutionStrategy(@Nullable final HttpExecutionStrategy builder, + private HttpExecutionStrategy computeClientExecutionStrategy(BuilderType builderType, + @Nullable final HttpExecutionStrategy builder, @Nullable final HttpExecutionStrategy filter, @Nullable final HttpExecutionStrategy lb, - @Nullable final HttpExecutionStrategy cf) { + @Nullable final HttpExecutionStrategy cf, + ClientApi clientApi) { @Nullable HttpExecutionStrategy chain = mergeStrategies(cf, mergeStrategies(lb, filter)); - HttpExecutionStrategy merged = null == chain || !chain.hasOffloads() ? - null == builder ? defaultStrategy() : builder : - null == builder || defaultStrategy() == builder ? chain : - builder.hasOffloads() ? mergeStrategies(builder, chain) : chain; + HttpExecutionStrategy merged = null != chain && chain.hasOffloads() ? + null == builder || defaultStrategy() == builder ? + chain : builder.hasOffloads() ? mergeStrategies(builder, chain) : offloadNone() : + null == builder ? defaultStrategy() : builder; - return merged; + switch(builderType) { + case Single: + if (defaultStrategy() == merged) { + switch (clientApi) { + case BlockingAggregate: + return offloadNone(); + case BlockingStreaming: + return HttpExecutionStrategies.customStrategyBuilder().offloadSend().build(); + case AsyncAggregate: + return HttpExecutionStrategies.customStrategyBuilder().offloadReceiveData().build(); + case AsyncStreaming: + return HttpExecutionStrategies.customStrategyBuilder() + .offloadSend().offloadReceiveMetadata().offloadReceiveData().build(); + default: + throw new AssertionError("Unexpected client api: " + clientApi); + } + } else { + return merged; + } + case Multi_ExecutionStrategy_On_Builder: + if (null == builder || defaultStrategy() == builder) { + if (defaultStrategy() == merged) { + merged = offloadNone(); + } + switch (clientApi) { + case BlockingAggregate: + return merged; + case BlockingStreaming: + return HttpExecutionStrategies.customStrategyBuilder().offloadSend().build().merge(merged); + case AsyncAggregate: + return HttpExecutionStrategies.customStrategyBuilder() + .offloadReceiveData().build().merge(merged); + case AsyncStreaming: + return HttpExecutionStrategies.customStrategyBuilder() + .offloadSend().offloadReceiveMetadata().offloadReceiveData().build().merge(merged); + default: + throw new AssertionError("Unexpected client api: " + clientApi); + } + } + return merged; + case Multi_ExecutionStrategy_In_Initializer: + if (null == builder || defaultStrategy() == builder || !builder.hasOffloads()) { + if (defaultStrategy() == merged || (null != builder && !builder.hasOffloads())) { + merged = offloadNone(); + } + switch (clientApi) { + case BlockingAggregate: + return merged; + case BlockingStreaming: + return HttpExecutionStrategies.customStrategyBuilder().offloadSend().build().merge(merged); + case AsyncAggregate: + return HttpExecutionStrategies.customStrategyBuilder() + .offloadReceiveData().build().merge(merged); + case AsyncStreaming: + return HttpExecutionStrategies.customStrategyBuilder() + .offloadSend().offloadReceiveMetadata().offloadReceiveData().build().merge(merged); + default: + throw new AssertionError("Unexpected client api: " + clientApi); + } + } + return merged; + default: + throw new AssertionError("Unexpected builder type: " + builderType); + } } private @Nullable HttpExecutionStrategy mergeStrategies(@Nullable HttpExecutionStrategy first, @@ -390,37 +465,40 @@ private String getResponse(ClientApi clientApi, StreamingHttpClient client, Stri private static final class ClientInvokingThreadRecorder implements StreamingHttpClientFilterFactory { private final EnumSet offloadPoints; - private final ConcurrentMap invokingThreads = new ConcurrentHashMap<>(); + private final ConcurrentMap invokingThreads = new ConcurrentHashMap<>(); private final Queue errors = new LinkedBlockingQueue<>(); ClientInvokingThreadRecorder(ClientApi clientApi, HttpExecutionStrategy streamingAsyncStrategy) { - switch (clientApi) { - case BlockingAggregate: - offloadPoints = EnumSet.noneOf(ClientOffloadPoint.class); - break; - case BlockingStreaming: - offloadPoints = EnumSet.of(RequestPayloadSubscription); - break; - case AsyncAggregate: - offloadPoints = EnumSet.of(ResponseData); - break; - case AsyncStreaming: - offloadPoints = EnumSet.allOf(ClientOffloadPoint.class); - break; - default: - throw new AssertionError("unexpected case " + clientApi); - } - - if (defaultStrategy() != streamingAsyncStrategy) { + if (!streamingAsyncStrategy.hasOffloads()) { + offloadPoints = EnumSet.noneOf(ClientOffloadPoint.class); + } else if (defaultStrategy() != streamingAsyncStrategy) { + offloadPoints = EnumSet.noneOf(ClientOffloadPoint.class); // adjust expected offloads for specific execution strategy if (streamingAsyncStrategy.isSendOffloaded()) { - offloadPoints.add(RequestPayloadSubscription); + offloadPoints.add(Send); } if (streamingAsyncStrategy.isMetadataReceiveOffloaded()) { - offloadPoints.add(ResponseMeta); + offloadPoints.add(ReceiveMeta); } if (streamingAsyncStrategy.isDataReceiveOffloaded()) { - offloadPoints.add(ResponseData); + offloadPoints.add(ReceiveData); + } + } else { + switch (clientApi) { + case BlockingAggregate: + offloadPoints = EnumSet.noneOf(ClientOffloadPoint.class); + break; + case BlockingStreaming: + offloadPoints = EnumSet.of(Send); + break; + case AsyncAggregate: + offloadPoints = EnumSet.of(ReceiveData); + break; + case AsyncStreaming: + offloadPoints = EnumSet.allOf(ClientOffloadPoint.class); + break; + default: + throw new AssertionError("unexpected case " + clientApi); } } } @@ -439,16 +517,16 @@ public StreamingHttpClientFilter create(final FilterableStreamingHttpClient clie protected Single request(final StreamingHttpRequester delegate, final StreamingHttpRequest request) { return delegate.request(request.transformPayloadBody(payload -> - payload.beforeRequest(__ -> recordThread(RequestPayloadSubscription)))) - .beforeOnSuccess(__ -> recordThread(ResponseMeta)) + payload.beforeRequest(__ -> recordThread(Send)))) + .beforeOnSuccess(__ -> recordThread(ReceiveMeta)) .map(resp -> resp.transformPayloadBody(payload -> - payload.beforeOnNext(__ -> recordThread(ResponseData)))); + payload.beforeOnNext(__ -> recordThread(ReceiveData)))); } }; } void recordThread(final ClientOffloadPoint offloadPoint) { - invokingThreads.compute(offloadPoint, (ClientOffloadPoint offload, Throwable recorded) -> { + invokingThreads.compute(offloadPoint, (ClientOffloadPoint offload, String recorded) -> { Thread current = Thread.currentThread(); boolean ioThread = IoThreadFactory.IoThread.isIoThread(current); if (offloadPoints.contains(offloadPoint)) { @@ -460,7 +538,7 @@ void recordThread(final ClientOffloadPoint offloadPoint) { errors.add(new AssertionError("Expected ioThread at " + offloadPoint)); } } - return new Throwable("stack crawl : " + Thread.currentThread()); + return ioThread ? "eventLoop" : "offloaded"; }); } From 65ceda1a31993e37b6ac0f5180510563d681e717 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Thu, 31 Mar 2022 18:23:17 -0700 Subject: [PATCH 03/54] fixes ClientEffectiveStrategyTest for multi builder with initializer --- .../netty/DefaultMultiAddressUrlHttpClientBuilder.java | 7 +++++-- .../http/netty/ClientEffectiveStrategyTest.java | 3 --- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index a3cec4368f..ca33ca6445 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -86,6 +86,7 @@ import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.HttpExecutionStrategies.offloadAll; +import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNever; import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNone; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static io.servicetalk.http.netty.DefaultSingleAddressHttpClientBuilder.setExecutionContext; @@ -411,8 +412,10 @@ private HttpExecutionStrategy updateStrategy(final ContextMap contextMap, HttpExecutionStrategy useStrategy = requestStrategy.hasOffloads() ? defaultStrategy() == singleClientStrategy ? defaultStrategy() == requestStrategy ? offloadAll() : requestStrategy : - singleClientStrategy.merge(defaultStrategy() == requestStrategy ? - clientAPI.defaultStrategy() : requestStrategy) : + offloadNever() != singleClientStrategy ? + singleClientStrategy.merge(defaultStrategy() == requestStrategy ? + clientAPI.defaultStrategy() : requestStrategy) : + requestStrategy : offloadNone(); contextMap.put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); return useStrategy; diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 6f7def4826..ba397f1db6 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -399,7 +399,6 @@ private HttpExecutionStrategy computeClientExecutionStrategy(BuilderType builder } return merged; case Multi_ExecutionStrategy_In_Initializer: - if (null == builder || defaultStrategy() == builder || !builder.hasOffloads()) { if (defaultStrategy() == merged || (null != builder && !builder.hasOffloads())) { merged = offloadNone(); } @@ -417,8 +416,6 @@ private HttpExecutionStrategy computeClientExecutionStrategy(BuilderType builder default: throw new AssertionError("Unexpected client api: " + clientApi); } - } - return merged; default: throw new AssertionError("Unexpected builder type: " + builderType); } From 61814d12b7f88c4681b7ce6ed547d56908bdf6d9 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Fri, 1 Apr 2022 11:53:58 -0700 Subject: [PATCH 04/54] pre-final cleanup --- .../http/api/HttpApiConversions.java | 4 +- .../api/MultiAddressHttpClientBuilder.java | 7 ++- .../api/SingleAddressHttpClientBuilder.java | 12 +++++ .../api/SpecialHttpExecutionStrategy.java | 2 +- .../ClientStrategyInfluencerChainBuilder.java | 48 +++++++++---------- ...faultMultiAddressUrlHttpClientBuilder.java | 37 ++++++++------ .../netty/ClientEffectiveStrategyTest.java | 7 ++- 7 files changed, 69 insertions(+), 48 deletions(-) diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java index db3beea0fa..b24af59f91 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java @@ -30,7 +30,7 @@ public final class HttpApiConversions { /** - * The "flavors" of client API available + * The "flavors" of client API available. */ public enum ClientAPI { BLOCKING_AGGREGATED(DEFAULT_BLOCKING_CONNECTION_STRATEGY), @@ -252,7 +252,7 @@ public static HttpClient toClient(StreamingHttpClient original, HttpExecutionStr /** * Convert from {@link FilterableStreamingHttpClient} to {@link StreamingHttpClient}. * - * @param client orignal {@link FilterableClientToClient} to convert. + * @param client Original {@link FilterableClientToClient} to convert. * @param strategy required strategy for the service when invoking the resulting {@link HttpClient} * @return The conversion result. */ diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java index 42eca4d9b3..1907462a75 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java @@ -71,8 +71,11 @@ default SingleAddressInitializer append(SingleAddressInitializer toA /** * {@inheritDoc} - *

Provides the default strategy for the {@link SingleAddressHttpClientBuilder} used to construct client - * instances. The {@link #initializer(SingleAddressInitializer)} may override the default execution strategy.

+ *

Provides the base execution strategy for all clients created from this builder and the default strategy for + * the {@link SingleAddressHttpClientBuilder} used to construct client instances. The + * {@link #initializer(SingleAddressInitializer)} may be used for some customization of the execution strategy for a + * specific client. Unless {@link HttpExecutionStrategies#offloadNone()} is specified as the execution strategy on + * this builder then the single client computed execution strategy will be merged with this builder strategy. * * @param strategy {@inheritDoc} * @return {@inheritDoc} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java index 4590a682f2..c1d47664e6 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java @@ -171,6 +171,18 @@ SingleAddressHttpClientBuilder appendConnectionFilter( @Override SingleAddressHttpClientBuilder executor(Executor executor); + /** + * {@inheritDoc} + * + *

Unless {@link HttpExecutionStrategies#offloadNone()} is specified as the execution strategy on + * this builder, the actual execution strategy used will be influenced by the filters added via + * {@link #appendClientFilter(StreamingHttpClientFilterFactory)}, + * {@link #appendConnectionFilter(StreamingHttpConnectionFilterFactory)}, and + * {@link #appendConnectionFactoryFilter(ConnectionFactoryFilter)}

, etc. + * + * @param strategy {@inheritDoc} + * @return {@inheritDoc} + */ @Override SingleAddressHttpClientBuilder executionStrategy(HttpExecutionStrategy strategy); diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SpecialHttpExecutionStrategy.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SpecialHttpExecutionStrategy.java index 7e5da6adfe..bd0ae48c97 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SpecialHttpExecutionStrategy.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SpecialHttpExecutionStrategy.java @@ -129,7 +129,7 @@ public HttpExecutionStrategy merge(final HttpExecutionStrategy other) { // assert false : "merging defaultStrategy() with other strategies is deprecated"; if (!mergeWarning) { mergeWarning = true; - LOGGER.warn("merging defaultStrategy() with other strategies is deprecated", new Throwable("culprit")); + LOGGER.warn("merging defaultStrategy() with other strategies is deprecated"); } return other; } diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ClientStrategyInfluencerChainBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ClientStrategyInfluencerChainBuilder.java index ec9891a6af..5ce8cb031e 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ClientStrategyInfluencerChainBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ClientStrategyInfluencerChainBuilder.java @@ -69,17 +69,17 @@ void add(HttpLoadBalancerFactory lb) { private void add(String purpose, ExecutionStrategyInfluencer influencer, HttpExecutionStrategy strategy) { if (offloadNever() == strategy) { - // LOGGER.warn("{}#requiredOffloads() returns offloadNever(), which is unexpected. " + - // "offloadNone() should be used instead. " + - // "Making automatic adjustment, update the {} to avoid this warning.", - // influencer, purpose); + LOGGER.warn("{}#requiredOffloads() returns offloadNever(), which is unexpected. " + + "offloadNone() should be used instead. " + + "Making automatic adjustment, update the {} to avoid this warning.", + influencer, purpose); strategy = offloadNone(); } if (defaultStrategy() == strategy) { - // LOGGER.warn("{}#requiredOffloads() returns defaultStrategy(), which is unexpected. " + - // "offloadAll() (safe default) or more appropriate custom strategy should be used instead." + - // "Making automatic adjustment, update the {} to avoid this warning.", - // influencer, purpose); + LOGGER.warn("{}#requiredOffloads() returns defaultStrategy(), which is unexpected. " + + "offloadAll() (safe default) or more appropriate custom strategy should be used instead." + + "Making automatic adjustment, update the {} to avoid this warning.", + influencer, purpose); strategy = offloadAll(); } clientChain = null != clientChain ? clientChain.merge(strategy) : strategy; @@ -88,17 +88,17 @@ private void add(String purpose, ExecutionStrategyInfluencer influencer, Http void add(ConnectionFactoryFilter connectionFactoryFilter) { ExecutionStrategy filterOffloads = connectionFactoryFilter.requiredOffloads(); if (offloadNever() == filterOffloads) { - // LOGGER.warn("{}#requiredOffloads() returns offloadNever(), which is unexpected. " + - // "offloadNone() should be used instead. " + - // "Making automatic adjustment, update the filter.", - // connectionFactoryFilter); + LOGGER.warn("{}#requiredOffloads() returns offloadNever(), which is unexpected. " + + "offloadNone() should be used instead. " + + "Making automatic adjustment, update the filter.", + connectionFactoryFilter); filterOffloads = offloadNone(); } if (defaultStrategy() == filterOffloads) { - // LOGGER.warn("{}#requiredOffloads() returns defaultStrategy(), which is unexpected. " + - // "offloadAll() (safe default) or more appropriate custom strategy should be used instead." + - // "Making automatic adjustment, consider updating the filter.", - // connectionFactoryFilter); + LOGGER.warn("{}#requiredOffloads() returns defaultStrategy(), which is unexpected. " + + "offloadAll() (safe default) or more appropriate custom strategy should be used instead." + + "Making automatic adjustment, consider updating the filter.", + connectionFactoryFilter); filterOffloads = offloadAll(); } connFactoryChain = null != connFactoryChain ? @@ -108,17 +108,17 @@ void add(ConnectionFactoryFilter connectio void add(StreamingHttpConnectionFilterFactory connectionFilter) { HttpExecutionStrategy filterOffloads = connectionFilter.requiredOffloads(); if (offloadNever() == filterOffloads) { - // LOGGER.warn("{}#requiredOffloads() returns offloadNever(), which is unexpected. " + - // "offloadNone() should be used instead. " + - // "Making automatic adjustment, consider updating the filter.", - // connectionFilter); + LOGGER.warn("{}#requiredOffloads() returns offloadNever(), which is unexpected. " + + "offloadNone() should be used instead. " + + "Making automatic adjustment, consider updating the filter.", + connectionFilter); filterOffloads = offloadNone(); } if (defaultStrategy() == filterOffloads) { - // LOGGER.warn("{}#requiredOffloads() returns defaultStrategy(), which is unexpected. " + - // "offloadAll() (safe default) or more appropriate custom strategy should be used instead." + - // "Making automatic adjustment, consider updating the filter.", - // connectionFilter); + LOGGER.warn("{}#requiredOffloads() returns defaultStrategy(), which is unexpected. " + + "offloadAll() (safe default) or more appropriate custom strategy should be used instead." + + "Making automatic adjustment, consider updating the filter.", + connectionFilter); filterOffloads = offloadAll(); } if (filterOffloads.hasOffloads()) { diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index ca33ca6445..17fed07b3e 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -85,9 +85,6 @@ import static io.servicetalk.http.api.HttpContextKeys.HTTP_CLIENT_API_KEY; import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; -import static io.servicetalk.http.api.HttpExecutionStrategies.offloadAll; -import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNever; -import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNone; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static io.servicetalk.http.netty.DefaultSingleAddressHttpClientBuilder.setExecutionContext; import static java.util.Objects.requireNonNull; @@ -272,17 +269,17 @@ public StreamingHttpClient apply(final UrlKey urlKey) { StreamingHttpClient singleClient = builder.buildStreaming(); - return new StreamingHttpClientExecutionStrategy(singleClient, executionContext.executionStrategy()); + return new SingleAddressStreamingHttpClientWrapper(singleClient, executionContext.executionStrategy()); } } - private static final class StreamingHttpClientExecutionStrategy implements StreamingHttpClient { + private static final class SingleAddressStreamingHttpClientWrapper implements StreamingHttpClient { final StreamingHttpClient singleClient; final HttpExecutionStrategy multiClientStrategy; - StreamingHttpClientExecutionStrategy(final StreamingHttpClient singleClient, - final HttpExecutionStrategy multiClientStrategy) { + SingleAddressStreamingHttpClientWrapper(final StreamingHttpClient singleClient, + final HttpExecutionStrategy multiClientStrategy) { this.singleClient = singleClient; this.multiClientStrategy = multiClientStrategy; } @@ -329,7 +326,7 @@ public Single request(final StreamingHttpRequest request) // created and hence could have an incorrect default strategy. Doing this makes sure we never call // the method without strategy just as we do for the regular connection. return Single.defer(() -> { - updateStrategy(request.context(), ourStrategy); + applyMultiAddressStrategy(request.context(), ourStrategy); return rc.request(request).shareContextOnSubscribe(); }); } @@ -400,24 +397,34 @@ public StreamingHttpRequest newRequest(final HttpRequestMethod method, final Str @Override public Single request(final StreamingHttpRequest request) { return defer(() -> { - updateStrategy(request.context(), singleClient.executionContext().executionStrategy()); + applyMultiAddressStrategy(request.context(), singleClient.executionContext().executionStrategy()); return singleClient.request(request); }); } - private HttpExecutionStrategy updateStrategy(final ContextMap contextMap, - final HttpExecutionStrategy singleClientStrategy) { + private HttpExecutionStrategy applyMultiAddressStrategy(final ContextMap contextMap, + final HttpExecutionStrategy singleClientStrategy) { HttpExecutionStrategy requestStrategy = contextMap.get(HTTP_EXECUTION_STRATEGY_KEY); HttpApiConversions.ClientAPI clientAPI = contextMap.get(HTTP_CLIENT_API_KEY); HttpExecutionStrategy useStrategy = requestStrategy.hasOffloads() ? + // multi-client offloads something defaultStrategy() == singleClientStrategy ? - defaultStrategy() == requestStrategy ? offloadAll() : requestStrategy : - offloadNever() != singleClientStrategy ? + defaultStrategy() == requestStrategy ? + // async streaming client with default strategy + HttpApiConversions.ClientAPI.ASYNC_STREAMING.defaultStrategy() : + // single client tried to "reset" strategy to default, use multi-client strategy + requestStrategy : + // non-default strategy client + singleClientStrategy.hasOffloads() ? + // merge single client strategy with request strategy singleClientStrategy.merge(defaultStrategy() == requestStrategy ? clientAPI.defaultStrategy() : requestStrategy) : + // single client tried to force no offloads, use multi-client strategy requestStrategy : - offloadNone(); - contextMap.put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); + requestStrategy; + if (useStrategy != requestStrategy) { + contextMap.put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); + } return useStrategy; } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index ba397f1db6..da9febc5ed 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -140,7 +140,7 @@ private enum ClientApi { BlockingAggregate, BlockingStreaming, AsyncAggregate, - AsyncStreaming; + AsyncStreaming } /** @@ -323,7 +323,6 @@ public HttpExecutionStrategy requiredOffloads() { throw new AssertionError("Unexpected clientType"); } - ClientApi[] clientApis = {ClientApi.BlockingStreaming}; // Exercise the client try (StreamingHttpClient client = Objects.requireNonNull(clientBuilder.get())) { String responseBody = getResponse(clientApi, client, requestTarget); @@ -355,10 +354,10 @@ private HttpExecutionStrategy computeClientExecutionStrategy(BuilderType builder HttpExecutionStrategy merged = null != chain && chain.hasOffloads() ? null == builder || defaultStrategy() == builder ? - chain : builder.hasOffloads() ? mergeStrategies(builder, chain) : offloadNone() : + chain : builder.hasOffloads() ? mergeStrategies(builder, chain) : builder : null == builder ? defaultStrategy() : builder; - switch(builderType) { + switch (builderType) { case Single: if (defaultStrategy() == merged) { switch (clientApi) { From 09e2ea9ed3164949063c527f87c1be3385da48df Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Fri, 1 Apr 2022 13:34:35 -0700 Subject: [PATCH 05/54] documentation improvements --- .../api/MultiAddressHttpClientBuilder.java | 19 ++++++++++++ .../api/SingleAddressHttpClientBuilder.java | 20 ++++++++++-- .../ClientStrategyInfluencerChainBuilder.java | 10 +++--- .../netty/ClientEffectiveStrategyTest.java | 31 +++++++++++++------ 4 files changed, 64 insertions(+), 16 deletions(-) diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java index 1907462a75..b88a2e9dce 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java @@ -71,12 +71,31 @@ default SingleAddressInitializer append(SingleAddressInitializer toA /** * {@inheritDoc} + * *

Provides the base execution strategy for all clients created from this builder and the default strategy for * the {@link SingleAddressHttpClientBuilder} used to construct client instances. The * {@link #initializer(SingleAddressInitializer)} may be used for some customization of the execution strategy for a * specific client. Unless {@link HttpExecutionStrategies#offloadNone()} is specified as the execution strategy on * this builder then the single client computed execution strategy will be merged with this builder strategy. * + *

+ *
unspecified or {@link HttpExecutionStrategies#defaultStrategy()} + *
Effective execution strategy will be appropriate for the API (async/blocking streaming/aggregate) of the + * client used and will be be merged with the computed execution strategy of the single address client produced + * by {@link SingleAddressHttpClientBuilder}. + * + *
{@link HttpExecutionStrategies#offloadNone()} + * (or deprecated {@link HttpExecutionStrategies#offloadNever()}) + *
No offloading will be used regardless of the client API used or the computed execution strategy of the + * contained single address client. + * + *
A custom execution strategy ({@link HttpExecutionStrategies#customStrategyBuilder()}) or + * {@link HttpExecutionStrategies#offloadAll()} + *
Will be used, as specified, without regard to the API of the client used and will be merged with the + * computed execution strategy of the single address client produced by + * {@link SingleAddressHttpClientBuilder}. + *
+ * * @param strategy {@inheritDoc} * @return {@inheritDoc} */ diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java index c1d47664e6..8bce957cb5 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java @@ -175,11 +175,27 @@ SingleAddressHttpClientBuilder appendConnectionFilter( * {@inheritDoc} * *

Unless {@link HttpExecutionStrategies#offloadNone()} is specified as the execution strategy on - * this builder, the actual execution strategy used will be influenced by the filters added via - * {@link #appendClientFilter(StreamingHttpClientFilterFactory)}, + * this builder, the actual execution strategy used will be influenced by the execution strategy required by filters + * added via {@link #appendClientFilter(StreamingHttpClientFilterFactory)}, * {@link #appendConnectionFilter(StreamingHttpConnectionFilterFactory)}, and * {@link #appendConnectionFactoryFilter(ConnectionFactoryFilter)}

, etc. * + *
+ *
unspecified or {@link HttpExecutionStrategies#defaultStrategy()} + *
Effective execution strategy will be appropriate for the API (async/blocking streaming/aggregate) of the + * client used and will be {@linkplain HttpExecutionStrategyInfluencer influenced} by required strategies of + * any filters. + * + *
{@link HttpExecutionStrategies#offloadNone()} + * (or deprecated {@link HttpExecutionStrategies#offloadNever()}) + *
No offloading will be used regardless of the client API used or the influence of the filters. + * + *
A custom execution strategy ({@link HttpExecutionStrategies#customStrategyBuilder()}) or + * {@link HttpExecutionStrategies#offloadAll()} + *
Will be used, as specified, without regard to the API of the client used. Effective execution strategy + * will be specified value plus any additional offloads required by any filters. + *
+ * * @param strategy {@inheritDoc} * @return {@inheritDoc} */ diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ClientStrategyInfluencerChainBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ClientStrategyInfluencerChainBuilder.java index 5ce8cb031e..2a95e0d45a 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ClientStrategyInfluencerChainBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ClientStrategyInfluencerChainBuilder.java @@ -126,7 +126,7 @@ void add(StreamingHttpConnectionFilterFactory connectionFilter) { } } - HttpExecutionStrategy buildForClient(HttpExecutionStrategy transportStrategy) { + HttpExecutionStrategy buildForClient(HttpExecutionStrategy builderStrategy) { HttpExecutionStrategy chainStrategy = clientChain; if (null != connFilterChain) { chainStrategy = null != chainStrategy ? chainStrategy.merge(connFilterChain) : connFilterChain; @@ -138,10 +138,10 @@ HttpExecutionStrategy buildForClient(HttpExecutionStrategy transportStrategy) { } return (null == chainStrategy || !chainStrategy.hasOffloads()) ? - transportStrategy : - defaultStrategy() == transportStrategy ? - chainStrategy : transportStrategy.hasOffloads() ? - chainStrategy.merge(transportStrategy) : offloadNone(); + builderStrategy : + defaultStrategy() == builderStrategy ? + chainStrategy : builderStrategy.hasOffloads() ? + chainStrategy.merge(builderStrategy) : builderStrategy; } ExecutionStrategy buildForConnectionFactory() { diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index da9febc5ed..b10f9f48f2 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -91,15 +91,15 @@ class ClientEffectiveStrategyTest { private static final String SCHEME = "http"; private static final String PATH = "/"; private static final String GREETING = "Hello"; - private static final HttpExecutionStrategy CUSTOM_OFFLOAD_ALL = new HttpExecutionStrategy() { + private static final HttpExecutionStrategy CUSTOM_OFFLOAD_SEND = new HttpExecutionStrategy() { @Override public boolean isMetadataReceiveOffloaded() { - return true; + return false; } @Override public boolean isDataReceiveOffloaded() { - return true; + return false; } @Override @@ -109,17 +109,17 @@ public boolean isSendOffloaded() { @Override public boolean isEventOffloaded() { - return true; + return false; } @Override public HttpExecutionStrategy merge(final HttpExecutionStrategy other) { - return HttpExecutionStrategies.offloadAll(); + return HttpExecutionStrategies.customStrategyBuilder().offloadSend().build().merge(other); } @Override public boolean isCloseOffloaded() { - return true; + return false; } }; @@ -315,7 +315,7 @@ public HttpExecutionStrategy requiredOffloads() { if (BuilderType.Multi_ExecutionStrategy_Initializer_Override == builderType && null != builderStrategy) { // This is expected to ALWAYS be overridden in initializer. - multiClientBuilder.executionStrategy(CUSTOM_OFFLOAD_ALL); + multiClientBuilder.executionStrategy(CUSTOM_OFFLOAD_SEND); } clientBuilder = multiClientBuilder::buildStreaming; break; @@ -353,9 +353,22 @@ private HttpExecutionStrategy computeClientExecutionStrategy(BuilderType builder @Nullable HttpExecutionStrategy chain = mergeStrategies(cf, mergeStrategies(lb, filter)); HttpExecutionStrategy merged = null != chain && chain.hasOffloads() ? + // filter chain has offloads null == builder || defaultStrategy() == builder ? - chain : builder.hasOffloads() ? mergeStrategies(builder, chain) : builder : - null == builder ? defaultStrategy() : builder; + // builder has default strategy, use chain strategy + chain : + // builder specifies strategy. + builder.hasOffloads() ? + // combine with chain strategy + mergeStrategies(builder, chain) : + // builder wants no offloads + builder : + // filter chain has no offloads + null == builder ? + // unspecified, it is default + defaultStrategy() : + // use builder strategy as specified + builder; switch (builderType) { case Single: From db7f8922418b4859b1e7de043e83cfd133b0a6b6 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Thu, 7 Apr 2022 10:32:08 -0700 Subject: [PATCH 06/54] Handle single address builder overrides of offloadNone() --- ...faultMultiAddressUrlHttpClientBuilder.java | 11 ++- .../netty/ClientEffectiveStrategyTest.java | 83 +++++++------------ 2 files changed, 42 insertions(+), 52 deletions(-) diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index 17fed07b3e..3fc88a1d77 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -421,7 +421,16 @@ private HttpExecutionStrategy applyMultiAddressStrategy(final ContextMap context clientAPI.defaultStrategy() : requestStrategy) : // single client tried to force no offloads, use multi-client strategy requestStrategy : - requestStrategy; + // multi-client configured for no offloads + singleClientStrategy.hasOffloads() ? + // override in single client + defaultStrategy() == singleClientStrategy ? + // use API default + clientAPI.defaultStrategy() : + // use as specified + singleClientStrategy : + // single client does not override + requestStrategy; if (useStrategy != requestStrategy) { contextMap.put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index b10f9f48f2..d7c914b32d 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -86,42 +86,11 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -@Execution(ExecutionMode.SAME_THREAD) +@Execution(ExecutionMode.CONCURRENT) class ClientEffectiveStrategyTest { private static final String SCHEME = "http"; private static final String PATH = "/"; private static final String GREETING = "Hello"; - private static final HttpExecutionStrategy CUSTOM_OFFLOAD_SEND = new HttpExecutionStrategy() { - @Override - public boolean isMetadataReceiveOffloaded() { - return false; - } - - @Override - public boolean isDataReceiveOffloaded() { - return false; - } - - @Override - public boolean isSendOffloaded() { - return true; - } - - @Override - public boolean isEventOffloaded() { - return false; - } - - @Override - public HttpExecutionStrategy merge(final HttpExecutionStrategy other) { - return HttpExecutionStrategies.customStrategyBuilder().offloadSend().build().merge(other); - } - - @Override - public boolean isCloseOffloaded() { - return false; - } - }; /** * Which builder API will be used and where will ExecutionStrategy be initialized. @@ -143,15 +112,6 @@ private enum ClientApi { AsyncStreaming } - /** - * Execution points at which the client will sample the executing thread - */ - enum ClientOffloadPoint { - Send, - ReceiveMeta, - ReceiveData - } - private static final HttpExecutionStrategy[] BUILDER_STRATEGIES = { null, // unspecified offloadNever(), @@ -202,9 +162,6 @@ enum ClientOffloadPoint { static Stream casesSupplier() { List arguments = new ArrayList<>(); for (BuilderType builderType : BuilderType.values()) { - if (BuilderType.Multi_ExecutionStrategy_Initializer_Override == builderType) { - continue; - } for (ClientApi clientApi : ClientApi.values()) { for (HttpExecutionStrategy builderStrategy : BUILDER_STRATEGIES) { if (BuilderType.Multi_ExecutionStrategy_Initializer_Override == builderType && @@ -315,7 +272,7 @@ public HttpExecutionStrategy requiredOffloads() { if (BuilderType.Multi_ExecutionStrategy_Initializer_Override == builderType && null != builderStrategy) { // This is expected to ALWAYS be overridden in initializer. - multiClientBuilder.executionStrategy(CUSTOM_OFFLOAD_SEND); + multiClientBuilder.executionStrategy(offloadNone()); } clientBuilder = multiClientBuilder::buildStreaming; break; @@ -327,7 +284,7 @@ public HttpExecutionStrategy requiredOffloads() { try (StreamingHttpClient client = Objects.requireNonNull(clientBuilder.get())) { String responseBody = getResponse(clientApi, client, requestTarget); assertThat(responseBody, is(GREETING)); - invokingThreadsRecorder.verifyOffloads(); + invokingThreadsRecorder.verifyOffloads(clientApi); } } @@ -335,6 +292,7 @@ public HttpExecutionStrategy requiredOffloads() { * Computes the base execution strategy that the client will use based on the selected builder strategy, filter * strategy, load balancer strategy, connection factory filter strategy. * + * @param builderType Type of the builder being used for creating the client. * @param builder strategy specified for client builder or null to use builder default. * @param filter strategy specified for client stream filter to be added to client builder or null if no * filter will be added. @@ -342,14 +300,15 @@ public HttpExecutionStrategy requiredOffloads() { * load balancer will be added. * @param cf strategy specified for connection filter factory to be added to client builder or null if no * connection filter will be added. + * @param clientApi the client API which will be used for the request. * @return The strategy as computed */ - private HttpExecutionStrategy computeClientExecutionStrategy(BuilderType builderType, + private HttpExecutionStrategy computeClientExecutionStrategy(final BuilderType builderType, @Nullable final HttpExecutionStrategy builder, @Nullable final HttpExecutionStrategy filter, @Nullable final HttpExecutionStrategy lb, @Nullable final HttpExecutionStrategy cf, - ClientApi clientApi) { + final ClientApi clientApi) { @Nullable HttpExecutionStrategy chain = mergeStrategies(cf, mergeStrategies(lb, filter)); HttpExecutionStrategy merged = null != chain && chain.hasOffloads() ? @@ -371,6 +330,7 @@ private HttpExecutionStrategy computeClientExecutionStrategy(BuilderType builder builder; switch (builderType) { + case Multi_ExecutionStrategy_Initializer_Override: case Single: if (defaultStrategy() == merged) { switch (clientApi) { @@ -510,8 +470,21 @@ private static final class ClientInvokingThreadRecorder implements StreamingHttp throw new AssertionError("unexpected case " + clientApi); } } - } + if (defaultStrategy() != streamingAsyncStrategy) { + // adjust expected offloads for specific execution strategy + if (streamingAsyncStrategy.isSendOffloaded()) { + offloadPoints.add(Send); + } + if (streamingAsyncStrategy.isMetadataReceiveOffloaded()) { + offloadPoints.add(ReceiveMeta); + } + if (streamingAsyncStrategy.isDataReceiveOffloaded()) { + offloadPoints.add(ReceiveData); + } + } + System.out.println("API: " + clientApi + " base:" + streamingAsyncStrategy + " Expecting " + offloadPoints); + } @Override public HttpExecutionStrategy requiredOffloads() { // No influence since we do not block. @@ -551,8 +524,8 @@ void recordThread(final ClientOffloadPoint offloadPoint) { }); } - public void verifyOffloads() { - assertNoAsyncErrors(errors); + public void verifyOffloads(ClientApi clientApi) { + assertNoAsyncErrors("API=" + clientApi + " Async Errors! See suppressed" ,errors); assertThat("Unexpected offload points recorded. " + invokingThreads, invokingThreads.size(), Matchers.is(ClientOffloadPoint.values().length)); } @@ -577,4 +550,12 @@ public ExecutionStrategy requiredOffloads() { return ExecutionStrategy.offloadNone(); } } + /** + * Execution points at which the client will sample the executing thread + */ + enum ClientOffloadPoint { + Send, + ReceiveMeta, + ReceiveData + } } From 6392624f3fbd0fcc4e9eedbdc39e467d80dc994f Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Thu, 7 Apr 2022 10:44:05 -0700 Subject: [PATCH 07/54] checkstyle nits --- .../servicetalk/http/netty/ClientEffectiveStrategyTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index d7c914b32d..83cd793103 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -485,6 +485,7 @@ private static final class ClientInvokingThreadRecorder implements StreamingHttp } System.out.println("API: " + clientApi + " base:" + streamingAsyncStrategy + " Expecting " + offloadPoints); } + @Override public HttpExecutionStrategy requiredOffloads() { // No influence since we do not block. @@ -525,7 +526,7 @@ void recordThread(final ClientOffloadPoint offloadPoint) { } public void verifyOffloads(ClientApi clientApi) { - assertNoAsyncErrors("API=" + clientApi + " Async Errors! See suppressed" ,errors); + assertNoAsyncErrors("API=" + clientApi + " Async Errors! See suppressed", errors); assertThat("Unexpected offload points recorded. " + invokingThreads, invokingThreads.size(), Matchers.is(ClientOffloadPoint.values().length)); } @@ -550,6 +551,7 @@ public ExecutionStrategy requiredOffloads() { return ExecutionStrategy.offloadNone(); } } + /** * Execution points at which the client will sample the executing thread */ From 24d6216cf13fd3b207f1317d902a8c5a76fc2ff6 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Thu, 7 Apr 2022 16:58:23 -0700 Subject: [PATCH 08/54] combine client api test cases --- .../netty/ClientEffectiveStrategyTest.java | 144 +++++++++--------- 1 file changed, 75 insertions(+), 69 deletions(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 83cd793103..8a4a3dcde9 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -25,6 +25,8 @@ import io.servicetalk.concurrent.api.Single; import io.servicetalk.http.api.BlockingHttpClient; import io.servicetalk.http.api.BlockingStreamingHttpClient; +import io.servicetalk.http.api.BlockingStreamingHttpRequest; +import io.servicetalk.http.api.BlockingStreamingHttpResponse; import io.servicetalk.http.api.FilterableStreamingHttpClient; import io.servicetalk.http.api.FilterableStreamingHttpConnection; import io.servicetalk.http.api.FilterableStreamingHttpLoadBalancedConnection; @@ -32,6 +34,8 @@ import io.servicetalk.http.api.HttpExecutionStrategies; import io.servicetalk.http.api.HttpExecutionStrategy; import io.servicetalk.http.api.HttpLoadBalancerFactory; +import io.servicetalk.http.api.HttpRequest; +import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.api.HttpServerBuilder; import io.servicetalk.http.api.MultiAddressHttpClientBuilder; import io.servicetalk.http.api.SingleAddressHttpClientBuilder; @@ -102,16 +106,6 @@ private enum BuilderType { Multi_ExecutionStrategy_Initializer_Override } - /** - * Which API flavor will be used. - */ - private enum ClientApi { - BlockingAggregate, - BlockingStreaming, - AsyncAggregate, - AsyncStreaming - } - private static final HttpExecutionStrategy[] BUILDER_STRATEGIES = { null, // unspecified offloadNever(), @@ -162,19 +156,17 @@ private enum ClientApi { static Stream casesSupplier() { List arguments = new ArrayList<>(); for (BuilderType builderType : BuilderType.values()) { - for (ClientApi clientApi : ClientApi.values()) { - for (HttpExecutionStrategy builderStrategy : BUILDER_STRATEGIES) { - if (BuilderType.Multi_ExecutionStrategy_Initializer_Override == builderType && - null == builderStrategy) { - // null builderStrategy won't actually override, so skip. - continue; - } - for (HttpExecutionStrategy filterStrategy : FILTER_STRATEGIES) { - for (HttpExecutionStrategy lbStrategy : LB_STRATEGIES) { - for (HttpExecutionStrategy cfStrategy : CF_STRATEGIES) { - arguments.add(Arguments.of(builderType, clientApi, builderStrategy, - filterStrategy, lbStrategy, cfStrategy)); - } + for (HttpExecutionStrategy builderStrategy : BUILDER_STRATEGIES) { + if (BuilderType.Multi_ExecutionStrategy_Initializer_Override == builderType && + null == builderStrategy) { + // null builderStrategy won't actually override, so skip. + continue; + } + for (HttpExecutionStrategy filterStrategy : FILTER_STRATEGIES) { + for (HttpExecutionStrategy lbStrategy : LB_STRATEGIES) { + for (HttpExecutionStrategy cfStrategy : CF_STRATEGIES) { + arguments.add(Arguments.of(builderType, builderStrategy, + filterStrategy, lbStrategy, cfStrategy)); } } } @@ -188,18 +180,14 @@ static void shutdown() throws Exception { context.closeGracefully(); } - @ParameterizedTest(name = "Type={0} API={1} builder={2} filter={3} LB={4} CF={5}") + @ParameterizedTest(name = "Type={0} builder={1} filter={2} LB={3} CF={4}") @MethodSource("casesSupplier") - void clientStrategy(final BuilderType builderType, ClientApi clientApi, + void clientStrategy(final BuilderType builderType, @Nullable final HttpExecutionStrategy builderStrategy, @Nullable final HttpExecutionStrategy filterStrategy, @Nullable final HttpExecutionStrategy lbStrategy, @Nullable final HttpExecutionStrategy cfStrategy) throws Exception { - HttpExecutionStrategy effectiveStrategy = computeClientExecutionStrategy( - builderType, builderStrategy, filterStrategy, lbStrategy, cfStrategy, clientApi); - - ClientInvokingThreadRecorder invokingThreadsRecorder = - new ClientInvokingThreadRecorder(clientApi, effectiveStrategy); + ClientInvokingThreadRecorder invokingThreadsRecorder = new ClientInvokingThreadRecorder(); MultiAddressHttpClientBuilder.SingleAddressInitializer initializer = (scheme, address, clientBuilder) -> { @@ -281,10 +269,16 @@ public HttpExecutionStrategy requiredOffloads() { } // Exercise the client - try (StreamingHttpClient client = Objects.requireNonNull(clientBuilder.get())) { - String responseBody = getResponse(clientApi, client, requestTarget); - assertThat(responseBody, is(GREETING)); - invokingThreadsRecorder.verifyOffloads(clientApi); + for (final ClientApi clientApi : ClientApi.values()) { + try (StreamingHttpClient client = Objects.requireNonNull(clientBuilder.get())) { + HttpExecutionStrategy effectiveStrategy = computeClientExecutionStrategy( + builderType, builderStrategy, filterStrategy, lbStrategy, cfStrategy, clientApi); + + invokingThreadsRecorder.reset(clientApi, effectiveStrategy); + String responseBody = getResponse(clientApi, client, requestTarget); + assertThat("Unexpected response: " + responseBody, responseBody, is(GREETING)); + invokingThreadsRecorder.verifyOffloads(clientApi); + } } } @@ -303,7 +297,7 @@ public HttpExecutionStrategy requiredOffloads() { * @param clientApi the client API which will be used for the request. * @return The strategy as computed */ - private HttpExecutionStrategy computeClientExecutionStrategy(final BuilderType builderType, + private static HttpExecutionStrategy computeClientExecutionStrategy(final BuilderType builderType, @Nullable final HttpExecutionStrategy builder, @Nullable final HttpExecutionStrategy filter, @Nullable final HttpExecutionStrategy lb, @@ -393,7 +387,7 @@ private HttpExecutionStrategy computeClientExecutionStrategy(final BuilderType b } } - private @Nullable HttpExecutionStrategy mergeStrategies(@Nullable HttpExecutionStrategy first, + private static @Nullable HttpExecutionStrategy mergeStrategies(@Nullable HttpExecutionStrategy first, @Nullable HttpExecutionStrategy second) { first = offloadNever() != first ? defaultStrategy() != first ? first : offloadAll() : offloadNone(); second = offloadNever() != second ? defaultStrategy() != second ? second : offloadAll() : offloadNone(); @@ -402,30 +396,43 @@ private HttpExecutionStrategy computeClientExecutionStrategy(final BuilderType b private String getResponse(ClientApi clientApi, StreamingHttpClient client, String requestTarget) throws Exception { switch (clientApi) { - case BlockingAggregate: + case BlockingAggregate: { BlockingHttpClient blockingClient = client.asBlockingClient(); - return blockingClient.request(blockingClient.get(requestTarget)).payloadBody() - .toString(StandardCharsets.US_ASCII); - case BlockingStreaming: + HttpRequest request = blockingClient.get(requestTarget); + HttpResponse response = blockingClient.request(request); + return response.payloadBody().toString(StandardCharsets.US_ASCII); + } + + case BlockingStreaming: { BlockingStreamingHttpClient blockingStreamingClient = client.asBlockingStreamingClient(); - Supplier supplier = client.executionContext().bufferAllocator()::newCompositeBuffer; - return StreamSupport.stream( - blockingStreamingClient.request(blockingStreamingClient.get(requestTarget)) - .payloadBody().spliterator(), false) + BlockingStreamingHttpRequest request = blockingStreamingClient.get(requestTarget); + BlockingStreamingHttpResponse response = blockingStreamingClient.request(request); + Supplier supplier = + blockingStreamingClient.executionContext().bufferAllocator()::newCompositeBuffer; + return StreamSupport.stream(response.payloadBody().spliterator(), false) .reduce((Buffer base, Buffer buffer) -> (base instanceof CompositeBuffer ? ((CompositeBuffer) base) : supplier.get().addBuffer(base)).addBuffer(buffer)) .map(buffer -> buffer.toString(StandardCharsets.US_ASCII)) - .orElse(""); - case AsyncStreaming: - return client.request(client.get(requestTarget)).flatMap(resp -> resp.payloadBody().collect(() -> + .orElseThrow(() -> new AssertionError("No payload in response")); + } + + case AsyncAggregate: { + HttpClient httpClient = client.asClient(); + HttpRequest request = httpClient.get(requestTarget); + HttpResponse response = httpClient.request(request).toFuture().get(); + return response.payloadBody().toString(StandardCharsets.US_ASCII); + } + + case AsyncStreaming: { + StreamingHttpRequest request = client.get(requestTarget); + CompositeBuffer responsePayload = client.request(request) + .flatMap(resp -> resp.payloadBody().collect(() -> client.executionContext().bufferAllocator().newCompositeBuffer(), CompositeBuffer::addBuffer)) - .toFuture().get() - .toString(StandardCharsets.US_ASCII); - case AsyncAggregate: - HttpClient httpClient = client.asClient(); - return httpClient.request(httpClient.get(requestTarget)).toFuture().get().payloadBody() - .toString(StandardCharsets.US_ASCII); + .toFuture().get(); + return responsePayload.toString(StandardCharsets.US_ASCII); + } + default: throw new AssertionError("Unexpected client api " + clientApi); } @@ -433,11 +440,13 @@ private String getResponse(ClientApi clientApi, StreamingHttpClient client, Stri private static final class ClientInvokingThreadRecorder implements StreamingHttpClientFilterFactory { - private final EnumSet offloadPoints; + private EnumSet offloadPoints; private final ConcurrentMap invokingThreads = new ConcurrentHashMap<>(); private final Queue errors = new LinkedBlockingQueue<>(); - ClientInvokingThreadRecorder(ClientApi clientApi, HttpExecutionStrategy streamingAsyncStrategy) { + void reset(ClientApi clientApi, HttpExecutionStrategy streamingAsyncStrategy) { + invokingThreads.clear(); + errors.clear(); if (!streamingAsyncStrategy.hasOffloads()) { offloadPoints = EnumSet.noneOf(ClientOffloadPoint.class); } else if (defaultStrategy() != streamingAsyncStrategy) { @@ -453,6 +462,7 @@ private static final class ClientInvokingThreadRecorder implements StreamingHttp offloadPoints.add(ReceiveData); } } else { + // apply default offloads per client api switch (clientApi) { case BlockingAggregate: offloadPoints = EnumSet.noneOf(ClientOffloadPoint.class); @@ -470,20 +480,6 @@ private static final class ClientInvokingThreadRecorder implements StreamingHttp throw new AssertionError("unexpected case " + clientApi); } } - - if (defaultStrategy() != streamingAsyncStrategy) { - // adjust expected offloads for specific execution strategy - if (streamingAsyncStrategy.isSendOffloaded()) { - offloadPoints.add(Send); - } - if (streamingAsyncStrategy.isMetadataReceiveOffloaded()) { - offloadPoints.add(ReceiveMeta); - } - if (streamingAsyncStrategy.isDataReceiveOffloaded()) { - offloadPoints.add(ReceiveData); - } - } - System.out.println("API: " + clientApi + " base:" + streamingAsyncStrategy + " Expecting " + offloadPoints); } @Override @@ -552,6 +548,16 @@ public ExecutionStrategy requiredOffloads() { } } + /** + * Which API flavor will be used. + */ + private enum ClientApi { + BlockingAggregate, + BlockingStreaming, + AsyncAggregate, + AsyncStreaming + } + /** * Execution points at which the client will sample the executing thread */ From dd7a88dd6026810fd621d4b54d3048099ef3bd0a Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Wed, 13 Apr 2022 14:03:04 -0700 Subject: [PATCH 09/54] review feedback --- .../http/api/FilterableClientToClient.java | 17 ++----- .../http/api/HttpApiConversions.java | 23 --------- .../servicetalk/http/api/HttpContextKeys.java | 6 --- .../api/MultiAddressHttpClientBuilder.java | 8 ++- ...reamingHttpClientToBlockingHttpClient.java | 12 ++--- ...tpClientToBlockingStreamingHttpClient.java | 9 +--- .../api/StreamingHttpClientToHttpClient.java | 9 +--- ...faultMultiAddressUrlHttpClientBuilder.java | 49 ++++++++----------- .../netty/ClientEffectiveStrategyTest.java | 5 ++ 9 files changed, 39 insertions(+), 99 deletions(-) diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/FilterableClientToClient.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/FilterableClientToClient.java index 575112a217..faa20265e9 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/FilterableClientToClient.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/FilterableClientToClient.java @@ -25,7 +25,6 @@ import static io.servicetalk.http.api.HttpApiConversions.toReservedBlockingConnection; import static io.servicetalk.http.api.HttpApiConversions.toReservedBlockingStreamingConnection; import static io.servicetalk.http.api.HttpApiConversions.toReservedConnection; -import static io.servicetalk.http.api.HttpContextKeys.HTTP_CLIENT_API_KEY; import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; final class FilterableClientToClient implements StreamingHttpClient { @@ -60,10 +59,7 @@ public BlockingHttpClient asBlockingClient() { @Override public Single request(final StreamingHttpRequest request) { return Single.defer(() -> { - if (null == request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, - executionContext().executionStrategy())) { - request.context().put(HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.ASYNC_STREAMING); - } + request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, executionContext().executionStrategy()); return client.request(request).shareContextOnSubscribe(); }); } @@ -71,10 +67,7 @@ public Single request(final StreamingHttpRequest request) @Override public Single reserveConnection(final HttpRequestMetaData metaData) { return Single.defer(() -> { - if (null == metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, - executionContext().executionStrategy())) { - metaData.context().put(HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.ASYNC_STREAMING); - } + metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, executionContext().executionStrategy()); return client.reserveConnection(metaData).map(rc -> new ReservedStreamingHttpConnection() { @Override @@ -103,10 +96,8 @@ public Single request(final StreamingHttpRequest request) // created and hence could have an incorrect default strategy. Doing this makes sure we never call // the method without strategy just as we do for the regular connection. return Single.defer(() -> { - if (null == request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, - FilterableClientToClient.this.executionContext().executionStrategy())) { - request.context().put(HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.ASYNC_STREAMING); - } + request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, + FilterableClientToClient.this.executionContext().executionStrategy()); return rc.request(request).shareContextOnSubscribe(); }); } diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java index b24af59f91..1b52f344a0 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java @@ -20,35 +20,12 @@ import io.servicetalk.http.api.StreamingHttpClientToHttpClient.ReservedStreamingHttpConnectionToReservedHttpConnection; import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; -import static io.servicetalk.http.api.StreamingHttpConnectionToBlockingHttpConnection.DEFAULT_BLOCKING_CONNECTION_STRATEGY; -import static io.servicetalk.http.api.StreamingHttpConnectionToBlockingStreamingHttpConnection.DEFAULT_BLOCKING_STREAMING_CONNECTION_STRATEGY; -import static io.servicetalk.http.api.StreamingHttpConnectionToHttpConnection.DEFAULT_ASYNC_CONNECTION_STRATEGY; /** * Conversion routines to {@link StreamingHttpService}. */ public final class HttpApiConversions { - /** - * The "flavors" of client API available. - */ - public enum ClientAPI { - BLOCKING_AGGREGATED(DEFAULT_BLOCKING_CONNECTION_STRATEGY), - BLOCKING_STREAMING(DEFAULT_BLOCKING_STREAMING_CONNECTION_STRATEGY), - ASYNC_AGGREGATED(DEFAULT_ASYNC_CONNECTION_STRATEGY), - ASYNC_STREAMING(DefaultHttpExecutionStrategy.OFFLOAD_ALL_REQRESP_EVENT_STRATEGY); - - private final HttpExecutionStrategy defaultApiStrategy; - - ClientAPI(HttpExecutionStrategy defaultApiStrategy) { - this.defaultApiStrategy = defaultApiStrategy; - } - - public HttpExecutionStrategy defaultStrategy() { - return defaultApiStrategy; - } - } - private HttpApiConversions() { // no instances } diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpContextKeys.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpContextKeys.java index 0470ed9837..ff33d3282b 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpContextKeys.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpContextKeys.java @@ -33,12 +33,6 @@ public final class HttpContextKeys { public static final Key HTTP_EXECUTION_STRATEGY_KEY = newKey("HTTP_EXECUTION_STRATEGY_KEY", HttpExecutionStrategy.class); - /** - * Tracks the original API mode used for client API conversions - */ - public static final Key HTTP_CLIENT_API_KEY = - newKey("HTTP_CLIENT_API_KEY", HttpApiConversions.ClientAPI.class); - private HttpContextKeys() { // No instances } diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java index b88a2e9dce..95c820901b 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java @@ -84,12 +84,10 @@ default SingleAddressInitializer append(SingleAddressInitializer toA * client used and will be be merged with the computed execution strategy of the single address client produced * by {@link SingleAddressHttpClientBuilder}. * - *
{@link HttpExecutionStrategies#offloadNone()} - * (or deprecated {@link HttpExecutionStrategies#offloadNever()}) - *
No offloading will be used regardless of the client API used or the computed execution strategy of the - * contained single address client. * - *
A custom execution strategy ({@link HttpExecutionStrategies#customStrategyBuilder()}) or + *
{@link HttpExecutionStrategies#offloadNone()} + * (or deprecated {@link HttpExecutionStrategies#offloadNever()}), + * a custom execution strategy ({@link HttpExecutionStrategies#customStrategyBuilder()}), or * {@link HttpExecutionStrategies#offloadAll()} *
Will be used, as specified, without regard to the API of the client used and will be merged with the * computed execution strategy of the single address client produced by diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingHttpClient.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingHttpClient.java index 772105b583..b2f746b5e1 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingHttpClient.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingHttpClient.java @@ -18,7 +18,6 @@ import io.servicetalk.concurrent.BlockingIterable; import static io.servicetalk.http.api.BlockingUtils.blockingInvocation; -import static io.servicetalk.http.api.HttpContextKeys.HTTP_CLIENT_API_KEY; import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.RequestResponseFactories.toAggregated; @@ -32,8 +31,7 @@ final class StreamingHttpClientToBlockingHttpClient implements BlockingHttpClien private final HttpRequestResponseFactory reqRespFactory; StreamingHttpClientToBlockingHttpClient(final StreamingHttpClient client, final HttpExecutionStrategy strategy) { - this.strategy = defaultStrategy() == strategy ? - DEFAULT_BLOCKING_CONNECTION_STRATEGY : strategy; + this.strategy = defaultStrategy() == strategy ? DEFAULT_BLOCKING_CONNECTION_STRATEGY : strategy; this.client = client; context = new DelegatingHttpExecutionContext(client.executionContext()) { @Override @@ -46,9 +44,7 @@ public HttpExecutionStrategy executionStrategy() { @Override public ReservedBlockingHttpConnection reserveConnection(final HttpRequestMetaData metaData) throws Exception { - if (null == metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy)) { - metaData.context().put(HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.BLOCKING_AGGREGATED); - } + metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy); return blockingInvocation(client.reserveConnection(metaData) .map(c -> new ReservedStreamingHttpConnectionToReservedBlockingHttpConnection(c, this.strategy, reqRespFactory))); @@ -61,9 +57,7 @@ public StreamingHttpClient asStreamingClient() { @Override public HttpResponse request(final HttpRequest request) throws Exception { - if (null == request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy)) { - request.context().put(HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.BLOCKING_AGGREGATED); - } + request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy); return BlockingUtils.request(client, request); } diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingStreamingHttpClient.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingStreamingHttpClient.java index 89cae24967..a55b971893 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingStreamingHttpClient.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingStreamingHttpClient.java @@ -18,7 +18,6 @@ import io.servicetalk.concurrent.BlockingIterable; import static io.servicetalk.http.api.BlockingUtils.blockingInvocation; -import static io.servicetalk.http.api.HttpContextKeys.HTTP_CLIENT_API_KEY; import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.RequestResponseFactories.toBlockingStreaming; @@ -49,9 +48,7 @@ public ReservedBlockingStreamingHttpConnection reserveConnection(final HttpReque throws Exception { // It is assumed that users will always apply timeouts at the StreamingHttpService layer (e.g. via filter). // So we don't apply any explicit timeout here and just wait forever. - if (null == metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy)) { - metaData.context().put(HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.BLOCKING_STREAMING); - } + metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy); return blockingInvocation(client.reserveConnection(metaData) .map(c -> new ReservedStreamingHttpConnectionToBlockingStreaming(c, this.strategy, reqRespFactory))); } @@ -63,9 +60,7 @@ public StreamingHttpClient asStreamingClient() { @Override public BlockingStreamingHttpResponse request(final BlockingStreamingHttpRequest request) throws Exception { - if (null == request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy)) { - request.context().put(HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.BLOCKING_STREAMING); - } + request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy); return blockingInvocation(client.request(request.toStreamingRequest())).toBlockingStreamingResponse(); } diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToHttpClient.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToHttpClient.java index e0eee3b8d1..790e0d019f 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToHttpClient.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToHttpClient.java @@ -19,7 +19,6 @@ import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; -import static io.servicetalk.http.api.HttpContextKeys.HTTP_CLIENT_API_KEY; import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.RequestResponseFactories.toAggregated; @@ -47,9 +46,7 @@ public HttpExecutionStrategy executionStrategy() { @Override public Single request(final HttpRequest request) { return Single.defer(() -> { - if (null == request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy)) { - request.context().put(HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.ASYNC_AGGREGATED); - } + request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy); return client.request(request.toStreamingRequest()) .flatMap(response -> response.toResponse().shareContextOnSubscribe()) .shareContextOnSubscribe(); @@ -59,9 +56,7 @@ public Single request(final HttpRequest request) { @Override public Single reserveConnection(final HttpRequestMetaData metaData) { return Single.defer(() -> { - if (null == metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy)) { - metaData.context().put(HTTP_CLIENT_API_KEY, HttpApiConversions.ClientAPI.ASYNC_AGGREGATED); - } + metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, strategy); return client.reserveConnection(metaData) .map(c -> new ReservedStreamingHttpConnectionToReservedHttpConnection(c, this.strategy, reqRespFactory)) diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index 3fc88a1d77..0f1dd94cf6 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -32,7 +32,6 @@ import io.servicetalk.http.api.DefaultStreamingHttpRequestResponseFactory; import io.servicetalk.http.api.FilterableReservedStreamingHttpConnection; import io.servicetalk.http.api.FilterableStreamingHttpClient; -import io.servicetalk.http.api.HttpApiConversions; import io.servicetalk.http.api.HttpClient; import io.servicetalk.http.api.HttpConnectionContext; import io.servicetalk.http.api.HttpEventKey; @@ -82,9 +81,9 @@ import static io.servicetalk.http.api.HttpApiConversions.toReservedBlockingStreamingConnection; import static io.servicetalk.http.api.HttpApiConversions.toReservedConnection; import static io.servicetalk.http.api.HttpApiConversions.toStreamingClient; -import static io.servicetalk.http.api.HttpContextKeys.HTTP_CLIENT_API_KEY; import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; +import static io.servicetalk.http.api.HttpExecutionStrategies.offloadAll; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static io.servicetalk.http.netty.DefaultSingleAddressHttpClientBuilder.setExecutionContext; import static java.util.Objects.requireNonNull; @@ -284,6 +283,16 @@ private static final class SingleAddressStreamingHttpClientWrapper implements St this.multiClientStrategy = multiClientStrategy; } + @Override + public void close() throws Exception { + singleClient.close(); + } + + @Override + public void closeGracefully() throws Exception { + singleClient.closeGracefully(); + } + @Override public Completable closeAsync() { return singleClient.closeAsync(); @@ -404,33 +413,15 @@ public Single request(final StreamingHttpRequest request) private HttpExecutionStrategy applyMultiAddressStrategy(final ContextMap contextMap, final HttpExecutionStrategy singleClientStrategy) { - HttpExecutionStrategy requestStrategy = contextMap.get(HTTP_EXECUTION_STRATEGY_KEY); - HttpApiConversions.ClientAPI clientAPI = contextMap.get(HTTP_CLIENT_API_KEY); - HttpExecutionStrategy useStrategy = requestStrategy.hasOffloads() ? - // multi-client offloads something - defaultStrategy() == singleClientStrategy ? - defaultStrategy() == requestStrategy ? - // async streaming client with default strategy - HttpApiConversions.ClientAPI.ASYNC_STREAMING.defaultStrategy() : - // single client tried to "reset" strategy to default, use multi-client strategy - requestStrategy : - // non-default strategy client - singleClientStrategy.hasOffloads() ? - // merge single client strategy with request strategy - singleClientStrategy.merge(defaultStrategy() == requestStrategy ? - clientAPI.defaultStrategy() : requestStrategy) : - // single client tried to force no offloads, use multi-client strategy - requestStrategy : - // multi-client configured for no offloads - singleClientStrategy.hasOffloads() ? - // override in single client - defaultStrategy() == singleClientStrategy ? - // use API default - clientAPI.defaultStrategy() : - // use as specified - singleClientStrategy : - // single client does not override - requestStrategy; + HttpExecutionStrategy requestStrategy = + contextMap.getOrDefault(HTTP_EXECUTION_STRATEGY_KEY, defaultStrategy()); + HttpExecutionStrategy useStrategy = defaultStrategy() == requestStrategy ? + offloadAll() : defaultStrategy() == singleClientStrategy || !singleClientStrategy.hasOffloads() ? + // single client is default or has no *additional* offloads + requestStrategy : + // add single client offloads to existing strategy + requestStrategy.merge(singleClientStrategy); + if (useStrategy != requestStrategy) { contextMap.put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 8a4a3dcde9..f231ee19f2 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -325,6 +325,11 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde switch (builderType) { case Multi_ExecutionStrategy_Initializer_Override: + if (null == merged || defaultStrategy() == merged) { + return offloadNone(); + } else { + return merged; + } case Single: if (defaultStrategy() == merged) { switch (clientApi) { From 074584a0202fe243569f4907ac1ce766d647898b Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Wed, 13 Apr 2022 14:11:46 -0700 Subject: [PATCH 10/54] remove DefaultClientGroup changes from PR --- .../io/servicetalk/client/api/DefaultClientGroup.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/servicetalk-client-api/src/main/java/io/servicetalk/client/api/DefaultClientGroup.java b/servicetalk-client-api/src/main/java/io/servicetalk/client/api/DefaultClientGroup.java index ffdbbdf366..430f9499d9 100644 --- a/servicetalk-client-api/src/main/java/io/servicetalk/client/api/DefaultClientGroup.java +++ b/servicetalk-client-api/src/main/java/io/servicetalk/client/api/DefaultClientGroup.java @@ -123,19 +123,17 @@ public Client get(final Key key) { try { client = requireNonNull(clientFactory.apply(key), "Newly created client can not be null"); } catch (Throwable t) { - final boolean removed = clientMap.remove(key, PLACEHOLDER_CLIENT); - assert removed : "Expected to remove PLACEHOLDER_CLIENT"; + clientMap.remove(key); // PLACEHOLDER_CLIENT throw new IllegalArgumentException("Failed to create new client", t); } - final boolean replaced = clientMap.replace(key, PLACEHOLDER_CLIENT, client); - assert replaced : "Expected to replace PLACEHOLDER_CLIENT"; + clientMap.put(key, client); // Overwrite PLACEHOLDER_CLIENT toSource(client.onClose()).subscribe(new RemoveClientOnClose(key, client)); LOGGER.debug("A new client {} was created", client); if (closed) { // group has been closed after a new client was created - if (clientMap.remove(key, client)) { // not closed by closing thread + if (clientMap.remove(key) != null) { // not closed by closing thread client.closeAsync().subscribe(); LOGGER.debug("Recently created client {} was removed and closed, group {} closed", client, this); } From 2be80b6312ac6c7e1940588560b5f1b21a4804d4 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Wed, 13 Apr 2022 15:56:57 -0700 Subject: [PATCH 11/54] test cleanup --- .../netty/ClientEffectiveStrategyTest.java | 106 +++++++++--------- 1 file changed, 54 insertions(+), 52 deletions(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index f231ee19f2..064fc56130 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -35,6 +35,7 @@ import io.servicetalk.http.api.HttpExecutionStrategy; import io.servicetalk.http.api.HttpLoadBalancerFactory; import io.servicetalk.http.api.HttpRequest; +import io.servicetalk.http.api.HttpRequestMethod; import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.api.HttpServerBuilder; import io.servicetalk.http.api.MultiAddressHttpClientBuilder; @@ -90,7 +91,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -@Execution(ExecutionMode.CONCURRENT) +@Execution(ExecutionMode.SAME_THREAD) class ClientEffectiveStrategyTest { private static final String SCHEME = "http"; private static final String PATH = "/"; @@ -100,10 +101,10 @@ class ClientEffectiveStrategyTest { * Which builder API will be used and where will ExecutionStrategy be initialized. */ private enum BuilderType { - Single, - Multi_ExecutionStrategy_On_Builder, - Multi_ExecutionStrategy_In_Initializer, - Multi_ExecutionStrategy_Initializer_Override + SINGLE_BUILDER, + MULTI_BUILDER, + MULTI_DEFAULT_SINGLE_BUILDER, + MULTI_NONE_SINGLE_BUILDER } private static final HttpExecutionStrategy[] BUILDER_STRATEGIES = { @@ -157,7 +158,7 @@ static Stream casesSupplier() { List arguments = new ArrayList<>(); for (BuilderType builderType : BuilderType.values()) { for (HttpExecutionStrategy builderStrategy : BUILDER_STRATEGIES) { - if (BuilderType.Multi_ExecutionStrategy_Initializer_Override == builderType && + if (BuilderType.MULTI_NONE_SINGLE_BUILDER == builderType && null == builderStrategy) { // null builderStrategy won't actually override, so skip. continue; @@ -233,14 +234,14 @@ public HttpExecutionStrategy requiredOffloads() { }); } - if (builderType != BuilderType.Multi_ExecutionStrategy_On_Builder && null != builderStrategy) { + if (builderType != BuilderType.MULTI_BUILDER && null != builderStrategy) { clientBuilder.executionStrategy(builderStrategy); } }; String requestTarget; Supplier clientBuilder; switch (builderType) { - case Single: + case SINGLE_BUILDER: requestTarget = PATH; SingleAddressHttpClientBuilder singleClientBuilder = HttpClients.forSingleAddress(serverHostAndPort(context)); @@ -248,16 +249,16 @@ public HttpExecutionStrategy requiredOffloads() { initializer.initialize(SCHEME, serverHostAndPort(context), singleClientBuilder); clientBuilder = singleClientBuilder::buildStreaming; break; - case Multi_ExecutionStrategy_On_Builder: - case Multi_ExecutionStrategy_In_Initializer: - case Multi_ExecutionStrategy_Initializer_Override: + case MULTI_BUILDER: + case MULTI_DEFAULT_SINGLE_BUILDER: + case MULTI_NONE_SINGLE_BUILDER: requestTarget = SCHEME + "://" + serverHostAndPort(context) + PATH; MultiAddressHttpClientBuilder multiClientBuilder = HttpClients.forMultiAddressUrl().initializer(initializer); - if (BuilderType.Multi_ExecutionStrategy_On_Builder == builderType && null != builderStrategy) { + if (BuilderType.MULTI_BUILDER == builderType && null != builderStrategy) { multiClientBuilder.executionStrategy(builderStrategy); } - if (BuilderType.Multi_ExecutionStrategy_Initializer_Override == builderType && + if (BuilderType.MULTI_NONE_SINGLE_BUILDER == builderType && null != builderStrategy) { // This is expected to ALWAYS be overridden in initializer. multiClientBuilder.executionStrategy(offloadNone()); @@ -275,7 +276,7 @@ public HttpExecutionStrategy requiredOffloads() { builderType, builderStrategy, filterStrategy, lbStrategy, cfStrategy, clientApi); invokingThreadsRecorder.reset(clientApi, effectiveStrategy); - String responseBody = getResponse(clientApi, client, requestTarget); + String responseBody = getResponse(clientApi, client, HttpRequestMethod.GET, requestTarget); assertThat("Unexpected response: " + responseBody, responseBody, is(GREETING)); invokingThreadsRecorder.verifyOffloads(clientApi); } @@ -324,22 +325,16 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde builder; switch (builderType) { - case Multi_ExecutionStrategy_Initializer_Override: - if (null == merged || defaultStrategy() == merged) { - return offloadNone(); - } else { - return merged; - } - case Single: + case SINGLE_BUILDER: if (defaultStrategy() == merged) { switch (clientApi) { - case BlockingAggregate: + case BLOCKING_AGGREGATE: return offloadNone(); - case BlockingStreaming: + case BLOCKING_STREAMING: return HttpExecutionStrategies.customStrategyBuilder().offloadSend().build(); - case AsyncAggregate: + case ASYNC_AGGREGATE: return HttpExecutionStrategies.customStrategyBuilder().offloadReceiveData().build(); - case AsyncStreaming: + case ASYNC_STREAMING: return HttpExecutionStrategies.customStrategyBuilder() .offloadSend().offloadReceiveMetadata().offloadReceiveData().build(); default: @@ -348,20 +343,20 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde } else { return merged; } - case Multi_ExecutionStrategy_On_Builder: + case MULTI_BUILDER: if (null == builder || defaultStrategy() == builder) { if (defaultStrategy() == merged) { merged = offloadNone(); } switch (clientApi) { - case BlockingAggregate: + case BLOCKING_AGGREGATE: return merged; - case BlockingStreaming: + case BLOCKING_STREAMING: return HttpExecutionStrategies.customStrategyBuilder().offloadSend().build().merge(merged); - case AsyncAggregate: + case ASYNC_AGGREGATE: return HttpExecutionStrategies.customStrategyBuilder() .offloadReceiveData().build().merge(merged); - case AsyncStreaming: + case ASYNC_STREAMING: return HttpExecutionStrategies.customStrategyBuilder() .offloadSend().offloadReceiveMetadata().offloadReceiveData().build().merge(merged); default: @@ -369,24 +364,30 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde } } return merged; - case Multi_ExecutionStrategy_In_Initializer: + case MULTI_DEFAULT_SINGLE_BUILDER: if (defaultStrategy() == merged || (null != builder && !builder.hasOffloads())) { merged = offloadNone(); } switch (clientApi) { - case BlockingAggregate: + case BLOCKING_AGGREGATE: return merged; - case BlockingStreaming: + case BLOCKING_STREAMING: return HttpExecutionStrategies.customStrategyBuilder().offloadSend().build().merge(merged); - case AsyncAggregate: + case ASYNC_AGGREGATE: return HttpExecutionStrategies.customStrategyBuilder() .offloadReceiveData().build().merge(merged); - case AsyncStreaming: + case ASYNC_STREAMING: return HttpExecutionStrategies.customStrategyBuilder() .offloadSend().offloadReceiveMetadata().offloadReceiveData().build().merge(merged); default: throw new AssertionError("Unexpected client api: " + clientApi); } + case MULTI_NONE_SINGLE_BUILDER: + if (null == merged || defaultStrategy() == merged) { + return offloadNone(); + } else { + return merged; + } default: throw new AssertionError("Unexpected builder type: " + builderType); } @@ -399,18 +400,19 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde return null == first ? second : null == second ? first : first.merge(second); } - private String getResponse(ClientApi clientApi, StreamingHttpClient client, String requestTarget) throws Exception { + private String getResponse(final ClientApi clientApi, final StreamingHttpClient client, + final HttpRequestMethod requestMethod, final String requestTarget) throws Exception { switch (clientApi) { - case BlockingAggregate: { + case BLOCKING_AGGREGATE: { BlockingHttpClient blockingClient = client.asBlockingClient(); - HttpRequest request = blockingClient.get(requestTarget); + HttpRequest request = blockingClient.newRequest(requestMethod, requestTarget); HttpResponse response = blockingClient.request(request); return response.payloadBody().toString(StandardCharsets.US_ASCII); } - case BlockingStreaming: { + case BLOCKING_STREAMING: { BlockingStreamingHttpClient blockingStreamingClient = client.asBlockingStreamingClient(); - BlockingStreamingHttpRequest request = blockingStreamingClient.get(requestTarget); + BlockingStreamingHttpRequest request = blockingStreamingClient.newRequest(requestMethod, requestTarget); BlockingStreamingHttpResponse response = blockingStreamingClient.request(request); Supplier supplier = blockingStreamingClient.executionContext().bufferAllocator()::newCompositeBuffer; @@ -421,15 +423,15 @@ private String getResponse(ClientApi clientApi, StreamingHttpClient client, Stri .orElseThrow(() -> new AssertionError("No payload in response")); } - case AsyncAggregate: { + case ASYNC_AGGREGATE: { HttpClient httpClient = client.asClient(); - HttpRequest request = httpClient.get(requestTarget); + HttpRequest request = httpClient.newRequest(requestMethod, requestTarget); HttpResponse response = httpClient.request(request).toFuture().get(); return response.payloadBody().toString(StandardCharsets.US_ASCII); } - case AsyncStreaming: { - StreamingHttpRequest request = client.get(requestTarget); + case ASYNC_STREAMING: { + StreamingHttpRequest request = client.newRequest(requestMethod, requestTarget); CompositeBuffer responsePayload = client.request(request) .flatMap(resp -> resp.payloadBody().collect(() -> client.executionContext().bufferAllocator().newCompositeBuffer(), @@ -469,16 +471,16 @@ void reset(ClientApi clientApi, HttpExecutionStrategy streamingAsyncStrategy) { } else { // apply default offloads per client api switch (clientApi) { - case BlockingAggregate: + case BLOCKING_AGGREGATE: offloadPoints = EnumSet.noneOf(ClientOffloadPoint.class); break; - case BlockingStreaming: + case BLOCKING_STREAMING: offloadPoints = EnumSet.of(Send); break; - case AsyncAggregate: + case ASYNC_AGGREGATE: offloadPoints = EnumSet.of(ReceiveData); break; - case AsyncStreaming: + case ASYNC_STREAMING: offloadPoints = EnumSet.allOf(ClientOffloadPoint.class); break; default: @@ -557,10 +559,10 @@ public ExecutionStrategy requiredOffloads() { * Which API flavor will be used. */ private enum ClientApi { - BlockingAggregate, - BlockingStreaming, - AsyncAggregate, - AsyncStreaming + BLOCKING_AGGREGATE, + BLOCKING_STREAMING, + ASYNC_AGGREGATE, + ASYNC_STREAMING } /** From 4ade1dceb82240456a38f4c7e869288b95074de4 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Thu, 14 Apr 2022 15:20:22 -0700 Subject: [PATCH 12/54] move FilterableClientToClient back --- .../http/api/HttpApiConversions.java | 12 - ...faultMultiAddressUrlHttpClientBuilder.java | 211 +++--------------- .../DefaultPartitionedHttpClientBuilder.java | 3 +- ...DefaultSingleAddressHttpClientBuilder.java | 3 +- .../http/netty}/FilterableClientToClient.java | 23 +- .../netty/ClientEffectiveStrategyTest.java | 2 +- 6 files changed, 61 insertions(+), 193 deletions(-) rename {servicetalk-http-api/src/main/java/io/servicetalk/http/api => servicetalk-http-netty/src/main/java/io/servicetalk/http/netty}/FilterableClientToClient.java (85%) diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java index 1b52f344a0..dbc8b2d02c 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java @@ -226,18 +226,6 @@ public static HttpClient toClient(StreamingHttpClient original, HttpExecutionStr return new StreamingHttpClientToHttpClient(original, strategy); } - /** - * Convert from {@link FilterableStreamingHttpClient} to {@link StreamingHttpClient}. - * - * @param client Original {@link FilterableClientToClient} to convert. - * @param strategy required strategy for the service when invoking the resulting {@link HttpClient} - * @return The conversion result. - */ - public static StreamingHttpClient toStreamingClient(final FilterableStreamingHttpClient client, - final HttpExecutionStrategy strategy) { - return new FilterableClientToClient(client, strategy); - } - /** * Convert from {@link StreamingHttpClient} to {@link BlockingHttpClient}. * diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index 0f1dd94cf6..154360c0fd 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -22,19 +22,13 @@ import io.servicetalk.concurrent.api.CompositeCloseable; import io.servicetalk.concurrent.api.Executor; import io.servicetalk.concurrent.api.ListenableAsyncCloseable; -import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; import io.servicetalk.concurrent.api.internal.SubscribableCompletable; import io.servicetalk.context.api.ContextMap; -import io.servicetalk.http.api.BlockingHttpClient; -import io.servicetalk.http.api.BlockingStreamingHttpClient; import io.servicetalk.http.api.DefaultHttpHeadersFactory; import io.servicetalk.http.api.DefaultStreamingHttpRequestResponseFactory; import io.servicetalk.http.api.FilterableReservedStreamingHttpConnection; import io.servicetalk.http.api.FilterableStreamingHttpClient; -import io.servicetalk.http.api.HttpClient; -import io.servicetalk.http.api.HttpConnectionContext; -import io.servicetalk.http.api.HttpEventKey; import io.servicetalk.http.api.HttpExecutionContext; import io.servicetalk.http.api.HttpExecutionStrategy; import io.servicetalk.http.api.HttpHeadersFactory; @@ -42,14 +36,13 @@ import io.servicetalk.http.api.HttpRequestMethod; import io.servicetalk.http.api.MultiAddressHttpClientBuilder; import io.servicetalk.http.api.RedirectConfig; -import io.servicetalk.http.api.ReservedBlockingHttpConnection; -import io.servicetalk.http.api.ReservedBlockingStreamingHttpConnection; -import io.servicetalk.http.api.ReservedHttpConnection; -import io.servicetalk.http.api.ReservedStreamingHttpConnection; import io.servicetalk.http.api.SingleAddressHttpClientBuilder; import io.servicetalk.http.api.StreamingHttpClient; +import io.servicetalk.http.api.StreamingHttpClientFilter; +import io.servicetalk.http.api.StreamingHttpClientFilterFactory; import io.servicetalk.http.api.StreamingHttpRequest; import io.servicetalk.http.api.StreamingHttpRequestResponseFactory; +import io.servicetalk.http.api.StreamingHttpRequester; import io.servicetalk.http.api.StreamingHttpResponse; import io.servicetalk.http.api.StreamingHttpResponseFactory; import io.servicetalk.http.utils.RedirectingHttpRequesterFilter; @@ -74,16 +67,10 @@ import static io.servicetalk.concurrent.api.AsyncCloseables.toListenableAsyncCloseable; import static io.servicetalk.concurrent.api.Single.defer; import static io.servicetalk.concurrent.internal.SubscriberUtils.deliverCompleteFromSource; -import static io.servicetalk.http.api.HttpApiConversions.toBlockingClient; -import static io.servicetalk.http.api.HttpApiConversions.toBlockingStreamingClient; -import static io.servicetalk.http.api.HttpApiConversions.toClient; -import static io.servicetalk.http.api.HttpApiConversions.toReservedBlockingConnection; -import static io.servicetalk.http.api.HttpApiConversions.toReservedBlockingStreamingConnection; -import static io.servicetalk.http.api.HttpApiConversions.toReservedConnection; -import static io.servicetalk.http.api.HttpApiConversions.toStreamingClient; import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.HttpExecutionStrategies.offloadAll; +import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNone; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static io.servicetalk.http.netty.DefaultSingleAddressHttpClientBuilder.setExecutionContext; import static java.util.Objects.requireNonNull; @@ -143,7 +130,7 @@ public StreamingHttpClient buildStreaming() { new RedirectingHttpRequesterFilter(redirectConfig).create(urlClient); LOGGER.debug("Multi-address client created with base strategy {}", executionContext.executionStrategy()); - return toStreamingClient(urlClient, executionContext.executionStrategy()); + return new FilterableClientToClient(urlClient, executionContext.executionStrategy()); } catch (final Throwable t) { closeables.closeAsync().subscribe(); throw t; @@ -262,180 +249,56 @@ public StreamingHttpClient apply(final UrlKey urlKey) { builder.sslConfig(DEFAULT_CLIENT_SSL_CONFIG); } + MultiAddressStrategyWrapper wrapper = new MultiAddressStrategyWrapper(); + builder.appendClientFilter(wrapper); + if (singleAddressInitializer != null) { singleAddressInitializer.initialize(urlKey.scheme, urlKey.hostAndPort, builder); } StreamingHttpClient singleClient = builder.buildStreaming(); - return new SingleAddressStreamingHttpClientWrapper(singleClient, executionContext.executionStrategy()); + wrapper.clientStrategy = singleClient.executionContext().executionStrategy(); + return singleClient; } } - private static final class SingleAddressStreamingHttpClientWrapper implements StreamingHttpClient { - - final StreamingHttpClient singleClient; - final HttpExecutionStrategy multiClientStrategy; - - SingleAddressStreamingHttpClientWrapper(final StreamingHttpClient singleClient, - final HttpExecutionStrategy multiClientStrategy) { - this.singleClient = singleClient; - this.multiClientStrategy = multiClientStrategy; - } - - @Override - public void close() throws Exception { - singleClient.close(); - } - - @Override - public void closeGracefully() throws Exception { - singleClient.closeGracefully(); - } - - @Override - public Completable closeAsync() { - return singleClient.closeAsync(); - } - - @Override - public Completable onClose() { - return singleClient.onClose(); - } + private static final class MultiAddressStrategyWrapper implements StreamingHttpClientFilterFactory { + HttpExecutionStrategy clientStrategy = defaultStrategy(); @Override - public Single reserveConnection(final HttpRequestMetaData metaData) { - HttpExecutionStrategy ourStrategy = singleClient.executionContext().executionStrategy(); - return Single.defer(() -> { - metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, ourStrategy); - return singleClient.reserveConnection(metaData).map(rc -> new ReservedStreamingHttpConnection() { - @Override - public ReservedHttpConnection asConnection() { - return toReservedConnection(this, ourStrategy); - } - - @Override - public ReservedBlockingStreamingHttpConnection asBlockingStreamingConnection() { - return toReservedBlockingStreamingConnection(this, ourStrategy); - } - - @Override - public ReservedBlockingHttpConnection asBlockingConnection() { - return toReservedBlockingConnection(this, ourStrategy); - } - - @Override - public Completable releaseAsync() { - return rc.releaseAsync(); - } - - @Override - public Single request(final StreamingHttpRequest request) { - // Use the strategy from the client as the underlying ReservedStreamingHttpConnection may be user - // created and hence could have an incorrect default strategy. Doing this makes sure we never call - // the method without strategy just as we do for the regular connection. - return Single.defer(() -> { - applyMultiAddressStrategy(request.context(), ourStrategy); - return rc.request(request).shareContextOnSubscribe(); - }); - } - - @Override - public HttpConnectionContext connectionContext() { - return rc.connectionContext(); - } - - @Override - public Publisher transportEventStream(final HttpEventKey eventKey) { - return rc.transportEventStream(eventKey); - } - - @Override - public HttpExecutionContext executionContext() { - return rc.executionContext(); - } - - @Override - public StreamingHttpResponseFactory httpResponseFactory() { - return rc.httpResponseFactory(); - } - - @Override - public Completable onClose() { - return rc.onClose(); - } - - @Override - public Completable closeAsync() { - return rc.closeAsync(); - } - + public StreamingHttpClientFilter create(final FilterableStreamingHttpClient client) { + return new StreamingHttpClientFilter(client) { @Override - public Completable closeAsyncGracefully() { - return rc.closeAsyncGracefully(); + protected Single request( + final StreamingHttpRequester delegate, final StreamingHttpRequest request) { + applyMultiAddressStrategy(request.context(), clientStrategy); + return super.request(delegate, request); } - @Override - public StreamingHttpRequest newRequest(final HttpRequestMethod method, final String requestTarget) { - return rc.newRequest(method, requestTarget); + private HttpExecutionStrategy applyMultiAddressStrategy( + final ContextMap contextMap, final HttpExecutionStrategy singleClientStrategy) { + HttpExecutionStrategy requestStrategy = + contextMap.getOrDefault(HTTP_EXECUTION_STRATEGY_KEY, defaultStrategy()); + HttpExecutionStrategy useStrategy = defaultStrategy() == requestStrategy ? + offloadAll() : + defaultStrategy() == singleClientStrategy || !singleClientStrategy.hasOffloads() ? + // single client is default or has no *additional* offloads + requestStrategy : + // add single client offloads to existing strategy + requestStrategy.merge(singleClientStrategy); + + if (useStrategy != requestStrategy) { + contextMap.put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); + } + return useStrategy; } - }).shareContextOnSubscribe(); - }); - } - - @Override - public HttpClient asClient() { - return toClient(this, singleClient.executionContext().executionStrategy()); - } - - @Override - public BlockingStreamingHttpClient asBlockingStreamingClient() { - return toBlockingStreamingClient(this, singleClient.executionContext().executionStrategy()); - } - - @Override - public BlockingHttpClient asBlockingClient() { - return toBlockingClient(this, singleClient.executionContext().executionStrategy()); - } - - @Override - public StreamingHttpRequest newRequest(final HttpRequestMethod method, final String requestTarget) { - return singleClient.newRequest(method, requestTarget); - } - - @Override - public Single request(final StreamingHttpRequest request) { - return defer(() -> { - applyMultiAddressStrategy(request.context(), singleClient.executionContext().executionStrategy()); - return singleClient.request(request); - }); - } - - private HttpExecutionStrategy applyMultiAddressStrategy(final ContextMap contextMap, - final HttpExecutionStrategy singleClientStrategy) { - HttpExecutionStrategy requestStrategy = - contextMap.getOrDefault(HTTP_EXECUTION_STRATEGY_KEY, defaultStrategy()); - HttpExecutionStrategy useStrategy = defaultStrategy() == requestStrategy ? - offloadAll() : defaultStrategy() == singleClientStrategy || !singleClientStrategy.hasOffloads() ? - // single client is default or has no *additional* offloads - requestStrategy : - // add single client offloads to existing strategy - requestStrategy.merge(singleClientStrategy); - - if (useStrategy != requestStrategy) { - contextMap.put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); - } - return useStrategy; - } - - @Override - public HttpExecutionContext executionContext() { - return singleClient.executionContext(); + }; } @Override - public StreamingHttpResponseFactory httpResponseFactory() { - return singleClient.httpResponseFactory(); + public HttpExecutionStrategy requiredOffloads() { + return offloadNone(); } } diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultPartitionedHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultPartitionedHttpClientBuilder.java index 376d5b8eb6..0fba847dca 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultPartitionedHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultPartitionedHttpClientBuilder.java @@ -63,7 +63,6 @@ import static io.servicetalk.concurrent.api.RetryStrategies.retryWithConstantBackoffDeltaJitter; import static io.servicetalk.concurrent.api.Single.defer; import static io.servicetalk.concurrent.api.Single.failed; -import static io.servicetalk.http.api.HttpApiConversions.toStreamingClient; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static io.servicetalk.http.netty.DefaultSingleAddressHttpClientBuilder.SD_RETRY_STRATEGY_INIT_DURATION; import static io.servicetalk.http.netty.DefaultSingleAddressHttpClientBuilder.SD_RETRY_STRATEGY_JITTER; @@ -133,7 +132,7 @@ public StreamingHttpClient buildStreaming() { executionContext, partitionMapFactory); LOGGER.debug("Partitioned client created with base strategy {}", executionContext.executionStrategy()); - return toStreamingClient(partitionedClient, executionContext.executionStrategy()); + return new FilterableClientToClient(partitionedClient, executionContext.executionStrategy()); } private static final class DefaultPartitionedStreamingHttpClientFilter implements diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java index 76b3f78965..ea1f4e2aca 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java @@ -68,7 +68,6 @@ import static io.servicetalk.concurrent.api.AsyncCloseables.newCompositeCloseable; import static io.servicetalk.concurrent.api.Processors.newCompletableProcessor; import static io.servicetalk.concurrent.api.RetryStrategies.retryWithConstantBackoffDeltaJitter; -import static io.servicetalk.http.api.HttpApiConversions.toStreamingClient; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_2_0; import static io.servicetalk.http.netty.AlpnIds.HTTP_2; @@ -294,7 +293,7 @@ ctx.builder.connectionFilterFactory, new AlpnReqRespFactoryFunc( HttpExecutionStrategy computedStrategy = ctx.builder.strategyComputation.buildForClient(executionStrategy); LOGGER.debug("Client for {} created with base strategy {} → computed strategy {}", targetAddress(ctx), executionStrategy, computedStrategy); - return toStreamingClient(currClientFilterFactory != null ? + return new FilterableClientToClient(currClientFilterFactory != null ? currClientFilterFactory.create(lbClient, lb.eventStream(), ctx.sdStatus) : lbClient, computedStrategy); } catch (final Throwable t) { diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/FilterableClientToClient.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/FilterableClientToClient.java similarity index 85% rename from servicetalk-http-api/src/main/java/io/servicetalk/http/api/FilterableClientToClient.java rename to servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/FilterableClientToClient.java index faa20265e9..3051128d98 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/FilterableClientToClient.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/FilterableClientToClient.java @@ -13,11 +13,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.servicetalk.http.api; +package io.servicetalk.http.netty; import io.servicetalk.concurrent.api.Completable; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; +import io.servicetalk.http.api.BlockingHttpClient; +import io.servicetalk.http.api.BlockingStreamingHttpClient; +import io.servicetalk.http.api.DelegatingHttpExecutionContext; +import io.servicetalk.http.api.FilterableStreamingHttpClient; +import io.servicetalk.http.api.HttpClient; +import io.servicetalk.http.api.HttpConnectionContext; +import io.servicetalk.http.api.HttpEventKey; +import io.servicetalk.http.api.HttpExecutionContext; +import io.servicetalk.http.api.HttpExecutionStrategy; +import io.servicetalk.http.api.HttpRequestMetaData; +import io.servicetalk.http.api.HttpRequestMethod; +import io.servicetalk.http.api.ReservedBlockingHttpConnection; +import io.servicetalk.http.api.ReservedBlockingStreamingHttpConnection; +import io.servicetalk.http.api.ReservedHttpConnection; +import io.servicetalk.http.api.ReservedStreamingHttpConnection; +import io.servicetalk.http.api.StreamingHttpClient; +import io.servicetalk.http.api.StreamingHttpRequest; +import io.servicetalk.http.api.StreamingHttpResponse; +import io.servicetalk.http.api.StreamingHttpResponseFactory; import static io.servicetalk.http.api.HttpApiConversions.toBlockingClient; import static io.servicetalk.http.api.HttpApiConversions.toBlockingStreamingClient; @@ -29,7 +48,7 @@ final class FilterableClientToClient implements StreamingHttpClient { private final FilterableStreamingHttpClient client; - private final DelegatingHttpExecutionContext executionContext; + private final HttpExecutionContext executionContext; FilterableClientToClient(FilterableStreamingHttpClient filteredClient, HttpExecutionStrategy strategy) { client = filteredClient; diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 064fc56130..9fb2b6bedc 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -91,7 +91,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -@Execution(ExecutionMode.SAME_THREAD) +@Execution(ExecutionMode.CONCURRENT) class ClientEffectiveStrategyTest { private static final String SCHEME = "http"; private static final String PATH = "/"; From b0d2c3e9dc522dd6c8e7a07f66d7968c55170cf9 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Fri, 15 Apr 2022 09:24:20 -0700 Subject: [PATCH 13/54] additional review feedback --- ...faultMultiAddressUrlHttpClientBuilder.java | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index 154360c0fd..c331f1d201 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -24,7 +24,6 @@ import io.servicetalk.concurrent.api.ListenableAsyncCloseable; import io.servicetalk.concurrent.api.Single; import io.servicetalk.concurrent.api.internal.SubscribableCompletable; -import io.servicetalk.context.api.ContextMap; import io.servicetalk.http.api.DefaultHttpHeadersFactory; import io.servicetalk.http.api.DefaultStreamingHttpRequestResponseFactory; import io.servicetalk.http.api.FilterableReservedStreamingHttpConnection; @@ -258,40 +257,42 @@ public StreamingHttpClient apply(final UrlKey urlKey) { StreamingHttpClient singleClient = builder.buildStreaming(); - wrapper.clientStrategy = singleClient.executionContext().executionStrategy(); + wrapper.singleStrategy = singleClient.executionContext().executionStrategy(); return singleClient; } } private static final class MultiAddressStrategyWrapper implements StreamingHttpClientFilterFactory { - HttpExecutionStrategy clientStrategy = defaultStrategy(); + /** + * The strategy used by the single client. Any requests made during the `buildStreaming` will have to use the + * default strategy. + */ + HttpExecutionStrategy singleStrategy = defaultStrategy(); + @Override public StreamingHttpClientFilter create(final FilterableStreamingHttpClient client) { return new StreamingHttpClientFilter(client) { @Override protected Single request( final StreamingHttpRequester delegate, final StreamingHttpRequest request) { - applyMultiAddressStrategy(request.context(), clientStrategy); - return super.request(delegate, request); - } - - private HttpExecutionStrategy applyMultiAddressStrategy( - final ContextMap contextMap, final HttpExecutionStrategy singleClientStrategy) { - HttpExecutionStrategy requestStrategy = - contextMap.getOrDefault(HTTP_EXECUTION_STRATEGY_KEY, defaultStrategy()); - HttpExecutionStrategy useStrategy = defaultStrategy() == requestStrategy ? - offloadAll() : - defaultStrategy() == singleClientStrategy || !singleClientStrategy.hasOffloads() ? - // single client is default or has no *additional* offloads - requestStrategy : - // add single client offloads to existing strategy - requestStrategy.merge(singleClientStrategy); - - if (useStrategy != requestStrategy) { - contextMap.put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); - } - return useStrategy; + return defer(() -> { + HttpExecutionStrategy requestStrategy = + request.context().getOrDefault(HTTP_EXECUTION_STRATEGY_KEY, defaultStrategy()); + HttpExecutionStrategy useStrategy = + null == requestStrategy || defaultStrategy() == requestStrategy ? + offloadAll() : + defaultStrategy() == singleStrategy || !singleStrategy.hasOffloads() ? + // single client is default or has no *additional* offloads + requestStrategy : + // add single client offloads to existing strategy + requestStrategy.merge(singleStrategy); + + if (useStrategy != requestStrategy) { + request.context().put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); + } + return delegate.request(request); + }); } }; } From 0f7c4447e0b46d3e30966dbe10d7b1036d4c02a0 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Fri, 15 Apr 2022 10:31:35 -0700 Subject: [PATCH 14/54] test cleanup --- .../netty/ClientEffectiveStrategyTest.java | 116 +++++++----------- 1 file changed, 41 insertions(+), 75 deletions(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 9fb2b6bedc..64029d1b9b 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -326,68 +326,22 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde switch (builderType) { case SINGLE_BUILDER: - if (defaultStrategy() == merged) { - switch (clientApi) { - case BLOCKING_AGGREGATE: - return offloadNone(); - case BLOCKING_STREAMING: - return HttpExecutionStrategies.customStrategyBuilder().offloadSend().build(); - case ASYNC_AGGREGATE: - return HttpExecutionStrategies.customStrategyBuilder().offloadReceiveData().build(); - case ASYNC_STREAMING: - return HttpExecutionStrategies.customStrategyBuilder() - .offloadSend().offloadReceiveMetadata().offloadReceiveData().build(); - default: - throw new AssertionError("Unexpected client api: " + clientApi); - } - } else { - return merged; - } + return null == merged || defaultStrategy() == merged ? clientApi.strategy() : merged; case MULTI_BUILDER: if (null == builder || defaultStrategy() == builder) { if (defaultStrategy() == merged) { merged = offloadNone(); } - switch (clientApi) { - case BLOCKING_AGGREGATE: - return merged; - case BLOCKING_STREAMING: - return HttpExecutionStrategies.customStrategyBuilder().offloadSend().build().merge(merged); - case ASYNC_AGGREGATE: - return HttpExecutionStrategies.customStrategyBuilder() - .offloadReceiveData().build().merge(merged); - case ASYNC_STREAMING: - return HttpExecutionStrategies.customStrategyBuilder() - .offloadSend().offloadReceiveMetadata().offloadReceiveData().build().merge(merged); - default: - throw new AssertionError("Unexpected client api: " + clientApi); - } + return clientApi.strategy().merge(merged); } return merged; case MULTI_DEFAULT_SINGLE_BUILDER: if (defaultStrategy() == merged || (null != builder && !builder.hasOffloads())) { merged = offloadNone(); } - switch (clientApi) { - case BLOCKING_AGGREGATE: - return merged; - case BLOCKING_STREAMING: - return HttpExecutionStrategies.customStrategyBuilder().offloadSend().build().merge(merged); - case ASYNC_AGGREGATE: - return HttpExecutionStrategies.customStrategyBuilder() - .offloadReceiveData().build().merge(merged); - case ASYNC_STREAMING: - return HttpExecutionStrategies.customStrategyBuilder() - .offloadSend().offloadReceiveMetadata().offloadReceiveData().build().merge(merged); - default: - throw new AssertionError("Unexpected client api: " + clientApi); - } + return clientApi.strategy().merge(merged); case MULTI_NONE_SINGLE_BUILDER: - if (null == merged || defaultStrategy() == merged) { - return offloadNone(); - } else { - return merged; - } + return null == merged || defaultStrategy() == merged ? offloadNone() : merged; default: throw new AssertionError("Unexpected builder type: " + builderType); } @@ -447,17 +401,15 @@ private String getResponse(final ClientApi clientApi, final StreamingHttpClient private static final class ClientInvokingThreadRecorder implements StreamingHttpClientFilterFactory { - private EnumSet offloadPoints; + private final EnumSet offloadPoints = EnumSet.noneOf(ClientOffloadPoint.class); private final ConcurrentMap invokingThreads = new ConcurrentHashMap<>(); private final Queue errors = new LinkedBlockingQueue<>(); void reset(ClientApi clientApi, HttpExecutionStrategy streamingAsyncStrategy) { invokingThreads.clear(); errors.clear(); - if (!streamingAsyncStrategy.hasOffloads()) { - offloadPoints = EnumSet.noneOf(ClientOffloadPoint.class); - } else if (defaultStrategy() != streamingAsyncStrategy) { - offloadPoints = EnumSet.noneOf(ClientOffloadPoint.class); + offloadPoints.clear(); + if (defaultStrategy() != streamingAsyncStrategy) { // adjust expected offloads for specific execution strategy if (streamingAsyncStrategy.isSendOffloaded()) { offloadPoints.add(Send); @@ -470,22 +422,7 @@ void reset(ClientApi clientApi, HttpExecutionStrategy streamingAsyncStrategy) { } } else { // apply default offloads per client api - switch (clientApi) { - case BLOCKING_AGGREGATE: - offloadPoints = EnumSet.noneOf(ClientOffloadPoint.class); - break; - case BLOCKING_STREAMING: - offloadPoints = EnumSet.of(Send); - break; - case ASYNC_AGGREGATE: - offloadPoints = EnumSet.of(ReceiveData); - break; - case ASYNC_STREAMING: - offloadPoints = EnumSet.allOf(ClientOffloadPoint.class); - break; - default: - throw new AssertionError("unexpected case " + clientApi); - } + offloadPoints.addAll(clientApi.offloads()); } } @@ -559,10 +496,39 @@ public ExecutionStrategy requiredOffloads() { * Which API flavor will be used. */ private enum ClientApi { - BLOCKING_AGGREGATE, - BLOCKING_STREAMING, - ASYNC_AGGREGATE, - ASYNC_STREAMING + BLOCKING_AGGREGATE(EnumSet.noneOf(ClientOffloadPoint.class)), + BLOCKING_STREAMING(EnumSet.of(Send)), + ASYNC_AGGREGATE(EnumSet.of(ReceiveData)), + ASYNC_STREAMING(EnumSet.allOf(ClientOffloadPoint.class)); + + private final EnumSet offloads; + private final HttpExecutionStrategy strategy; + + ClientApi(EnumSet offloads) { + this.offloads = offloads; + + HttpExecutionStrategies.Builder builder = HttpExecutionStrategies.customStrategyBuilder(); + + if (offloads.contains(Send)) { + builder.offloadSend(); + } + if (offloads.contains(ReceiveMeta)) { + builder.offloadReceiveMetadata(); + } + if (offloads.contains(ReceiveData)) { + builder.offloadReceiveData(); + } + + this.strategy = builder.build(); + } + + public EnumSet offloads() { + return offloads; + } + + public HttpExecutionStrategy strategy() { + return strategy; + } } /** From 3e151d6ee63b1d1963f7f86ba3477204468f17c7 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Fri, 15 Apr 2022 11:22:56 -0700 Subject: [PATCH 15/54] test cleanup --- .../servicetalk/http/netty/ClientEffectiveStrategyTest.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 64029d1b9b..7b6c245bab 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -329,10 +329,8 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde return null == merged || defaultStrategy() == merged ? clientApi.strategy() : merged; case MULTI_BUILDER: if (null == builder || defaultStrategy() == builder) { - if (defaultStrategy() == merged) { - merged = offloadNone(); - } - return clientApi.strategy().merge(merged); + return null == merged || defaultStrategy() == merged ? + offloadNone() : clientApi.strategy().merge(merged); } return merged; case MULTI_DEFAULT_SINGLE_BUILDER: From 035fdd3e9fd6de3f02eeec617e4ab251b7c7570e Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Fri, 15 Apr 2022 13:42:57 -0700 Subject: [PATCH 16/54] fix multi-address builder strategy computation --- .../servicetalk/http/netty/ClientEffectiveStrategyTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 7b6c245bab..bea3600f85 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -271,7 +271,7 @@ public HttpExecutionStrategy requiredOffloads() { // Exercise the client for (final ClientApi clientApi : ClientApi.values()) { - try (StreamingHttpClient client = Objects.requireNonNull(clientBuilder.get())) { + try (final StreamingHttpClient client = Objects.requireNonNull(clientBuilder.get())) { HttpExecutionStrategy effectiveStrategy = computeClientExecutionStrategy( builderType, builderStrategy, filterStrategy, lbStrategy, cfStrategy, clientApi); @@ -330,7 +330,7 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde case MULTI_BUILDER: if (null == builder || defaultStrategy() == builder) { return null == merged || defaultStrategy() == merged ? - offloadNone() : clientApi.strategy().merge(merged); + clientApi.strategy() : clientApi.strategy().merge(merged); } return merged; case MULTI_DEFAULT_SINGLE_BUILDER: From 030833a387ced7b36c9ccd8f70a74079f53186bc Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Fri, 15 Apr 2022 14:33:40 -0700 Subject: [PATCH 17/54] checkstyle nit --- .../io/servicetalk/http/netty/ClientEffectiveStrategyTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index bea3600f85..45575fb4e5 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -271,7 +271,7 @@ public HttpExecutionStrategy requiredOffloads() { // Exercise the client for (final ClientApi clientApi : ClientApi.values()) { - try (final StreamingHttpClient client = Objects.requireNonNull(clientBuilder.get())) { + try (StreamingHttpClient client = Objects.requireNonNull(clientBuilder.get())) { HttpExecutionStrategy effectiveStrategy = computeClientExecutionStrategy( builderType, builderStrategy, filterStrategy, lbStrategy, cfStrategy, clientApi); From 5cb3a927cc141973eb0adc583724d1635f520707 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Fri, 15 Apr 2022 15:02:54 -0700 Subject: [PATCH 18/54] more test simplification --- .../http/netty/ClientEffectiveStrategyTest.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 45575fb4e5..4d1858a6e5 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -73,6 +73,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -269,6 +270,8 @@ public HttpExecutionStrategy requiredOffloads() { throw new AssertionError("Unexpected clientType"); } + TimeUnit.MILLISECONDS.sleep(150); + // Exercise the client for (final ClientApi clientApi : ClientApi.values()) { try (StreamingHttpClient client = Objects.requireNonNull(clientBuilder.get())) { @@ -326,20 +329,18 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde switch (builderType) { case SINGLE_BUILDER: - return null == merged || defaultStrategy() == merged ? clientApi.strategy() : merged; + return defaultStrategy() == merged ? clientApi.strategy() : merged; case MULTI_BUILDER: - if (null == builder || defaultStrategy() == builder) { - return null == merged || defaultStrategy() == merged ? - clientApi.strategy() : clientApi.strategy().merge(merged); - } - return merged; + return null == builder || defaultStrategy() == builder ? + defaultStrategy() == merged ? clientApi.strategy() : clientApi.strategy().merge(merged) : + merged; case MULTI_DEFAULT_SINGLE_BUILDER: if (defaultStrategy() == merged || (null != builder && !builder.hasOffloads())) { merged = offloadNone(); } return clientApi.strategy().merge(merged); case MULTI_NONE_SINGLE_BUILDER: - return null == merged || defaultStrategy() == merged ? offloadNone() : merged; + return defaultStrategy() == merged ? offloadNone() : merged; default: throw new AssertionError("Unexpected builder type: " + builderType); } From efba308b35355b51ec4e7d9bc724bb24f69450d0 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Fri, 15 Apr 2022 15:38:14 -0700 Subject: [PATCH 19/54] even more review feedback --- ...faultMultiAddressUrlHttpClientBuilder.java | 13 +++----- ...DefaultSingleAddressHttpClientBuilder.java | 16 +++++++--- .../netty/ClientEffectiveStrategyTest.java | 31 +++++++------------ 3 files changed, 28 insertions(+), 32 deletions(-) diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index c331f1d201..da62a45514 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -93,6 +93,8 @@ final class DefaultMultiAddressUrlHttpClientBuilder private static final String HTTPS_SCHEME = HTTPS.toString(); + private static final MultiAddressStrategyWrapper MULTI_ADDRESS_STRATEGY_WRAPPER = new MultiAddressStrategyWrapper(); + private final Function> builderFactory; private final HttpExecutionContextBuilder executionContextBuilder = new HttpExecutionContextBuilder(); @@ -248,8 +250,7 @@ public StreamingHttpClient apply(final UrlKey urlKey) { builder.sslConfig(DEFAULT_CLIENT_SSL_CONFIG); } - MultiAddressStrategyWrapper wrapper = new MultiAddressStrategyWrapper(); - builder.appendClientFilter(wrapper); + builder.appendClientFilter(MULTI_ADDRESS_STRATEGY_WRAPPER); if (singleAddressInitializer != null) { singleAddressInitializer.initialize(urlKey.scheme, urlKey.hostAndPort, builder); @@ -257,19 +258,12 @@ public StreamingHttpClient apply(final UrlKey urlKey) { StreamingHttpClient singleClient = builder.buildStreaming(); - wrapper.singleStrategy = singleClient.executionContext().executionStrategy(); return singleClient; } } private static final class MultiAddressStrategyWrapper implements StreamingHttpClientFilterFactory { - /** - * The strategy used by the single client. Any requests made during the `buildStreaming` will have to use the - * default strategy. - */ - HttpExecutionStrategy singleStrategy = defaultStrategy(); - @Override public StreamingHttpClientFilter create(final FilterableStreamingHttpClient client) { return new StreamingHttpClientFilter(client) { @@ -277,6 +271,7 @@ public StreamingHttpClientFilter create(final FilterableStreamingHttpClient clie protected Single request( final StreamingHttpRequester delegate, final StreamingHttpRequest request) { return defer(() -> { + HttpExecutionStrategy singleStrategy = client.executionContext().executionStrategy(); HttpExecutionStrategy requestStrategy = request.context().getOrDefault(HTTP_EXECUTION_STRATEGY_KEY, defaultStrategy()); HttpExecutionStrategy useStrategy = diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java index ea1f4e2aca..c71be4ce6d 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java @@ -30,6 +30,7 @@ import io.servicetalk.concurrent.api.Executor; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.http.api.DefaultStreamingHttpRequestResponseFactory; +import io.servicetalk.http.api.DelegatingHttpExecutionContext; import io.servicetalk.http.api.FilterableStreamingHttpClient; import io.servicetalk.http.api.FilterableStreamingHttpConnection; import io.servicetalk.http.api.HttpExecutionContext; @@ -208,7 +209,15 @@ public StreamingHttpClient buildStreaming() { private static StreamingHttpClient buildStreaming(final HttpClientBuildContext ctx) { final ReadOnlyHttpClientConfig roConfig = ctx.httpConfig().asReadOnly(); - final HttpExecutionContext executionContext = ctx.builder.executionContextBuilder.build(); + final HttpExecutionContext builderExecutionContext = ctx.builder.executionContextBuilder.build(); + final HttpExecutionStrategy computedStrategy = + ctx.builder.strategyComputation.buildForClient(builderExecutionContext.executionStrategy()); + final HttpExecutionContext executionContext = new DelegatingHttpExecutionContext(builderExecutionContext) { + @Override + public HttpExecutionStrategy executionStrategy() { + return computedStrategy; + } + }; if (roConfig.h2Config() != null && roConfig.hasProxy()) { throw new IllegalStateException("Proxying is not yet supported with HTTP/2"); } @@ -233,7 +242,6 @@ private static StreamingHttpClient buildStreaming(final HttpClientBuildCo connectionFactoryStrategy = connectionFactoryStrategy.merge(proxy.requiredOffloads()); } - final HttpExecutionStrategy executionStrategy = executionContext.executionStrategy(); // closed by the LoadBalancer final ConnectionFactory connectionFactory; final StreamingHttpRequestResponseFactory reqRespFactory = defaultReqRespFactory(roConfig, @@ -290,9 +298,9 @@ ctx.builder.connectionFilterFactory, new AlpnReqRespFactoryFunc( currClientFilterFactory = appendFilter(currClientFilterFactory, ctx.builder.retryingHttpRequesterFilter); } - HttpExecutionStrategy computedStrategy = ctx.builder.strategyComputation.buildForClient(executionStrategy); + LOGGER.debug("Client for {} created with base strategy {} → computed strategy {}", - targetAddress(ctx), executionStrategy, computedStrategy); + targetAddress(ctx), builderExecutionContext.executionStrategy(), computedStrategy); return new FilterableClientToClient(currClientFilterFactory != null ? currClientFilterFactory.create(lbClient, lb.eventStream(), ctx.sdStatus) : lbClient, computedStrategy); diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 4d1858a6e5..d2ac39ef50 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -73,7 +73,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -270,15 +269,13 @@ public HttpExecutionStrategy requiredOffloads() { throw new AssertionError("Unexpected clientType"); } - TimeUnit.MILLISECONDS.sleep(150); - // Exercise the client for (final ClientApi clientApi : ClientApi.values()) { try (StreamingHttpClient client = Objects.requireNonNull(clientBuilder.get())) { HttpExecutionStrategy effectiveStrategy = computeClientExecutionStrategy( builderType, builderStrategy, filterStrategy, lbStrategy, cfStrategy, clientApi); - invokingThreadsRecorder.reset(clientApi, effectiveStrategy); + invokingThreadsRecorder.reset(effectiveStrategy); String responseBody = getResponse(clientApi, client, HttpRequestMethod.GET, requestTarget); assertThat("Unexpected response: " + responseBody, responseBody, is(GREETING)); invokingThreadsRecorder.verifyOffloads(clientApi); @@ -404,24 +401,20 @@ private static final class ClientInvokingThreadRecorder implements StreamingHttp private final ConcurrentMap invokingThreads = new ConcurrentHashMap<>(); private final Queue errors = new LinkedBlockingQueue<>(); - void reset(ClientApi clientApi, HttpExecutionStrategy streamingAsyncStrategy) { + void reset(HttpExecutionStrategy streamingAsyncStrategy) { invokingThreads.clear(); errors.clear(); offloadPoints.clear(); - if (defaultStrategy() != streamingAsyncStrategy) { - // adjust expected offloads for specific execution strategy - if (streamingAsyncStrategy.isSendOffloaded()) { - offloadPoints.add(Send); - } - if (streamingAsyncStrategy.isMetadataReceiveOffloaded()) { - offloadPoints.add(ReceiveMeta); - } - if (streamingAsyncStrategy.isDataReceiveOffloaded()) { - offloadPoints.add(ReceiveData); - } - } else { - // apply default offloads per client api - offloadPoints.addAll(clientApi.offloads()); + + // adjust expected offloads for specific execution strategy + if (streamingAsyncStrategy.isSendOffloaded()) { + offloadPoints.add(Send); + } + if (streamingAsyncStrategy.isMetadataReceiveOffloaded()) { + offloadPoints.add(ReceiveMeta); + } + if (streamingAsyncStrategy.isDataReceiveOffloaded()) { + offloadPoints.add(ReceiveData); } } From 47ec7f8cc3c80da46e06e52c8fc73541725ef2d9 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Mon, 18 Apr 2022 11:07:49 -0700 Subject: [PATCH 20/54] test improvements and suggestions from Idel's review --- .../netty/ClientEffectiveStrategyTest.java | 119 ++++++++++++++---- 1 file changed, 94 insertions(+), 25 deletions(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index d2ac39ef50..edd1efa92d 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -37,7 +37,6 @@ import io.servicetalk.http.api.HttpRequest; import io.servicetalk.http.api.HttpRequestMethod; import io.servicetalk.http.api.HttpResponse; -import io.servicetalk.http.api.HttpServerBuilder; import io.servicetalk.http.api.MultiAddressHttpClientBuilder; import io.servicetalk.http.api.SingleAddressHttpClientBuilder; import io.servicetalk.http.api.StreamingHttpClient; @@ -53,9 +52,12 @@ import io.servicetalk.transport.api.HostAndPort; import io.servicetalk.transport.api.IoThreadFactory; import io.servicetalk.transport.api.ServerContext; +import io.servicetalk.transport.netty.internal.ExecutionContextExtension; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.jupiter.params.ParameterizedTest; @@ -66,6 +68,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Objects; @@ -93,10 +96,23 @@ @Execution(ExecutionMode.CONCURRENT) class ClientEffectiveStrategyTest { + + @RegisterExtension + static final ExecutionContextExtension SERVER_CTX = + ExecutionContextExtension.cached("server-io", "server-executor") + .setClassLevel(true); + + @RegisterExtension + static final ExecutionContextExtension CLIENT_CTX = + ExecutionContextExtension.cached("client-io", "client-executor") + .setClassLevel(true); + private static final String SCHEME = "http"; private static final String PATH = "/"; private static final String GREETING = "Hello"; + private static final String MYNAME = "Nobody"; + /** * Which builder API will be used and where will ExecutionStrategy be initialized. */ @@ -140,19 +156,32 @@ private enum BuilderType { offloadAll(), }; - private static final ServerContext context; + private static ServerContext context; - static { + @BeforeAll + static void initServer() { try { - HttpServerBuilder serverBuilder = HttpServers.forAddress(localAddress(0)); - context = serverBuilder.listenBlocking((ctx, request, responseFactory) -> - responseFactory.ok().payloadBody(ctx.executionContext().bufferAllocator() - .fromAscii(GREETING))).toFuture().get(); + context = HttpServers.forAddress(localAddress(0)) + .ioExecutor(SERVER_CTX.ioExecutor()) + .executor(SERVER_CTX.executor()) + .listenBlocking((ctx, request, responseFactory) -> { + String requestBody = request.payloadBody().toString(StandardCharsets.UTF_8); + String response = GREETING + " " + requestBody; + return responseFactory.ok() + .payloadBody(ctx.executionContext().bufferAllocator().fromUtf8(response)); + }).toFuture().get(); } catch (Throwable all) { throw new AssertionError("Failed to initialize server", all); } } + @AfterAll + static void shutdownServer() throws Exception { + if (context != null) { + context.close(); + } + } + @SuppressWarnings("unused") static Stream casesSupplier() { List arguments = new ArrayList<>(); @@ -176,11 +205,6 @@ static Stream casesSupplier() { return arguments.stream(); } - @AfterAll - static void shutdown() throws Exception { - context.closeGracefully(); - } - @ParameterizedTest(name = "Type={0} builder={1} filter={2} LB={3} CF={4}") @MethodSource("casesSupplier") void clientStrategy(final BuilderType builderType, @@ -244,7 +268,9 @@ public HttpExecutionStrategy requiredOffloads() { case SINGLE_BUILDER: requestTarget = PATH; SingleAddressHttpClientBuilder singleClientBuilder = - HttpClients.forSingleAddress(serverHostAndPort(context)); + HttpClients.forSingleAddress(serverHostAndPort(context)) + .ioExecutor(CLIENT_CTX.ioExecutor()) + .executor(CLIENT_CTX.executor()); // apply initializer immediately initializer.initialize(SCHEME, serverHostAndPort(context), singleClientBuilder); clientBuilder = singleClientBuilder::buildStreaming; @@ -254,7 +280,10 @@ public HttpExecutionStrategy requiredOffloads() { case MULTI_NONE_SINGLE_BUILDER: requestTarget = SCHEME + "://" + serverHostAndPort(context) + PATH; MultiAddressHttpClientBuilder multiClientBuilder = - HttpClients.forMultiAddressUrl().initializer(initializer); + HttpClients.forMultiAddressUrl() + .initializer(initializer) + .ioExecutor(CLIENT_CTX.ioExecutor()) + .executor(CLIENT_CTX.executor()); if (BuilderType.MULTI_BUILDER == builderType && null != builderStrategy) { multiClientBuilder.executionStrategy(builderStrategy); } @@ -270,15 +299,23 @@ public HttpExecutionStrategy requiredOffloads() { } // Exercise the client - for (final ClientApi clientApi : ClientApi.values()) { - try (StreamingHttpClient client = Objects.requireNonNull(clientBuilder.get())) { + try (StreamingHttpClient client = Objects.requireNonNull(clientBuilder.get())) { + for (final ClientApi clientApi : ClientApi.values()) { HttpExecutionStrategy effectiveStrategy = computeClientExecutionStrategy( builderType, builderStrategy, filterStrategy, lbStrategy, cfStrategy, clientApi); invokingThreadsRecorder.reset(effectiveStrategy); - String responseBody = getResponse(clientApi, client, HttpRequestMethod.GET, requestTarget); - assertThat("Unexpected response: " + responseBody, responseBody, is(GREETING)); + String responseBody = getResponse(clientApi, client, HttpRequestMethod.POST, requestTarget, MYNAME); + assertThat("Unexpected response: " + responseBody, + responseBody, is(GREETING + " " + MYNAME)); invokingThreadsRecorder.verifyOffloads(clientApi); + + // Complete a second request because connection factory opening can offload strangely + // invokingThreadsRecorder.reset(effectiveStrategy); + // responseBody = getResponse(clientApi, client, HttpRequestMethod.POST, requestTarget, MYNAME); + // assertThat("Unexpected response: " + responseBody, + // responseBody, is(GREETING + " " + MYNAME)); + // invokingThreadsRecorder.verifyOffloads(clientApi); } } } @@ -350,44 +387,74 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde return null == first ? second : null == second ? first : first.merge(second); } + /** + * Return a string response by performing a request operation. + * + * @param clientApi The client API variation to use for the request. + * @param client The async streaming HTTP client to use for the request. + * @param requestMethod The request method to use. + * @param requestTarget The target of the request. + * @param requestBody The optional body for the request which will be converted to a UTF-8 payload + * @return The response converted to a String from UTF-8 payload. + * @throws Exception for any problems completing the request. + */ private String getResponse(final ClientApi clientApi, final StreamingHttpClient client, - final HttpRequestMethod requestMethod, final String requestTarget) throws Exception { + final HttpRequestMethod requestMethod, final String requestTarget, + final @Nullable String requestBody) throws Exception { switch (clientApi) { case BLOCKING_AGGREGATE: { BlockingHttpClient blockingClient = client.asBlockingClient(); HttpRequest request = blockingClient.newRequest(requestMethod, requestTarget); + if (null != requestBody) { + Buffer requestPayload = blockingClient.executionContext().bufferAllocator().fromUtf8(requestBody); + request.payloadBody(requestPayload); + } HttpResponse response = blockingClient.request(request); - return response.payloadBody().toString(StandardCharsets.US_ASCII); + return response.payloadBody().toString(StandardCharsets.UTF_8); } case BLOCKING_STREAMING: { BlockingStreamingHttpClient blockingStreamingClient = client.asBlockingStreamingClient(); BlockingStreamingHttpRequest request = blockingStreamingClient.newRequest(requestMethod, requestTarget); + if (null != requestBody) { + Buffer requestPayload = + blockingStreamingClient.executionContext().bufferAllocator().fromUtf8(requestBody); + request.payloadBody(Collections.singleton(requestPayload)); + } BlockingStreamingHttpResponse response = blockingStreamingClient.request(request); Supplier supplier = blockingStreamingClient.executionContext().bufferAllocator()::newCompositeBuffer; return StreamSupport.stream(response.payloadBody().spliterator(), false) .reduce((Buffer base, Buffer buffer) -> (base instanceof CompositeBuffer ? ((CompositeBuffer) base) : supplier.get().addBuffer(base)).addBuffer(buffer)) - .map(buffer -> buffer.toString(StandardCharsets.US_ASCII)) + .map(buffer -> buffer.toString(StandardCharsets.UTF_8)) .orElseThrow(() -> new AssertionError("No payload in response")); } case ASYNC_AGGREGATE: { HttpClient httpClient = client.asClient(); HttpRequest request = httpClient.newRequest(requestMethod, requestTarget); + if (null != requestBody) { + Buffer requestPayload = httpClient.executionContext().bufferAllocator().fromUtf8(requestBody); + request.payloadBody(requestPayload); + } HttpResponse response = httpClient.request(request).toFuture().get(); - return response.payloadBody().toString(StandardCharsets.US_ASCII); + return response.payloadBody().toString(StandardCharsets.UTF_8); } case ASYNC_STREAMING: { StreamingHttpRequest request = client.newRequest(requestMethod, requestTarget); + if (null != requestBody) { + Buffer requestPayload = + client.executionContext().bufferAllocator().fromUtf8(requestBody); + request.payloadBody(Publisher.from(requestPayload)); + } CompositeBuffer responsePayload = client.request(request) .flatMap(resp -> resp.payloadBody().collect(() -> client.executionContext().bufferAllocator().newCompositeBuffer(), CompositeBuffer::addBuffer)) .toFuture().get(); - return responsePayload.toString(StandardCharsets.US_ASCII); + return responsePayload.toString(StandardCharsets.UTF_8); } default: @@ -446,11 +513,13 @@ void recordThread(final ClientOffloadPoint offloadPoint) { boolean ioThread = IoThreadFactory.IoThread.isIoThread(current); if (offloadPoints.contains(offloadPoint)) { if (ioThread) { - errors.add(new AssertionError("Expected offloaded thread at " + offloadPoint)); + errors.add(new AssertionError("Expected offloaded thread at " + offloadPoint + + ", but was running on " + current.getName())); } } else { if (!ioThread) { - errors.add(new AssertionError("Expected ioThread at " + offloadPoint)); + errors.add(new AssertionError("Expected IoThread at " + offloadPoint + + ", but was running on " + current.getName())); } } return ioThread ? "eventLoop" : "offloaded"; From 8f0819d89a87b903f5bccdc363d097551066e2ba Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Tue, 19 Apr 2022 12:32:28 -0700 Subject: [PATCH 21/54] allow send offload to execute on application thread --- .../netty/ClientEffectiveStrategyTest.java | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index edd1efa92d..772c182134 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -464,6 +464,7 @@ private String getResponse(final ClientApi clientApi, final StreamingHttpClient private static final class ClientInvokingThreadRecorder implements StreamingHttpClientFilterFactory { + private Thread applicationThread; private final EnumSet offloadPoints = EnumSet.noneOf(ClientOffloadPoint.class); private final ConcurrentMap invokingThreads = new ConcurrentHashMap<>(); private final Queue errors = new LinkedBlockingQueue<>(); @@ -472,6 +473,7 @@ void reset(HttpExecutionStrategy streamingAsyncStrategy) { invokingThreads.clear(); errors.clear(); offloadPoints.clear(); + applicationThread = Thread.currentThread(); // adjust expected offloads for specific execution strategy if (streamingAsyncStrategy.isSendOffloaded()) { @@ -510,19 +512,22 @@ protected Single request(final StreamingHttpRequester del void recordThread(final ClientOffloadPoint offloadPoint) { invokingThreads.compute(offloadPoint, (ClientOffloadPoint offload, String recorded) -> { Thread current = Thread.currentThread(); + boolean appThread = (current == applicationThread); boolean ioThread = IoThreadFactory.IoThread.isIoThread(current); - if (offloadPoints.contains(offloadPoint)) { - if (ioThread) { - errors.add(new AssertionError("Expected offloaded thread at " + offloadPoint + - ", but was running on " + current.getName())); - } - } else { - if (!ioThread) { - errors.add(new AssertionError("Expected IoThread at " + offloadPoint + - ", but was running on " + current.getName())); + if (Send == offloadPoint && !appThread) { + if (offloadPoints.contains(offloadPoint)) { + if (ioThread) { + errors.add(new AssertionError("Expected offloaded thread at " + offloadPoint + + ", but was running on " + current.getName())); + } + } else { + if (!ioThread) { + errors.add(new AssertionError("Expected IoThread/Application at " + offloadPoint + + ", but was running on " + current.getName())); + } } } - return ioThread ? "eventLoop" : "offloaded"; + return ioThread ? "eventLoop" : appThread ? "application" : "offloaded"; }); } From 330824e90b450c6f2420dd774fff38629041b1a8 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Tue, 19 Apr 2022 12:43:52 -0700 Subject: [PATCH 22/54] allow send offload to execute on application thread --- .../servicetalk/http/netty/ClientEffectiveStrategyTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 772c182134..3ad1f2fc10 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -512,9 +512,11 @@ protected Single request(final StreamingHttpRequester del void recordThread(final ClientOffloadPoint offloadPoint) { invokingThreads.compute(offloadPoint, (ClientOffloadPoint offload, String recorded) -> { Thread current = Thread.currentThread(); - boolean appThread = (current == applicationThread); + boolean appThread = current == applicationThread; boolean ioThread = IoThreadFactory.IoThread.isIoThread(current); - if (Send == offloadPoint && !appThread) { + if (appThread && Send == offloadPoint) { + // We allow the app thread to be used for send. + } else { if (offloadPoints.contains(offloadPoint)) { if (ioThread) { errors.add(new AssertionError("Expected offloaded thread at " + offloadPoint + From 97453d973e51ee72bd1e2b1a762b9d91b986bf2c Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Tue, 19 Apr 2022 12:50:55 -0700 Subject: [PATCH 23/54] restore second request --- .../http/netty/ClientEffectiveStrategyTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 3ad1f2fc10..f042b6f2dc 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -311,11 +311,11 @@ public HttpExecutionStrategy requiredOffloads() { invokingThreadsRecorder.verifyOffloads(clientApi); // Complete a second request because connection factory opening can offload strangely - // invokingThreadsRecorder.reset(effectiveStrategy); - // responseBody = getResponse(clientApi, client, HttpRequestMethod.POST, requestTarget, MYNAME); - // assertThat("Unexpected response: " + responseBody, - // responseBody, is(GREETING + " " + MYNAME)); - // invokingThreadsRecorder.verifyOffloads(clientApi); + invokingThreadsRecorder.reset(effectiveStrategy); + responseBody = getResponse(clientApi, client, HttpRequestMethod.POST, requestTarget, MYNAME); + assertThat("Unexpected response: " + responseBody, + responseBody, is(GREETING + " " + MYNAME)); + invokingThreadsRecorder.verifyOffloads(clientApi); } } } From c7d00bd2cde793e9beb8410842803bf93c9e5d45 Mon Sep 17 00:00:00 2001 From: Idel Pivnitskiy Date: Wed, 20 Apr 2022 13:53:31 -0700 Subject: [PATCH 24/54] enhance visibility in tests --- .../netty/ClientEffectiveStrategyTest.java | 114 ++++++++---------- 1 file changed, 50 insertions(+), 64 deletions(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index f042b6f2dc..86cc64ce4a 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -31,6 +31,7 @@ import io.servicetalk.http.api.FilterableStreamingHttpConnection; import io.servicetalk.http.api.FilterableStreamingHttpLoadBalancedConnection; import io.servicetalk.http.api.HttpClient; +import io.servicetalk.http.api.HttpContextKeys; import io.servicetalk.http.api.HttpExecutionStrategies; import io.servicetalk.http.api.HttpExecutionStrategy; import io.servicetalk.http.api.HttpLoadBalancerFactory; @@ -68,7 +69,6 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Objects; @@ -91,8 +91,11 @@ import static io.servicetalk.test.resources.TestUtils.assertNoAsyncErrors; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; +import static java.util.Collections.singleton; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.Matchers.not; @Execution(ExecutionMode.CONCURRENT) class ClientEffectiveStrategyTest { @@ -108,10 +111,7 @@ class ClientEffectiveStrategyTest { .setClassLevel(true); private static final String SCHEME = "http"; - private static final String PATH = "/"; - private static final String GREETING = "Hello"; - - private static final String MYNAME = "Nobody"; + private static final String PATH = TestServiceStreaming.SVC_ECHO; /** * Which builder API will be used and where will ExecutionStrategy be initialized. @@ -164,12 +164,7 @@ static void initServer() { context = HttpServers.forAddress(localAddress(0)) .ioExecutor(SERVER_CTX.ioExecutor()) .executor(SERVER_CTX.executor()) - .listenBlocking((ctx, request, responseFactory) -> { - String requestBody = request.payloadBody().toString(StandardCharsets.UTF_8); - String response = GREETING + " " + requestBody; - return responseFactory.ok() - .payloadBody(ctx.executionContext().bufferAllocator().fromUtf8(response)); - }).toFuture().get(); + .listenStreamingAndAwait(new TestServiceStreaming()); } catch (Throwable all) { throw new AssertionError("Failed to initialize server", all); } @@ -305,17 +300,18 @@ public HttpExecutionStrategy requiredOffloads() { builderType, builderStrategy, filterStrategy, lbStrategy, cfStrategy, clientApi); invokingThreadsRecorder.reset(effectiveStrategy); - String responseBody = getResponse(clientApi, client, HttpRequestMethod.POST, requestTarget, MYNAME); - assertThat("Unexpected response: " + responseBody, - responseBody, is(GREETING + " " + MYNAME)); - invokingThreadsRecorder.verifyOffloads(clientApi); + String responseBody = getResponse(clientApi, client, HttpRequestMethod.POST, requestTarget); + assertThat("Unexpected response: " + responseBody, responseBody, is(not(emptyString()))); + invokingThreadsRecorder.verifyOffloads(clientApi, client.executionContext().executionStrategy(), + responseBody); // Complete a second request because connection factory opening can offload strangely invokingThreadsRecorder.reset(effectiveStrategy); - responseBody = getResponse(clientApi, client, HttpRequestMethod.POST, requestTarget, MYNAME); + responseBody = getResponse(clientApi, client, HttpRequestMethod.POST, requestTarget); assertThat("Unexpected response: " + responseBody, - responseBody, is(GREETING + " " + MYNAME)); - invokingThreadsRecorder.verifyOffloads(clientApi); + responseBody, is(not(emptyString()))); + invokingThreadsRecorder.verifyOffloads(clientApi, client.executionContext().executionStrategy(), + responseBody); } } } @@ -380,8 +376,9 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde } } - private static @Nullable HttpExecutionStrategy mergeStrategies(@Nullable HttpExecutionStrategy first, - @Nullable HttpExecutionStrategy second) { + @Nullable + private static HttpExecutionStrategy mergeStrategies(@Nullable HttpExecutionStrategy first, + @Nullable HttpExecutionStrategy second) { first = offloadNever() != first ? defaultStrategy() != first ? first : offloadAll() : offloadNone(); second = offloadNever() != second ? defaultStrategy() != second ? second : offloadAll() : offloadNone(); return null == first ? second : null == second ? first : first.merge(second); @@ -394,61 +391,50 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde * @param client The async streaming HTTP client to use for the request. * @param requestMethod The request method to use. * @param requestTarget The target of the request. - * @param requestBody The optional body for the request which will be converted to a UTF-8 payload * @return The response converted to a String from UTF-8 payload. * @throws Exception for any problems completing the request. */ - private String getResponse(final ClientApi clientApi, final StreamingHttpClient client, - final HttpRequestMethod requestMethod, final String requestTarget, - final @Nullable String requestBody) throws Exception { + private static String getResponse(final ClientApi clientApi, final StreamingHttpClient client, + final HttpRequestMethod requestMethod, + final String requestTarget) throws Exception { switch (clientApi) { case BLOCKING_AGGREGATE: { BlockingHttpClient blockingClient = client.asBlockingClient(); - HttpRequest request = blockingClient.newRequest(requestMethod, requestTarget); - if (null != requestBody) { - Buffer requestPayload = blockingClient.executionContext().bufferAllocator().fromUtf8(requestBody); - request.payloadBody(requestPayload); - } + HttpRequest request = blockingClient.newRequest(requestMethod, requestTarget) + .payloadBody(blockingClient.executionContext().bufferAllocator() + .fromUtf8(blockingClient.executionContext().executionStrategy().toString())); HttpResponse response = blockingClient.request(request); return response.payloadBody().toString(StandardCharsets.UTF_8); } case BLOCKING_STREAMING: { BlockingStreamingHttpClient blockingStreamingClient = client.asBlockingStreamingClient(); - BlockingStreamingHttpRequest request = blockingStreamingClient.newRequest(requestMethod, requestTarget); - if (null != requestBody) { - Buffer requestPayload = - blockingStreamingClient.executionContext().bufferAllocator().fromUtf8(requestBody); - request.payloadBody(Collections.singleton(requestPayload)); - } + BlockingStreamingHttpRequest request = blockingStreamingClient.newRequest(requestMethod, requestTarget) + .payloadBody(singleton(blockingStreamingClient.executionContext().bufferAllocator() + .fromUtf8(blockingStreamingClient.executionContext().executionStrategy().toString()))); BlockingStreamingHttpResponse response = blockingStreamingClient.request(request); Supplier supplier = blockingStreamingClient.executionContext().bufferAllocator()::newCompositeBuffer; return StreamSupport.stream(response.payloadBody().spliterator(), false) .reduce((Buffer base, Buffer buffer) -> (base instanceof CompositeBuffer ? - ((CompositeBuffer) base) : supplier.get().addBuffer(base)).addBuffer(buffer)) + (CompositeBuffer) base : supplier.get().addBuffer(base)).addBuffer(buffer)) .map(buffer -> buffer.toString(StandardCharsets.UTF_8)) .orElseThrow(() -> new AssertionError("No payload in response")); } case ASYNC_AGGREGATE: { HttpClient httpClient = client.asClient(); - HttpRequest request = httpClient.newRequest(requestMethod, requestTarget); - if (null != requestBody) { - Buffer requestPayload = httpClient.executionContext().bufferAllocator().fromUtf8(requestBody); - request.payloadBody(requestPayload); - } + HttpRequest request = httpClient.newRequest(requestMethod, requestTarget) + .payloadBody(httpClient.executionContext().bufferAllocator() + .fromUtf8(httpClient.executionContext().executionStrategy().toString())); HttpResponse response = httpClient.request(request).toFuture().get(); return response.payloadBody().toString(StandardCharsets.UTF_8); } case ASYNC_STREAMING: { - StreamingHttpRequest request = client.newRequest(requestMethod, requestTarget); - if (null != requestBody) { - Buffer requestPayload = - client.executionContext().bufferAllocator().fromUtf8(requestBody); - request.payloadBody(Publisher.from(requestPayload)); - } + StreamingHttpRequest request = client.newRequest(requestMethod, requestTarget) + .payloadBody(Publisher.from(client.executionContext().bufferAllocator() + .fromUtf8(client.executionContext().executionStrategy().toString()))); CompositeBuffer responsePayload = client.request(request) .flatMap(resp -> resp.payloadBody().collect(() -> client.executionContext().bufferAllocator().newCompositeBuffer(), @@ -464,7 +450,7 @@ private String getResponse(final ClientApi clientApi, final StreamingHttpClient private static final class ClientInvokingThreadRecorder implements StreamingHttpClientFilterFactory { - private Thread applicationThread; + private Thread applicationThread = Thread.currentThread(); private final EnumSet offloadPoints = EnumSet.noneOf(ClientOffloadPoint.class); private final ConcurrentMap invokingThreads = new ConcurrentHashMap<>(); private final Queue errors = new LinkedBlockingQueue<>(); @@ -500,16 +486,19 @@ public StreamingHttpClientFilter create(final FilterableStreamingHttpClient clie @Override protected Single request(final StreamingHttpRequester delegate, final StreamingHttpRequest request) { + final HttpExecutionStrategy clientStrategy = delegate.executionContext().executionStrategy(); + final HttpExecutionStrategy requestStrategy = request.context().get(HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY); return delegate.request(request.transformPayloadBody(payload -> - payload.beforeRequest(__ -> recordThread(Send)))) - .beforeOnSuccess(__ -> recordThread(ReceiveMeta)) + payload.beforeRequest(__ -> recordThread(Send, clientStrategy, requestStrategy)))) + .beforeOnSuccess(__ -> recordThread(ReceiveMeta, clientStrategy, requestStrategy)) .map(resp -> resp.transformPayloadBody(payload -> - payload.beforeOnNext(__ -> recordThread(ReceiveData)))); + payload.beforeOnNext(__ -> recordThread(ReceiveData, clientStrategy, requestStrategy)))); } }; } - void recordThread(final ClientOffloadPoint offloadPoint) { + void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStrategy clientStrategy, + @Nullable final HttpExecutionStrategy requestStrategy) { invokingThreads.compute(offloadPoint, (ClientOffloadPoint offload, String recorded) -> { Thread current = Thread.currentThread(); boolean appThread = current == applicationThread; @@ -520,12 +509,15 @@ void recordThread(final ClientOffloadPoint offloadPoint) { if (offloadPoints.contains(offloadPoint)) { if (ioThread) { errors.add(new AssertionError("Expected offloaded thread at " + offloadPoint + - ", but was running on " + current.getName())); + ", but was running on " + current.getName() + ". clientStrategy=" + clientStrategy + + ", requestStrategy=" + requestStrategy)); } } else { if (!ioThread) { - errors.add(new AssertionError("Expected IoThread/Application at " + offloadPoint + - ", but was running on " + current.getName())); + errors.add(new AssertionError("Expected IoThread or " + + applicationThread.getName() + " at " + offloadPoint + + ", but was running on an offloading executor thread: " + current.getName() + + ". clientStrategy=" + clientStrategy + ", requestStrategy=" + requestStrategy)); } } } @@ -533,8 +525,9 @@ void recordThread(final ClientOffloadPoint offloadPoint) { }); } - public void verifyOffloads(ClientApi clientApi) { - assertNoAsyncErrors("API=" + clientApi + " Async Errors! See suppressed", errors); + public void verifyOffloads(ClientApi clientApi, HttpExecutionStrategy clientStrategy, String apiStrategy) { + assertNoAsyncErrors("API=" + clientApi + ", clientStrategy=" + clientStrategy + ", apiStrategy=" + + apiStrategy + ". Async Errors! See suppressed", errors); assertThat("Unexpected offload points recorded. " + invokingThreads, invokingThreads.size(), Matchers.is(ClientOffloadPoint.values().length)); } @@ -569,12 +562,9 @@ private enum ClientApi { ASYNC_AGGREGATE(EnumSet.of(ReceiveData)), ASYNC_STREAMING(EnumSet.allOf(ClientOffloadPoint.class)); - private final EnumSet offloads; private final HttpExecutionStrategy strategy; ClientApi(EnumSet offloads) { - this.offloads = offloads; - HttpExecutionStrategies.Builder builder = HttpExecutionStrategies.customStrategyBuilder(); if (offloads.contains(Send)) { @@ -590,10 +580,6 @@ private enum ClientApi { this.strategy = builder.build(); } - public EnumSet offloads() { - return offloads; - } - public HttpExecutionStrategy strategy() { return strategy; } From 522e7e6a05d24d5bb093acd15febaec8a515397e Mon Sep 17 00:00:00 2001 From: Idel Pivnitskiy Date: Wed, 20 Apr 2022 14:59:56 -0700 Subject: [PATCH 25/54] more debugging --- .../internal/DefaultContextMap.java | 13 ++++++ .../http/api/HttpApiConversions.java | 7 --- ...faultMultiAddressUrlHttpClientBuilder.java | 2 + .../netty/ClientEffectiveStrategyTest.java | 44 ++++++++++++++----- .../src/main/resources/log4j2.xml | 1 + 5 files changed, 49 insertions(+), 18 deletions(-) diff --git a/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java b/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java index 254347e77a..7ca304b0a7 100644 --- a/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java +++ b/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java @@ -18,7 +18,11 @@ import io.servicetalk.context.api.ContextMap; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.BiPredicate; import java.util.function.Function; import javax.annotation.Nullable; @@ -33,6 +37,7 @@ public final class DefaultContextMap implements ContextMap { private final HashMap, Object> theMap; + private final ConcurrentMap, List> stacktraces = new ConcurrentHashMap<>(); /** * Creates a new instance. @@ -83,6 +88,8 @@ public T getOrDefault(final Key key, final T defaultValue) { @Override @SuppressWarnings("unchecked") public T put(final Key key, @Nullable final T value) { + List list = stacktraces.computeIfAbsent(key, __ -> new CopyOnWriteArrayList<>()); + list.add(new Throwable("put")); return (T) theMap.put(requireNonNull(key, "key"), value); } @@ -90,9 +97,15 @@ public T put(final Key key, @Nullable final T value) { @Override @SuppressWarnings("unchecked") public T putIfAbsent(final Key key, @Nullable final T value) { + List list = stacktraces.computeIfAbsent(key, __ -> new CopyOnWriteArrayList<>()); + list.add(new Throwable("putIfAbsent")); return (T) theMap.putIfAbsent(requireNonNull(key, "key"), value); } + public List stacktrace(Key key) { + return stacktraces.get(key); + } + @Nullable @Override @SuppressWarnings("unchecked") diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java index dbc8b2d02c..e98ae05428 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpApiConversions.java @@ -19,8 +19,6 @@ import io.servicetalk.http.api.StreamingHttpClientToBlockingStreamingHttpClient.ReservedStreamingHttpConnectionToBlockingStreaming; import io.servicetalk.http.api.StreamingHttpClientToHttpClient.ReservedStreamingHttpConnectionToReservedHttpConnection; -import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; - /** * Conversion routines to {@link StreamingHttpService}. */ @@ -446,9 +444,4 @@ public static HttpService toHttpService(StreamingHttpService service) { public static BlockingStreamingHttpService toBlockingStreamingHttpService(StreamingHttpService service) { return new StreamingHttpServiceToBlockingStreamingHttpService(service); } - - static HttpExecutionStrategy requestStrategy(HttpRequestMetaData metaData, HttpExecutionStrategy fallback) { - final HttpExecutionStrategy strategy = metaData.context().get(HTTP_EXECUTION_STRATEGY_KEY); - return strategy != null ? strategy : fallback; - } } diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index da62a45514..99ff1a0579 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -284,6 +284,8 @@ protected Single request( requestStrategy.merge(singleStrategy); if (useStrategy != requestStrategy) { + LOGGER.debug("Request strategy {} changes to {}. SingleAddressClient strategy: {}", + requestStrategy, useStrategy, singleStrategy); request.context().put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); } return delegate.request(request); diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 86cc64ce4a..ee366589d4 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -23,6 +23,8 @@ import io.servicetalk.client.api.ServiceDiscovererEvent; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; +import io.servicetalk.concurrent.internal.DefaultContextMap; +import io.servicetalk.context.api.ContextMap; import io.servicetalk.http.api.BlockingHttpClient; import io.servicetalk.http.api.BlockingStreamingHttpClient; import io.servicetalk.http.api.BlockingStreamingHttpRequest; @@ -31,7 +33,6 @@ import io.servicetalk.http.api.FilterableStreamingHttpConnection; import io.servicetalk.http.api.FilterableStreamingHttpLoadBalancedConnection; import io.servicetalk.http.api.HttpClient; -import io.servicetalk.http.api.HttpContextKeys; import io.servicetalk.http.api.HttpExecutionStrategies; import io.servicetalk.http.api.HttpExecutionStrategy; import io.servicetalk.http.api.HttpLoadBalancerFactory; @@ -81,6 +82,7 @@ import java.util.stream.StreamSupport; import javax.annotation.Nullable; +import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.HttpExecutionStrategies.offloadAll; import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNever; @@ -487,18 +489,22 @@ public StreamingHttpClientFilter create(final FilterableStreamingHttpClient clie protected Single request(final StreamingHttpRequester delegate, final StreamingHttpRequest request) { final HttpExecutionStrategy clientStrategy = delegate.executionContext().executionStrategy(); - final HttpExecutionStrategy requestStrategy = request.context().get(HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY); + final HttpExecutionStrategy requestStrategy = request.context().get(HTTP_EXECUTION_STRATEGY_KEY); return delegate.request(request.transformPayloadBody(payload -> - payload.beforeRequest(__ -> recordThread(Send, clientStrategy, requestStrategy)))) - .beforeOnSuccess(__ -> recordThread(ReceiveMeta, clientStrategy, requestStrategy)) - .map(resp -> resp.transformPayloadBody(payload -> - payload.beforeOnNext(__ -> recordThread(ReceiveData, clientStrategy, requestStrategy)))); + payload.beforeRequest(__ -> recordThread(Send, clientStrategy, requestStrategy, + request.context())))) + .beforeOnSuccess(__ -> recordThread(ReceiveMeta, clientStrategy, requestStrategy, + request.context())) + .map(resp -> resp.transformPayloadBody(payload -> payload + .beforeOnNext(__ -> recordThread(ReceiveData, clientStrategy, requestStrategy, + request.context())))); } }; } void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStrategy clientStrategy, - @Nullable final HttpExecutionStrategy requestStrategy) { + @Nullable final HttpExecutionStrategy requestStrategy, + final ContextMap reqCtx) { invokingThreads.compute(offloadPoint, (ClientOffloadPoint offload, String recorded) -> { Thread current = Thread.currentThread(); boolean appThread = current == applicationThread; @@ -508,16 +514,32 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra } else { if (offloadPoints.contains(offloadPoint)) { if (ioThread) { - errors.add(new AssertionError("Expected offloaded thread at " + offloadPoint + + final AssertionError e = new AssertionError("Expected offloaded thread at " + offloadPoint + ", but was running on " + current.getName() + ". clientStrategy=" + clientStrategy + - ", requestStrategy=" + requestStrategy)); + ", requestStrategy=" + requestStrategy); + if (reqCtx instanceof DefaultContextMap) { + List stacktrace = ((DefaultContextMap) reqCtx) + .stacktrace(HTTP_EXECUTION_STRATEGY_KEY); + for (Throwable t : stacktrace) { + e.addSuppressed(t); + } + } + errors.add(e); } } else { if (!ioThread) { - errors.add(new AssertionError("Expected IoThread or " + + final AssertionError e = new AssertionError("Expected IoThread or " + applicationThread.getName() + " at " + offloadPoint + ", but was running on an offloading executor thread: " + current.getName() + - ". clientStrategy=" + clientStrategy + ", requestStrategy=" + requestStrategy)); + ". clientStrategy=" + clientStrategy + ", requestStrategy=" + requestStrategy); + if (reqCtx instanceof DefaultContextMap) { + List stacktrace = ((DefaultContextMap) reqCtx) + .stacktrace(HTTP_EXECUTION_STRATEGY_KEY); + for (Throwable t : stacktrace) { + e.addSuppressed(t); + } + } + errors.add(e); } } } diff --git a/servicetalk-test-resources/src/main/resources/log4j2.xml b/servicetalk-test-resources/src/main/resources/log4j2.xml index 689b84662d..bf12238cb1 100644 --- a/servicetalk-test-resources/src/main/resources/log4j2.xml +++ b/servicetalk-test-resources/src/main/resources/log4j2.xml @@ -30,6 +30,7 @@ + From 36def497bd15605cf158912ef2f0db79e9b3eaef Mon Sep 17 00:00:00 2001 From: Idel Pivnitskiy Date: Wed, 20 Apr 2022 16:21:46 -0700 Subject: [PATCH 26/54] more debugging 2 --- .../internal/DefaultContextMap.java | 6 +- .../netty/ClientEffectiveStrategyTest.java | 58 +++++++++---------- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java b/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java index 7ca304b0a7..f98e95fdef 100644 --- a/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java +++ b/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java @@ -89,7 +89,8 @@ public T getOrDefault(final Key key, final T defaultValue) { @SuppressWarnings("unchecked") public T put(final Key key, @Nullable final T value) { List list = stacktraces.computeIfAbsent(key, __ -> new CopyOnWriteArrayList<>()); - list.add(new Throwable("put")); + list.add(new Throwable("put on " + Thread.currentThread().getName() + " at " + System.nanoTime() + + " for " + Integer.toHexString(hashCode()))); return (T) theMap.put(requireNonNull(key, "key"), value); } @@ -98,7 +99,8 @@ public T put(final Key key, @Nullable final T value) { @SuppressWarnings("unchecked") public T putIfAbsent(final Key key, @Nullable final T value) { List list = stacktraces.computeIfAbsent(key, __ -> new CopyOnWriteArrayList<>()); - list.add(new Throwable("putIfAbsent")); + list.add(new Throwable("putIfAbsent on " + Thread.currentThread().getName() + " at " + System.nanoTime() + + " for " + Integer.toHexString(hashCode()))); return (T) theMap.putIfAbsent(requireNonNull(key, "key"), value); } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index ee366589d4..c6a4271e2a 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -27,17 +27,15 @@ import io.servicetalk.context.api.ContextMap; import io.servicetalk.http.api.BlockingHttpClient; import io.servicetalk.http.api.BlockingStreamingHttpClient; -import io.servicetalk.http.api.BlockingStreamingHttpRequest; import io.servicetalk.http.api.BlockingStreamingHttpResponse; import io.servicetalk.http.api.FilterableStreamingHttpClient; import io.servicetalk.http.api.FilterableStreamingHttpConnection; import io.servicetalk.http.api.FilterableStreamingHttpLoadBalancedConnection; import io.servicetalk.http.api.HttpClient; +import io.servicetalk.http.api.HttpExecutionContext; import io.servicetalk.http.api.HttpExecutionStrategies; import io.servicetalk.http.api.HttpExecutionStrategy; import io.servicetalk.http.api.HttpLoadBalancerFactory; -import io.servicetalk.http.api.HttpRequest; -import io.servicetalk.http.api.HttpRequestMethod; import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.api.MultiAddressHttpClientBuilder; import io.servicetalk.http.api.SingleAddressHttpClientBuilder; @@ -67,7 +65,6 @@ import org.junit.jupiter.params.provider.MethodSource; import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.EnumSet; @@ -93,6 +90,7 @@ import static io.servicetalk.test.resources.TestUtils.assertNoAsyncErrors; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Collections.singleton; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -302,14 +300,14 @@ public HttpExecutionStrategy requiredOffloads() { builderType, builderStrategy, filterStrategy, lbStrategy, cfStrategy, clientApi); invokingThreadsRecorder.reset(effectiveStrategy); - String responseBody = getResponse(clientApi, client, HttpRequestMethod.POST, requestTarget); + String responseBody = getResponse(clientApi, client, requestTarget); assertThat("Unexpected response: " + responseBody, responseBody, is(not(emptyString()))); invokingThreadsRecorder.verifyOffloads(clientApi, client.executionContext().executionStrategy(), responseBody); // Complete a second request because connection factory opening can offload strangely invokingThreadsRecorder.reset(effectiveStrategy); - responseBody = getResponse(clientApi, client, HttpRequestMethod.POST, requestTarget); + responseBody = getResponse(clientApi, client, requestTarget); assertThat("Unexpected response: " + responseBody, responseBody, is(not(emptyString()))); invokingThreadsRecorder.verifyOffloads(clientApi, client.executionContext().executionStrategy(), @@ -391,58 +389,50 @@ private static HttpExecutionStrategy mergeStrategies(@Nullable HttpExecutionStra * * @param clientApi The client API variation to use for the request. * @param client The async streaming HTTP client to use for the request. - * @param requestMethod The request method to use. * @param requestTarget The target of the request. * @return The response converted to a String from UTF-8 payload. * @throws Exception for any problems completing the request. */ private static String getResponse(final ClientApi clientApi, final StreamingHttpClient client, - final HttpRequestMethod requestMethod, final String requestTarget) throws Exception { switch (clientApi) { case BLOCKING_AGGREGATE: { BlockingHttpClient blockingClient = client.asBlockingClient(); - HttpRequest request = blockingClient.newRequest(requestMethod, requestTarget) - .payloadBody(blockingClient.executionContext().bufferAllocator() - .fromUtf8(blockingClient.executionContext().executionStrategy().toString())); - HttpResponse response = blockingClient.request(request); - return response.payloadBody().toString(StandardCharsets.UTF_8); + HttpResponse response = blockingClient.request(blockingClient.post(requestTarget) + .payloadBody(content(blockingClient.executionContext()))); + return response.payloadBody().toString(UTF_8); } case BLOCKING_STREAMING: { BlockingStreamingHttpClient blockingStreamingClient = client.asBlockingStreamingClient(); - BlockingStreamingHttpRequest request = blockingStreamingClient.newRequest(requestMethod, requestTarget) - .payloadBody(singleton(blockingStreamingClient.executionContext().bufferAllocator() - .fromUtf8(blockingStreamingClient.executionContext().executionStrategy().toString()))); - BlockingStreamingHttpResponse response = blockingStreamingClient.request(request); + BlockingStreamingHttpResponse response = blockingStreamingClient.request( + blockingStreamingClient.post(requestTarget) + .payloadBody(singleton(content(blockingStreamingClient.executionContext())))); Supplier supplier = blockingStreamingClient.executionContext().bufferAllocator()::newCompositeBuffer; return StreamSupport.stream(response.payloadBody().spliterator(), false) .reduce((Buffer base, Buffer buffer) -> (base instanceof CompositeBuffer ? (CompositeBuffer) base : supplier.get().addBuffer(base)).addBuffer(buffer)) - .map(buffer -> buffer.toString(StandardCharsets.UTF_8)) + .map(buffer -> buffer.toString(UTF_8)) .orElseThrow(() -> new AssertionError("No payload in response")); } case ASYNC_AGGREGATE: { HttpClient httpClient = client.asClient(); - HttpRequest request = httpClient.newRequest(requestMethod, requestTarget) - .payloadBody(httpClient.executionContext().bufferAllocator() - .fromUtf8(httpClient.executionContext().executionStrategy().toString())); - HttpResponse response = httpClient.request(request).toFuture().get(); - return response.payloadBody().toString(StandardCharsets.UTF_8); + HttpResponse response = httpClient.request(httpClient.post(requestTarget) + .payloadBody(content(httpClient.executionContext()))) + .toFuture().get(); + return response.payloadBody().toString(UTF_8); } case ASYNC_STREAMING: { - StreamingHttpRequest request = client.newRequest(requestMethod, requestTarget) - .payloadBody(Publisher.from(client.executionContext().bufferAllocator() - .fromUtf8(client.executionContext().executionStrategy().toString()))); - CompositeBuffer responsePayload = client.request(request) + CompositeBuffer responsePayload = client.request(client.post(requestTarget) + .payloadBody(Publisher.from(content(client.executionContext())))) .flatMap(resp -> resp.payloadBody().collect(() -> client.executionContext().bufferAllocator().newCompositeBuffer(), CompositeBuffer::addBuffer)) .toFuture().get(); - return responsePayload.toString(StandardCharsets.UTF_8); + return responsePayload.toString(UTF_8); } default: @@ -450,6 +440,10 @@ private static String getResponse(final ClientApi clientApi, final StreamingHttp } } + private static Buffer content(HttpExecutionContext ctx) { + return ctx.bufferAllocator().fromUtf8(ctx.executionStrategy().toString()); + } + private static final class ClientInvokingThreadRecorder implements StreamingHttpClientFilterFactory { private Thread applicationThread = Thread.currentThread(); @@ -516,7 +510,8 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra if (ioThread) { final AssertionError e = new AssertionError("Expected offloaded thread at " + offloadPoint + ", but was running on " + current.getName() + ". clientStrategy=" + clientStrategy + - ", requestStrategy=" + requestStrategy); + ", requestStrategy=" + requestStrategy + + ", requestContext=" + Integer.toHexString(reqCtx.hashCode())); if (reqCtx instanceof DefaultContextMap) { List stacktrace = ((DefaultContextMap) reqCtx) .stacktrace(HTTP_EXECUTION_STRATEGY_KEY); @@ -531,7 +526,8 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra final AssertionError e = new AssertionError("Expected IoThread or " + applicationThread.getName() + " at " + offloadPoint + ", but was running on an offloading executor thread: " + current.getName() + - ". clientStrategy=" + clientStrategy + ", requestStrategy=" + requestStrategy); + ". clientStrategy=" + clientStrategy + ", requestStrategy=" + requestStrategy + + ", requestContext=" + Integer.toHexString(reqCtx.hashCode())); if (reqCtx instanceof DefaultContextMap) { List stacktrace = ((DefaultContextMap) reqCtx) .stacktrace(HTTP_EXECUTION_STRATEGY_KEY); @@ -543,7 +539,7 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra } } } - return ioThread ? "eventLoop" : appThread ? "application" : "offloaded"; + return ioThread ? "eventLoop" : (appThread ? "application" : "offloaded"); }); } From 9e6530d90a0186a1a84e70de0cdfd45d494538f3 Mon Sep 17 00:00:00 2001 From: Idel Pivnitskiy Date: Wed, 20 Apr 2022 17:08:37 -0700 Subject: [PATCH 27/54] add timestamps --- .../internal/DefaultContextMap.java | 14 +++++++---- .../netty/ClientEffectiveStrategyTest.java | 23 ++++++++++++------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java b/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java index f98e95fdef..ede00fd62a 100644 --- a/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java +++ b/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java @@ -89,8 +89,9 @@ public T getOrDefault(final Key key, final T defaultValue) { @SuppressWarnings("unchecked") public T put(final Key key, @Nullable final T value) { List list = stacktraces.computeIfAbsent(key, __ -> new CopyOnWriteArrayList<>()); - list.add(new Throwable("put on " + Thread.currentThread().getName() + " at " + System.nanoTime() + - " for " + Integer.toHexString(hashCode()))); + list.add(new Throwable("put(" + value + ") on " + Thread.currentThread().getName() + + " at " + System.nanoTime() + + " for " + Integer.toHexString(System.identityHashCode(this)))); return (T) theMap.put(requireNonNull(key, "key"), value); } @@ -98,10 +99,13 @@ public T put(final Key key, @Nullable final T value) { @Override @SuppressWarnings("unchecked") public T putIfAbsent(final Key key, @Nullable final T value) { + final T oldVal = (T) theMap.putIfAbsent(requireNonNull(key, "key"), value); List list = stacktraces.computeIfAbsent(key, __ -> new CopyOnWriteArrayList<>()); - list.add(new Throwable("putIfAbsent on " + Thread.currentThread().getName() + " at " + System.nanoTime() + - " for " + Integer.toHexString(hashCode()))); - return (T) theMap.putIfAbsent(requireNonNull(key, "key"), value); + list.add(new Throwable("putIfAbsent(" + value + ")=" + oldVal + + " on " + Thread.currentThread().getName() + + " at " + System.nanoTime() + + " for " + Integer.toHexString(System.identityHashCode(this)))); + return oldVal; } public List stacktrace(Key key) { diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index c6a4271e2a..d8161adc85 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -299,19 +299,22 @@ public HttpExecutionStrategy requiredOffloads() { HttpExecutionStrategy effectiveStrategy = computeClientExecutionStrategy( builderType, builderStrategy, filterStrategy, lbStrategy, cfStrategy, clientApi); + long startTime = System.nanoTime(); invokingThreadsRecorder.reset(effectiveStrategy); String responseBody = getResponse(clientApi, client, requestTarget); + long endTime = System.nanoTime(); assertThat("Unexpected response: " + responseBody, responseBody, is(not(emptyString()))); invokingThreadsRecorder.verifyOffloads(clientApi, client.executionContext().executionStrategy(), - responseBody); + responseBody, startTime, endTime); // Complete a second request because connection factory opening can offload strangely + startTime = System.nanoTime(); invokingThreadsRecorder.reset(effectiveStrategy); responseBody = getResponse(clientApi, client, requestTarget); - assertThat("Unexpected response: " + responseBody, - responseBody, is(not(emptyString()))); + endTime = System.nanoTime(); + assertThat("Unexpected response: " + responseBody, responseBody, is(not(emptyString()))); invokingThreadsRecorder.verifyOffloads(clientApi, client.executionContext().executionStrategy(), - responseBody); + responseBody, startTime, endTime); } } } @@ -511,7 +514,8 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra final AssertionError e = new AssertionError("Expected offloaded thread at " + offloadPoint + ", but was running on " + current.getName() + ". clientStrategy=" + clientStrategy + ", requestStrategy=" + requestStrategy + - ", requestContext=" + Integer.toHexString(reqCtx.hashCode())); + ", timestamp=" + System.nanoTime() + + ", requestContext=" + Integer.toHexString(System.identityHashCode(reqCtx))); if (reqCtx instanceof DefaultContextMap) { List stacktrace = ((DefaultContextMap) reqCtx) .stacktrace(HTTP_EXECUTION_STRATEGY_KEY); @@ -527,7 +531,8 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra applicationThread.getName() + " at " + offloadPoint + ", but was running on an offloading executor thread: " + current.getName() + ". clientStrategy=" + clientStrategy + ", requestStrategy=" + requestStrategy + - ", requestContext=" + Integer.toHexString(reqCtx.hashCode())); + ", timestamp=" + System.nanoTime() + + ", requestContext=" + Integer.toHexString(System.identityHashCode(reqCtx))); if (reqCtx instanceof DefaultContextMap) { List stacktrace = ((DefaultContextMap) reqCtx) .stacktrace(HTTP_EXECUTION_STRATEGY_KEY); @@ -543,9 +548,11 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra }); } - public void verifyOffloads(ClientApi clientApi, HttpExecutionStrategy clientStrategy, String apiStrategy) { + public void verifyOffloads(ClientApi clientApi, HttpExecutionStrategy clientStrategy, String apiStrategy, + long startTime, long endTime) { assertNoAsyncErrors("API=" + clientApi + ", clientStrategy=" + clientStrategy + ", apiStrategy=" + - apiStrategy + ". Async Errors! See suppressed", errors); + apiStrategy + ", startTime=" + startTime + ", endTime=" + endTime + + ". Async Errors! See suppressed", errors); assertThat("Unexpected offload points recorded. " + invokingThreads, invokingThreads.size(), Matchers.is(ClientOffloadPoint.values().length)); } From 2c7e1dfc91377be99125a18388642811dc3f6f56 Mon Sep 17 00:00:00 2001 From: Idel Pivnitskiy Date: Wed, 20 Apr 2022 17:47:33 -0700 Subject: [PATCH 28/54] change order or ClientApi --- .../io/servicetalk/http/netty/ClientEffectiveStrategyTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index d8161adc85..2063d3790e 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -582,8 +582,8 @@ public ExecutionStrategy requiredOffloads() { * Which API flavor will be used. */ private enum ClientApi { - BLOCKING_AGGREGATE(EnumSet.noneOf(ClientOffloadPoint.class)), BLOCKING_STREAMING(EnumSet.of(Send)), + BLOCKING_AGGREGATE(EnumSet.noneOf(ClientOffloadPoint.class)), ASYNC_AGGREGATE(EnumSet.of(ReceiveData)), ASYNC_STREAMING(EnumSet.allOf(ClientOffloadPoint.class)); From 5d950944591390c665a08f971f8c4a935cb5f9a4 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Wed, 20 Apr 2022 11:56:56 -0700 Subject: [PATCH 29/54] only allow application thread if not offloading --- .../servicetalk/http/netty/ClientEffectiveStrategyTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 2063d3790e..341c82b250 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -506,8 +506,8 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra Thread current = Thread.currentThread(); boolean appThread = current == applicationThread; boolean ioThread = IoThreadFactory.IoThread.isIoThread(current); - if (appThread && Send == offloadPoint) { - // We allow the app thread to be used for send. + if (appThread && Send == offloadPoint && !offloadPoints.contains(Send)) { + // We allow the app thread to be used for send if not offloaded } else { if (offloadPoints.contains(offloadPoint)) { if (ioThread) { From be76acf4221d50e10cd02f9bdfa4e14a8e890229 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Thu, 21 Apr 2022 08:49:25 -0700 Subject: [PATCH 30/54] use local for simplification --- ...faultMultiAddressUrlHttpClientBuilder.java | 62 +++---- .../http/netty/FilterableClientToClient.java | 10 +- .../netty/ClientEffectiveStrategyTest.java | 153 ++++++++---------- 3 files changed, 95 insertions(+), 130 deletions(-) diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index 99ff1a0579..0d52aeacb9 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -17,13 +17,10 @@ import io.servicetalk.buffer.api.BufferAllocator; import io.servicetalk.client.api.ClientGroup; -import io.servicetalk.concurrent.api.AsyncCloseable; import io.servicetalk.concurrent.api.Completable; import io.servicetalk.concurrent.api.CompositeCloseable; import io.servicetalk.concurrent.api.Executor; -import io.servicetalk.concurrent.api.ListenableAsyncCloseable; import io.servicetalk.concurrent.api.Single; -import io.servicetalk.concurrent.api.internal.SubscribableCompletable; import io.servicetalk.http.api.DefaultHttpHeadersFactory; import io.servicetalk.http.api.DefaultStreamingHttpRequestResponseFactory; import io.servicetalk.http.api.FilterableReservedStreamingHttpConnection; @@ -63,9 +60,7 @@ import static io.netty.handler.codec.http.HttpScheme.HTTP; import static io.netty.handler.codec.http.HttpScheme.HTTPS; import static io.servicetalk.concurrent.api.AsyncCloseables.newCompositeCloseable; -import static io.servicetalk.concurrent.api.AsyncCloseables.toListenableAsyncCloseable; import static io.servicetalk.concurrent.api.Single.defer; -import static io.servicetalk.concurrent.internal.SubscriberUtils.deliverCompleteFromSource; import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.HttpExecutionStrategies.offloadAll; @@ -115,13 +110,11 @@ public StreamingHttpClient buildStreaming() { final CompositeCloseable closeables = newCompositeCloseable(); try { final HttpExecutionContext executionContext = executionContextBuilder.build(); - final ClientFactory clientFactory = new ClientFactory(builderFactory, - executionContext, - singleAddressInitializer); - final CachingKeyFactory keyFactory = closeables.prepend(new CachingKeyFactory()); + final ClientFactory clientFactory = + new ClientFactory(builderFactory, executionContext, singleAddressInitializer); final HttpHeadersFactory headersFactory = this.headersFactory; FilterableStreamingHttpClient urlClient = closeables.prepend( - new StreamingUrlHttpClient(executionContext, clientFactory, keyFactory, + new StreamingUrlHttpClient(executionContext, clientFactory, new DefaultStreamingHttpRequestResponseFactory(executionContext.bufferAllocator(), headersFactory != null ? headersFactory : DefaultHttpHeadersFactory.INSTANCE, HTTP_1_1))); @@ -141,7 +134,7 @@ public StreamingHttpClient buildStreaming() { /** * Returns a cached {@link UrlKey} or creates a new one based on {@link StreamingHttpRequest} information. */ - private static final class CachingKeyFactory implements AsyncCloseable { + private static final class CachingKeyFactory { private final ConcurrentMap urlKeyCache = new ConcurrentHashMap<>(); @@ -163,12 +156,11 @@ public UrlKey get(final HttpRequestMetaData metaData) throws MalformedURLExcepti final int port = parsedPort >= 0 ? parsedPort : (HTTPS_SCHEME.equalsIgnoreCase(scheme) ? HTTPS : HTTP).port(); + // Adjust the URI in the request to relative "Single Address Client" form. metaData.requestTarget(absoluteToRelativeFormRequestTarget(metaData.requestTarget(), scheme, host)); final String key = scheme + ':' + host + ':' + port; - final UrlKey urlKey = urlKeyCache.get(key); - return urlKey != null ? urlKey : urlKeyCache.computeIfAbsent(key, ignore -> - new UrlKey(scheme, HostAndPort.of(host, port))); + return urlKeyCache.computeIfAbsent(key, ignore -> new UrlKey(scheme, HostAndPort.of(host, port))); } // This code is similar to io.servicetalk.http.utils.RedirectSingle#absoluteToRelativeFormRequestTarget @@ -179,20 +171,6 @@ private static String absoluteToRelativeFormRequestTarget(final String requestTa final int relativeReferenceIdx = requestTarget.indexOf('/', fromIndex); return relativeReferenceIdx < 0 ? "/" : requestTarget.substring(relativeReferenceIdx); } - - @Override - public Completable closeAsync() { - // Make a best effort to clear the map. Note that we don't attempt to resolve race conditions between - // closing the client and in flight requests adding Keys to the map. We also don't attempt to remove - // from the map if a request fails, or a request is made after the client is closed. - return new SubscribableCompletable() { - @Override - protected void handleSubscribe(final Subscriber subscriber) { - urlKeyCache.clear(); - deliverCompleteFromSource(subscriber); - } - }; - } } private static final class UrlKey { @@ -221,6 +199,11 @@ public boolean equals(final Object o) { public int hashCode() { return 31 * hostAndPort.hashCode() + scheme.hashCode(); } + + @Override + public String toString() { + return scheme + ":" + hostAndPort; + } } private static final class ClientFactory implements Function { @@ -258,6 +241,10 @@ public StreamingHttpClient apply(final UrlKey urlKey) { StreamingHttpClient singleClient = builder.buildStreaming(); + LOGGER.debug("Single-address client created for {} with base strategy {} and strategy {}", + urlKey, + executionContext.executionStrategy(), singleClient.executionContext().executionStrategy()); + return singleClient; } } @@ -304,21 +291,14 @@ private static final class StreamingUrlHttpClient implements FilterableStreaming private final HttpExecutionContext executionContext; private final StreamingHttpRequestResponseFactory reqRespFactory; private final ClientGroup group; - private final CachingKeyFactory keyFactory; - private final ListenableAsyncCloseable closeable; + private final CachingKeyFactory keyFactory = new CachingKeyFactory(); StreamingUrlHttpClient(final HttpExecutionContext executionContext, final Function clientFactory, - final CachingKeyFactory keyFactory, final StreamingHttpRequestResponseFactory reqRespFactory) { - this.reqRespFactory = requireNonNull(reqRespFactory); + this.reqRespFactory = reqRespFactory; this.group = ClientGroup.from(clientFactory); - this.keyFactory = keyFactory; - CompositeCloseable compositeCloseable = newCompositeCloseable(); - compositeCloseable.append(group); - compositeCloseable.append(keyFactory); - closeable = toListenableAsyncCloseable(compositeCloseable); - this.executionContext = requireNonNull(executionContext); + this.executionContext = executionContext; } private FilterableStreamingHttpClient selectClient(HttpRequestMetaData metaData) throws MalformedURLException { @@ -360,17 +340,17 @@ public StreamingHttpResponseFactory httpResponseFactory() { @Override public Completable onClose() { - return closeable.onClose(); + return group.onClose(); } @Override public Completable closeAsync() { - return closeable.closeAsync(); + return group.closeAsync(); } @Override public Completable closeAsyncGracefully() { - return closeable.closeAsyncGracefully(); + return group.closeAsyncGracefully(); } @Override diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/FilterableClientToClient.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/FilterableClientToClient.java index 3051128d98..3f24c5f9d5 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/FilterableClientToClient.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/FilterableClientToClient.java @@ -86,22 +86,22 @@ public Single request(final StreamingHttpRequest request) @Override public Single reserveConnection(final HttpRequestMetaData metaData) { return Single.defer(() -> { - metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, executionContext().executionStrategy()); - + HttpExecutionStrategy clientstrategy = executionContext().executionStrategy(); + metaData.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, clientstrategy); return client.reserveConnection(metaData).map(rc -> new ReservedStreamingHttpConnection() { @Override public ReservedHttpConnection asConnection() { - return toReservedConnection(this, executionContext.executionStrategy()); + return toReservedConnection(this, clientstrategy); } @Override public ReservedBlockingStreamingHttpConnection asBlockingStreamingConnection() { - return toReservedBlockingStreamingConnection(this, executionContext.executionStrategy()); + return toReservedBlockingStreamingConnection(this, clientstrategy); } @Override public ReservedBlockingHttpConnection asBlockingConnection() { - return toReservedBlockingConnection(this, executionContext.executionStrategy()); + return toReservedBlockingConnection(this, clientstrategy); } @Override diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 341c82b250..e129579c52 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -23,19 +23,20 @@ import io.servicetalk.client.api.ServiceDiscovererEvent; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; -import io.servicetalk.concurrent.internal.DefaultContextMap; -import io.servicetalk.context.api.ContextMap; import io.servicetalk.http.api.BlockingHttpClient; import io.servicetalk.http.api.BlockingStreamingHttpClient; +import io.servicetalk.http.api.BlockingStreamingHttpRequest; import io.servicetalk.http.api.BlockingStreamingHttpResponse; import io.servicetalk.http.api.FilterableStreamingHttpClient; import io.servicetalk.http.api.FilterableStreamingHttpConnection; import io.servicetalk.http.api.FilterableStreamingHttpLoadBalancedConnection; import io.servicetalk.http.api.HttpClient; -import io.servicetalk.http.api.HttpExecutionContext; +import io.servicetalk.http.api.HttpContextKeys; import io.servicetalk.http.api.HttpExecutionStrategies; import io.servicetalk.http.api.HttpExecutionStrategy; import io.servicetalk.http.api.HttpLoadBalancerFactory; +import io.servicetalk.http.api.HttpRequest; +import io.servicetalk.http.api.HttpRequestMethod; import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.api.MultiAddressHttpClientBuilder; import io.servicetalk.http.api.SingleAddressHttpClientBuilder; @@ -65,6 +66,7 @@ import org.junit.jupiter.params.provider.MethodSource; import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.EnumSet; @@ -79,7 +81,6 @@ import java.util.stream.StreamSupport; import javax.annotation.Nullable; -import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.HttpExecutionStrategies.offloadAll; import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNever; @@ -90,12 +91,9 @@ import static io.servicetalk.test.resources.TestUtils.assertNoAsyncErrors; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; -import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Collections.singleton; -import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.emptyString; -import static org.hamcrest.Matchers.not; @Execution(ExecutionMode.CONCURRENT) class ClientEffectiveStrategyTest { @@ -111,7 +109,8 @@ class ClientEffectiveStrategyTest { .setClassLevel(true); private static final String SCHEME = "http"; - private static final String PATH = TestServiceStreaming.SVC_ECHO; + private static final String PATH = "/"; + private static final String GREETING = "Hello"; /** * Which builder API will be used and where will ExecutionStrategy be initialized. @@ -164,7 +163,12 @@ static void initServer() { context = HttpServers.forAddress(localAddress(0)) .ioExecutor(SERVER_CTX.ioExecutor()) .executor(SERVER_CTX.executor()) - .listenStreamingAndAwait(new TestServiceStreaming()); + .listenBlocking((ctx, request, responseFactory) -> { + String requestBody = request.payloadBody().toString(StandardCharsets.UTF_8); + String response = GREETING + " " + requestBody; + return responseFactory.ok() + .payloadBody(ctx.executionContext().bufferAllocator().fromUtf8(response)); + }).toFuture().get(); } catch (Throwable all) { throw new AssertionError("Failed to initialize server", all); } @@ -299,22 +303,18 @@ public HttpExecutionStrategy requiredOffloads() { HttpExecutionStrategy effectiveStrategy = computeClientExecutionStrategy( builderType, builderStrategy, filterStrategy, lbStrategy, cfStrategy, clientApi); - long startTime = System.nanoTime(); invokingThreadsRecorder.reset(effectiveStrategy); - String responseBody = getResponse(clientApi, client, requestTarget); - long endTime = System.nanoTime(); - assertThat("Unexpected response: " + responseBody, responseBody, is(not(emptyString()))); + String responseBody = getResponse(clientApi, client, HttpRequestMethod.POST, requestTarget); + assertThat("Unexpected response: " + responseBody, responseBody, startsWith(GREETING)); invokingThreadsRecorder.verifyOffloads(clientApi, client.executionContext().executionStrategy(), - responseBody, startTime, endTime); + responseBody); // Complete a second request because connection factory opening can offload strangely - startTime = System.nanoTime(); invokingThreadsRecorder.reset(effectiveStrategy); - responseBody = getResponse(clientApi, client, requestTarget); - endTime = System.nanoTime(); - assertThat("Unexpected response: " + responseBody, responseBody, is(not(emptyString()))); + responseBody = getResponse(clientApi, client, HttpRequestMethod.POST, requestTarget); + assertThat("Unexpected response: " + responseBody, responseBody, startsWith(GREETING)); invokingThreadsRecorder.verifyOffloads(clientApi, client.executionContext().executionStrategy(), - responseBody, startTime, endTime); + responseBody); } } } @@ -379,9 +379,8 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde } } - @Nullable - private static HttpExecutionStrategy mergeStrategies(@Nullable HttpExecutionStrategy first, - @Nullable HttpExecutionStrategy second) { + private static @Nullable HttpExecutionStrategy mergeStrategies(@Nullable HttpExecutionStrategy first, + @Nullable HttpExecutionStrategy second) { first = offloadNever() != first ? defaultStrategy() != first ? first : offloadAll() : offloadNone(); second = offloadNever() != second ? defaultStrategy() != second ? second : offloadAll() : offloadNone(); return null == first ? second : null == second ? first : first.merge(second); @@ -392,50 +391,57 @@ private static HttpExecutionStrategy mergeStrategies(@Nullable HttpExecutionStra * * @param clientApi The client API variation to use for the request. * @param client The async streaming HTTP client to use for the request. + * @param requestMethod The request method to use. * @param requestTarget The target of the request. * @return The response converted to a String from UTF-8 payload. * @throws Exception for any problems completing the request. */ - private static String getResponse(final ClientApi clientApi, final StreamingHttpClient client, - final String requestTarget) throws Exception { + private String getResponse(final ClientApi clientApi, final StreamingHttpClient client, + final HttpRequestMethod requestMethod, final String requestTarget) throws Exception { switch (clientApi) { case BLOCKING_AGGREGATE: { BlockingHttpClient blockingClient = client.asBlockingClient(); - HttpResponse response = blockingClient.request(blockingClient.post(requestTarget) - .payloadBody(content(blockingClient.executionContext()))); - return response.payloadBody().toString(UTF_8); + HttpRequest request = blockingClient.newRequest(requestMethod, requestTarget) + .payloadBody(blockingClient.executionContext().bufferAllocator() + .fromUtf8(blockingClient.executionContext().executionStrategy().toString())); + HttpResponse response = blockingClient.request(request); + return response.payloadBody().toString(StandardCharsets.UTF_8); } case BLOCKING_STREAMING: { BlockingStreamingHttpClient blockingStreamingClient = client.asBlockingStreamingClient(); - BlockingStreamingHttpResponse response = blockingStreamingClient.request( - blockingStreamingClient.post(requestTarget) - .payloadBody(singleton(content(blockingStreamingClient.executionContext())))); + BlockingStreamingHttpRequest request = blockingStreamingClient.newRequest(requestMethod, requestTarget) + .payloadBody(singleton(blockingStreamingClient.executionContext().bufferAllocator() + .fromUtf8(blockingStreamingClient.executionContext().executionStrategy().toString()))); + BlockingStreamingHttpResponse response = blockingStreamingClient.request(request); Supplier supplier = blockingStreamingClient.executionContext().bufferAllocator()::newCompositeBuffer; return StreamSupport.stream(response.payloadBody().spliterator(), false) .reduce((Buffer base, Buffer buffer) -> (base instanceof CompositeBuffer ? - (CompositeBuffer) base : supplier.get().addBuffer(base)).addBuffer(buffer)) - .map(buffer -> buffer.toString(UTF_8)) + ((CompositeBuffer) base) : supplier.get().addBuffer(base)).addBuffer(buffer)) + .map(buffer -> buffer.toString(StandardCharsets.UTF_8)) .orElseThrow(() -> new AssertionError("No payload in response")); } case ASYNC_AGGREGATE: { HttpClient httpClient = client.asClient(); - HttpResponse response = httpClient.request(httpClient.post(requestTarget) - .payloadBody(content(httpClient.executionContext()))) - .toFuture().get(); - return response.payloadBody().toString(UTF_8); + HttpRequest request = httpClient.newRequest(requestMethod, requestTarget) + .payloadBody(httpClient.executionContext().bufferAllocator() + .fromUtf8(httpClient.executionContext().executionStrategy().toString())); + HttpResponse response = httpClient.request(request).toFuture().get(); + return response.payloadBody().toString(StandardCharsets.UTF_8); } case ASYNC_STREAMING: { - CompositeBuffer responsePayload = client.request(client.post(requestTarget) - .payloadBody(Publisher.from(content(client.executionContext())))) + StreamingHttpRequest request = client.newRequest(requestMethod, requestTarget) + .payloadBody(Publisher.from(client.executionContext().bufferAllocator() + .fromUtf8(client.executionContext().executionStrategy().toString()))); + CompositeBuffer responsePayload = client.request(request) .flatMap(resp -> resp.payloadBody().collect(() -> client.executionContext().bufferAllocator().newCompositeBuffer(), CompositeBuffer::addBuffer)) .toFuture().get(); - return responsePayload.toString(UTF_8); + return responsePayload.toString(StandardCharsets.UTF_8); } default: @@ -443,13 +449,9 @@ private static String getResponse(final ClientApi clientApi, final StreamingHttp } } - private static Buffer content(HttpExecutionContext ctx) { - return ctx.bufferAllocator().fromUtf8(ctx.executionStrategy().toString()); - } - private static final class ClientInvokingThreadRecorder implements StreamingHttpClientFilterFactory { - private Thread applicationThread = Thread.currentThread(); + private Thread applicationThread; private final EnumSet offloadPoints = EnumSet.noneOf(ClientOffloadPoint.class); private final ConcurrentMap invokingThreads = new ConcurrentHashMap<>(); private final Queue errors = new LinkedBlockingQueue<>(); @@ -486,22 +488,20 @@ public StreamingHttpClientFilter create(final FilterableStreamingHttpClient clie protected Single request(final StreamingHttpRequester delegate, final StreamingHttpRequest request) { final HttpExecutionStrategy clientStrategy = delegate.executionContext().executionStrategy(); - final HttpExecutionStrategy requestStrategy = request.context().get(HTTP_EXECUTION_STRATEGY_KEY); + final HttpExecutionStrategy requestStrategy = + request.context().get(HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY); return delegate.request(request.transformPayloadBody(payload -> - payload.beforeRequest(__ -> recordThread(Send, clientStrategy, requestStrategy, - request.context())))) - .beforeOnSuccess(__ -> recordThread(ReceiveMeta, clientStrategy, requestStrategy, - request.context())) - .map(resp -> resp.transformPayloadBody(payload -> payload - .beforeOnNext(__ -> recordThread(ReceiveData, clientStrategy, requestStrategy, - request.context())))); + payload.beforeRequest(__ -> recordThread(Send, clientStrategy, requestStrategy)))) + .beforeOnSuccess(__ -> recordThread(ReceiveMeta, clientStrategy, requestStrategy)) + .map(resp -> resp.transformPayloadBody(payload -> + payload.beforeOnNext(__ -> + recordThread(ReceiveData, clientStrategy, requestStrategy)))); } }; } void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStrategy clientStrategy, - @Nullable final HttpExecutionStrategy requestStrategy, - final ContextMap reqCtx) { + @Nullable final HttpExecutionStrategy requestStrategy) { invokingThreads.compute(offloadPoint, (ClientOffloadPoint offload, String recorded) -> { Thread current = Thread.currentThread(); boolean appThread = current == applicationThread; @@ -511,48 +511,26 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra } else { if (offloadPoints.contains(offloadPoint)) { if (ioThread) { - final AssertionError e = new AssertionError("Expected offloaded thread at " + offloadPoint + + errors.add(new AssertionError("Expected offloaded thread at " + offloadPoint + ", but was running on " + current.getName() + ". clientStrategy=" + clientStrategy + - ", requestStrategy=" + requestStrategy + - ", timestamp=" + System.nanoTime() + - ", requestContext=" + Integer.toHexString(System.identityHashCode(reqCtx))); - if (reqCtx instanceof DefaultContextMap) { - List stacktrace = ((DefaultContextMap) reqCtx) - .stacktrace(HTTP_EXECUTION_STRATEGY_KEY); - for (Throwable t : stacktrace) { - e.addSuppressed(t); - } - } - errors.add(e); + ", requestStrategy=" + requestStrategy)); } } else { if (!ioThread) { - final AssertionError e = new AssertionError("Expected IoThread or " + + errors.add(new AssertionError("Expected IoThread or " + applicationThread.getName() + " at " + offloadPoint + ", but was running on an offloading executor thread: " + current.getName() + - ". clientStrategy=" + clientStrategy + ", requestStrategy=" + requestStrategy + - ", timestamp=" + System.nanoTime() + - ", requestContext=" + Integer.toHexString(System.identityHashCode(reqCtx))); - if (reqCtx instanceof DefaultContextMap) { - List stacktrace = ((DefaultContextMap) reqCtx) - .stacktrace(HTTP_EXECUTION_STRATEGY_KEY); - for (Throwable t : stacktrace) { - e.addSuppressed(t); - } - } - errors.add(e); + ". clientStrategy=" + clientStrategy + ", requestStrategy=" + requestStrategy)); } } } - return ioThread ? "eventLoop" : (appThread ? "application" : "offloaded"); + return ioThread ? "eventLoop" : appThread ? "application" : "offloaded"; }); } - public void verifyOffloads(ClientApi clientApi, HttpExecutionStrategy clientStrategy, String apiStrategy, - long startTime, long endTime) { + public void verifyOffloads(ClientApi clientApi, HttpExecutionStrategy clientStrategy, String apiStrategy) { assertNoAsyncErrors("API=" + clientApi + ", clientStrategy=" + clientStrategy + ", apiStrategy=" + - apiStrategy + ", startTime=" + startTime + ", endTime=" + endTime + - ". Async Errors! See suppressed", errors); + apiStrategy + ". Async Errors! See suppressed", errors); assertThat("Unexpected offload points recorded. " + invokingThreads, invokingThreads.size(), Matchers.is(ClientOffloadPoint.values().length)); } @@ -587,9 +565,12 @@ private enum ClientApi { ASYNC_AGGREGATE(EnumSet.of(ReceiveData)), ASYNC_STREAMING(EnumSet.allOf(ClientOffloadPoint.class)); + private final EnumSet offloads; private final HttpExecutionStrategy strategy; ClientApi(EnumSet offloads) { + this.offloads = offloads; + HttpExecutionStrategies.Builder builder = HttpExecutionStrategies.customStrategyBuilder(); if (offloads.contains(Send)) { @@ -605,6 +586,10 @@ private enum ClientApi { this.strategy = builder.build(); } + public EnumSet offloads() { + return offloads; + } + public HttpExecutionStrategy strategy() { return strategy; } From 1419244232b9feedfdf259daf30f5b008ddb8a28 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Thu, 21 Apr 2022 08:57:49 -0700 Subject: [PATCH 31/54] revert unintended changes --- ...faultMultiAddressUrlHttpClientBuilder.java | 62 ++++--- .../netty/ClientEffectiveStrategyTest.java | 153 ++++++++++-------- 2 files changed, 125 insertions(+), 90 deletions(-) diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index 0d52aeacb9..99ff1a0579 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -17,10 +17,13 @@ import io.servicetalk.buffer.api.BufferAllocator; import io.servicetalk.client.api.ClientGroup; +import io.servicetalk.concurrent.api.AsyncCloseable; import io.servicetalk.concurrent.api.Completable; import io.servicetalk.concurrent.api.CompositeCloseable; import io.servicetalk.concurrent.api.Executor; +import io.servicetalk.concurrent.api.ListenableAsyncCloseable; import io.servicetalk.concurrent.api.Single; +import io.servicetalk.concurrent.api.internal.SubscribableCompletable; import io.servicetalk.http.api.DefaultHttpHeadersFactory; import io.servicetalk.http.api.DefaultStreamingHttpRequestResponseFactory; import io.servicetalk.http.api.FilterableReservedStreamingHttpConnection; @@ -60,7 +63,9 @@ import static io.netty.handler.codec.http.HttpScheme.HTTP; import static io.netty.handler.codec.http.HttpScheme.HTTPS; import static io.servicetalk.concurrent.api.AsyncCloseables.newCompositeCloseable; +import static io.servicetalk.concurrent.api.AsyncCloseables.toListenableAsyncCloseable; import static io.servicetalk.concurrent.api.Single.defer; +import static io.servicetalk.concurrent.internal.SubscriberUtils.deliverCompleteFromSource; import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.HttpExecutionStrategies.offloadAll; @@ -110,11 +115,13 @@ public StreamingHttpClient buildStreaming() { final CompositeCloseable closeables = newCompositeCloseable(); try { final HttpExecutionContext executionContext = executionContextBuilder.build(); - final ClientFactory clientFactory = - new ClientFactory(builderFactory, executionContext, singleAddressInitializer); + final ClientFactory clientFactory = new ClientFactory(builderFactory, + executionContext, + singleAddressInitializer); + final CachingKeyFactory keyFactory = closeables.prepend(new CachingKeyFactory()); final HttpHeadersFactory headersFactory = this.headersFactory; FilterableStreamingHttpClient urlClient = closeables.prepend( - new StreamingUrlHttpClient(executionContext, clientFactory, + new StreamingUrlHttpClient(executionContext, clientFactory, keyFactory, new DefaultStreamingHttpRequestResponseFactory(executionContext.bufferAllocator(), headersFactory != null ? headersFactory : DefaultHttpHeadersFactory.INSTANCE, HTTP_1_1))); @@ -134,7 +141,7 @@ public StreamingHttpClient buildStreaming() { /** * Returns a cached {@link UrlKey} or creates a new one based on {@link StreamingHttpRequest} information. */ - private static final class CachingKeyFactory { + private static final class CachingKeyFactory implements AsyncCloseable { private final ConcurrentMap urlKeyCache = new ConcurrentHashMap<>(); @@ -156,11 +163,12 @@ public UrlKey get(final HttpRequestMetaData metaData) throws MalformedURLExcepti final int port = parsedPort >= 0 ? parsedPort : (HTTPS_SCHEME.equalsIgnoreCase(scheme) ? HTTPS : HTTP).port(); - // Adjust the URI in the request to relative "Single Address Client" form. metaData.requestTarget(absoluteToRelativeFormRequestTarget(metaData.requestTarget(), scheme, host)); final String key = scheme + ':' + host + ':' + port; - return urlKeyCache.computeIfAbsent(key, ignore -> new UrlKey(scheme, HostAndPort.of(host, port))); + final UrlKey urlKey = urlKeyCache.get(key); + return urlKey != null ? urlKey : urlKeyCache.computeIfAbsent(key, ignore -> + new UrlKey(scheme, HostAndPort.of(host, port))); } // This code is similar to io.servicetalk.http.utils.RedirectSingle#absoluteToRelativeFormRequestTarget @@ -171,6 +179,20 @@ private static String absoluteToRelativeFormRequestTarget(final String requestTa final int relativeReferenceIdx = requestTarget.indexOf('/', fromIndex); return relativeReferenceIdx < 0 ? "/" : requestTarget.substring(relativeReferenceIdx); } + + @Override + public Completable closeAsync() { + // Make a best effort to clear the map. Note that we don't attempt to resolve race conditions between + // closing the client and in flight requests adding Keys to the map. We also don't attempt to remove + // from the map if a request fails, or a request is made after the client is closed. + return new SubscribableCompletable() { + @Override + protected void handleSubscribe(final Subscriber subscriber) { + urlKeyCache.clear(); + deliverCompleteFromSource(subscriber); + } + }; + } } private static final class UrlKey { @@ -199,11 +221,6 @@ public boolean equals(final Object o) { public int hashCode() { return 31 * hostAndPort.hashCode() + scheme.hashCode(); } - - @Override - public String toString() { - return scheme + ":" + hostAndPort; - } } private static final class ClientFactory implements Function { @@ -241,10 +258,6 @@ public StreamingHttpClient apply(final UrlKey urlKey) { StreamingHttpClient singleClient = builder.buildStreaming(); - LOGGER.debug("Single-address client created for {} with base strategy {} and strategy {}", - urlKey, - executionContext.executionStrategy(), singleClient.executionContext().executionStrategy()); - return singleClient; } } @@ -291,14 +304,21 @@ private static final class StreamingUrlHttpClient implements FilterableStreaming private final HttpExecutionContext executionContext; private final StreamingHttpRequestResponseFactory reqRespFactory; private final ClientGroup group; - private final CachingKeyFactory keyFactory = new CachingKeyFactory(); + private final CachingKeyFactory keyFactory; + private final ListenableAsyncCloseable closeable; StreamingUrlHttpClient(final HttpExecutionContext executionContext, final Function clientFactory, + final CachingKeyFactory keyFactory, final StreamingHttpRequestResponseFactory reqRespFactory) { - this.reqRespFactory = reqRespFactory; + this.reqRespFactory = requireNonNull(reqRespFactory); this.group = ClientGroup.from(clientFactory); - this.executionContext = executionContext; + this.keyFactory = keyFactory; + CompositeCloseable compositeCloseable = newCompositeCloseable(); + compositeCloseable.append(group); + compositeCloseable.append(keyFactory); + closeable = toListenableAsyncCloseable(compositeCloseable); + this.executionContext = requireNonNull(executionContext); } private FilterableStreamingHttpClient selectClient(HttpRequestMetaData metaData) throws MalformedURLException { @@ -340,17 +360,17 @@ public StreamingHttpResponseFactory httpResponseFactory() { @Override public Completable onClose() { - return group.onClose(); + return closeable.onClose(); } @Override public Completable closeAsync() { - return group.closeAsync(); + return closeable.closeAsync(); } @Override public Completable closeAsyncGracefully() { - return group.closeAsyncGracefully(); + return closeable.closeAsyncGracefully(); } @Override diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index e129579c52..341c82b250 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -23,20 +23,19 @@ import io.servicetalk.client.api.ServiceDiscovererEvent; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; +import io.servicetalk.concurrent.internal.DefaultContextMap; +import io.servicetalk.context.api.ContextMap; import io.servicetalk.http.api.BlockingHttpClient; import io.servicetalk.http.api.BlockingStreamingHttpClient; -import io.servicetalk.http.api.BlockingStreamingHttpRequest; import io.servicetalk.http.api.BlockingStreamingHttpResponse; import io.servicetalk.http.api.FilterableStreamingHttpClient; import io.servicetalk.http.api.FilterableStreamingHttpConnection; import io.servicetalk.http.api.FilterableStreamingHttpLoadBalancedConnection; import io.servicetalk.http.api.HttpClient; -import io.servicetalk.http.api.HttpContextKeys; +import io.servicetalk.http.api.HttpExecutionContext; import io.servicetalk.http.api.HttpExecutionStrategies; import io.servicetalk.http.api.HttpExecutionStrategy; import io.servicetalk.http.api.HttpLoadBalancerFactory; -import io.servicetalk.http.api.HttpRequest; -import io.servicetalk.http.api.HttpRequestMethod; import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.api.MultiAddressHttpClientBuilder; import io.servicetalk.http.api.SingleAddressHttpClientBuilder; @@ -66,7 +65,6 @@ import org.junit.jupiter.params.provider.MethodSource; import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.EnumSet; @@ -81,6 +79,7 @@ import java.util.stream.StreamSupport; import javax.annotation.Nullable; +import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.HttpExecutionStrategies.offloadAll; import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNever; @@ -91,9 +90,12 @@ import static io.servicetalk.test.resources.TestUtils.assertNoAsyncErrors; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Collections.singleton; -import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.Matchers.not; @Execution(ExecutionMode.CONCURRENT) class ClientEffectiveStrategyTest { @@ -109,8 +111,7 @@ class ClientEffectiveStrategyTest { .setClassLevel(true); private static final String SCHEME = "http"; - private static final String PATH = "/"; - private static final String GREETING = "Hello"; + private static final String PATH = TestServiceStreaming.SVC_ECHO; /** * Which builder API will be used and where will ExecutionStrategy be initialized. @@ -163,12 +164,7 @@ static void initServer() { context = HttpServers.forAddress(localAddress(0)) .ioExecutor(SERVER_CTX.ioExecutor()) .executor(SERVER_CTX.executor()) - .listenBlocking((ctx, request, responseFactory) -> { - String requestBody = request.payloadBody().toString(StandardCharsets.UTF_8); - String response = GREETING + " " + requestBody; - return responseFactory.ok() - .payloadBody(ctx.executionContext().bufferAllocator().fromUtf8(response)); - }).toFuture().get(); + .listenStreamingAndAwait(new TestServiceStreaming()); } catch (Throwable all) { throw new AssertionError("Failed to initialize server", all); } @@ -303,18 +299,22 @@ public HttpExecutionStrategy requiredOffloads() { HttpExecutionStrategy effectiveStrategy = computeClientExecutionStrategy( builderType, builderStrategy, filterStrategy, lbStrategy, cfStrategy, clientApi); + long startTime = System.nanoTime(); invokingThreadsRecorder.reset(effectiveStrategy); - String responseBody = getResponse(clientApi, client, HttpRequestMethod.POST, requestTarget); - assertThat("Unexpected response: " + responseBody, responseBody, startsWith(GREETING)); + String responseBody = getResponse(clientApi, client, requestTarget); + long endTime = System.nanoTime(); + assertThat("Unexpected response: " + responseBody, responseBody, is(not(emptyString()))); invokingThreadsRecorder.verifyOffloads(clientApi, client.executionContext().executionStrategy(), - responseBody); + responseBody, startTime, endTime); // Complete a second request because connection factory opening can offload strangely + startTime = System.nanoTime(); invokingThreadsRecorder.reset(effectiveStrategy); - responseBody = getResponse(clientApi, client, HttpRequestMethod.POST, requestTarget); - assertThat("Unexpected response: " + responseBody, responseBody, startsWith(GREETING)); + responseBody = getResponse(clientApi, client, requestTarget); + endTime = System.nanoTime(); + assertThat("Unexpected response: " + responseBody, responseBody, is(not(emptyString()))); invokingThreadsRecorder.verifyOffloads(clientApi, client.executionContext().executionStrategy(), - responseBody); + responseBody, startTime, endTime); } } } @@ -379,8 +379,9 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde } } - private static @Nullable HttpExecutionStrategy mergeStrategies(@Nullable HttpExecutionStrategy first, - @Nullable HttpExecutionStrategy second) { + @Nullable + private static HttpExecutionStrategy mergeStrategies(@Nullable HttpExecutionStrategy first, + @Nullable HttpExecutionStrategy second) { first = offloadNever() != first ? defaultStrategy() != first ? first : offloadAll() : offloadNone(); second = offloadNever() != second ? defaultStrategy() != second ? second : offloadAll() : offloadNone(); return null == first ? second : null == second ? first : first.merge(second); @@ -391,57 +392,50 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde * * @param clientApi The client API variation to use for the request. * @param client The async streaming HTTP client to use for the request. - * @param requestMethod The request method to use. * @param requestTarget The target of the request. * @return The response converted to a String from UTF-8 payload. * @throws Exception for any problems completing the request. */ - private String getResponse(final ClientApi clientApi, final StreamingHttpClient client, - final HttpRequestMethod requestMethod, final String requestTarget) throws Exception { + private static String getResponse(final ClientApi clientApi, final StreamingHttpClient client, + final String requestTarget) throws Exception { switch (clientApi) { case BLOCKING_AGGREGATE: { BlockingHttpClient blockingClient = client.asBlockingClient(); - HttpRequest request = blockingClient.newRequest(requestMethod, requestTarget) - .payloadBody(blockingClient.executionContext().bufferAllocator() - .fromUtf8(blockingClient.executionContext().executionStrategy().toString())); - HttpResponse response = blockingClient.request(request); - return response.payloadBody().toString(StandardCharsets.UTF_8); + HttpResponse response = blockingClient.request(blockingClient.post(requestTarget) + .payloadBody(content(blockingClient.executionContext()))); + return response.payloadBody().toString(UTF_8); } case BLOCKING_STREAMING: { BlockingStreamingHttpClient blockingStreamingClient = client.asBlockingStreamingClient(); - BlockingStreamingHttpRequest request = blockingStreamingClient.newRequest(requestMethod, requestTarget) - .payloadBody(singleton(blockingStreamingClient.executionContext().bufferAllocator() - .fromUtf8(blockingStreamingClient.executionContext().executionStrategy().toString()))); - BlockingStreamingHttpResponse response = blockingStreamingClient.request(request); + BlockingStreamingHttpResponse response = blockingStreamingClient.request( + blockingStreamingClient.post(requestTarget) + .payloadBody(singleton(content(blockingStreamingClient.executionContext())))); Supplier supplier = blockingStreamingClient.executionContext().bufferAllocator()::newCompositeBuffer; return StreamSupport.stream(response.payloadBody().spliterator(), false) .reduce((Buffer base, Buffer buffer) -> (base instanceof CompositeBuffer ? - ((CompositeBuffer) base) : supplier.get().addBuffer(base)).addBuffer(buffer)) - .map(buffer -> buffer.toString(StandardCharsets.UTF_8)) + (CompositeBuffer) base : supplier.get().addBuffer(base)).addBuffer(buffer)) + .map(buffer -> buffer.toString(UTF_8)) .orElseThrow(() -> new AssertionError("No payload in response")); } case ASYNC_AGGREGATE: { HttpClient httpClient = client.asClient(); - HttpRequest request = httpClient.newRequest(requestMethod, requestTarget) - .payloadBody(httpClient.executionContext().bufferAllocator() - .fromUtf8(httpClient.executionContext().executionStrategy().toString())); - HttpResponse response = httpClient.request(request).toFuture().get(); - return response.payloadBody().toString(StandardCharsets.UTF_8); + HttpResponse response = httpClient.request(httpClient.post(requestTarget) + .payloadBody(content(httpClient.executionContext()))) + .toFuture().get(); + return response.payloadBody().toString(UTF_8); } case ASYNC_STREAMING: { - StreamingHttpRequest request = client.newRequest(requestMethod, requestTarget) - .payloadBody(Publisher.from(client.executionContext().bufferAllocator() - .fromUtf8(client.executionContext().executionStrategy().toString()))); - CompositeBuffer responsePayload = client.request(request) + CompositeBuffer responsePayload = client.request(client.post(requestTarget) + .payloadBody(Publisher.from(content(client.executionContext())))) .flatMap(resp -> resp.payloadBody().collect(() -> client.executionContext().bufferAllocator().newCompositeBuffer(), CompositeBuffer::addBuffer)) .toFuture().get(); - return responsePayload.toString(StandardCharsets.UTF_8); + return responsePayload.toString(UTF_8); } default: @@ -449,9 +443,13 @@ private String getResponse(final ClientApi clientApi, final StreamingHttpClient } } + private static Buffer content(HttpExecutionContext ctx) { + return ctx.bufferAllocator().fromUtf8(ctx.executionStrategy().toString()); + } + private static final class ClientInvokingThreadRecorder implements StreamingHttpClientFilterFactory { - private Thread applicationThread; + private Thread applicationThread = Thread.currentThread(); private final EnumSet offloadPoints = EnumSet.noneOf(ClientOffloadPoint.class); private final ConcurrentMap invokingThreads = new ConcurrentHashMap<>(); private final Queue errors = new LinkedBlockingQueue<>(); @@ -488,20 +486,22 @@ public StreamingHttpClientFilter create(final FilterableStreamingHttpClient clie protected Single request(final StreamingHttpRequester delegate, final StreamingHttpRequest request) { final HttpExecutionStrategy clientStrategy = delegate.executionContext().executionStrategy(); - final HttpExecutionStrategy requestStrategy = - request.context().get(HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY); + final HttpExecutionStrategy requestStrategy = request.context().get(HTTP_EXECUTION_STRATEGY_KEY); return delegate.request(request.transformPayloadBody(payload -> - payload.beforeRequest(__ -> recordThread(Send, clientStrategy, requestStrategy)))) - .beforeOnSuccess(__ -> recordThread(ReceiveMeta, clientStrategy, requestStrategy)) - .map(resp -> resp.transformPayloadBody(payload -> - payload.beforeOnNext(__ -> - recordThread(ReceiveData, clientStrategy, requestStrategy)))); + payload.beforeRequest(__ -> recordThread(Send, clientStrategy, requestStrategy, + request.context())))) + .beforeOnSuccess(__ -> recordThread(ReceiveMeta, clientStrategy, requestStrategy, + request.context())) + .map(resp -> resp.transformPayloadBody(payload -> payload + .beforeOnNext(__ -> recordThread(ReceiveData, clientStrategy, requestStrategy, + request.context())))); } }; } void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStrategy clientStrategy, - @Nullable final HttpExecutionStrategy requestStrategy) { + @Nullable final HttpExecutionStrategy requestStrategy, + final ContextMap reqCtx) { invokingThreads.compute(offloadPoint, (ClientOffloadPoint offload, String recorded) -> { Thread current = Thread.currentThread(); boolean appThread = current == applicationThread; @@ -511,26 +511,48 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra } else { if (offloadPoints.contains(offloadPoint)) { if (ioThread) { - errors.add(new AssertionError("Expected offloaded thread at " + offloadPoint + + final AssertionError e = new AssertionError("Expected offloaded thread at " + offloadPoint + ", but was running on " + current.getName() + ". clientStrategy=" + clientStrategy + - ", requestStrategy=" + requestStrategy)); + ", requestStrategy=" + requestStrategy + + ", timestamp=" + System.nanoTime() + + ", requestContext=" + Integer.toHexString(System.identityHashCode(reqCtx))); + if (reqCtx instanceof DefaultContextMap) { + List stacktrace = ((DefaultContextMap) reqCtx) + .stacktrace(HTTP_EXECUTION_STRATEGY_KEY); + for (Throwable t : stacktrace) { + e.addSuppressed(t); + } + } + errors.add(e); } } else { if (!ioThread) { - errors.add(new AssertionError("Expected IoThread or " + + final AssertionError e = new AssertionError("Expected IoThread or " + applicationThread.getName() + " at " + offloadPoint + ", but was running on an offloading executor thread: " + current.getName() + - ". clientStrategy=" + clientStrategy + ", requestStrategy=" + requestStrategy)); + ". clientStrategy=" + clientStrategy + ", requestStrategy=" + requestStrategy + + ", timestamp=" + System.nanoTime() + + ", requestContext=" + Integer.toHexString(System.identityHashCode(reqCtx))); + if (reqCtx instanceof DefaultContextMap) { + List stacktrace = ((DefaultContextMap) reqCtx) + .stacktrace(HTTP_EXECUTION_STRATEGY_KEY); + for (Throwable t : stacktrace) { + e.addSuppressed(t); + } + } + errors.add(e); } } } - return ioThread ? "eventLoop" : appThread ? "application" : "offloaded"; + return ioThread ? "eventLoop" : (appThread ? "application" : "offloaded"); }); } - public void verifyOffloads(ClientApi clientApi, HttpExecutionStrategy clientStrategy, String apiStrategy) { + public void verifyOffloads(ClientApi clientApi, HttpExecutionStrategy clientStrategy, String apiStrategy, + long startTime, long endTime) { assertNoAsyncErrors("API=" + clientApi + ", clientStrategy=" + clientStrategy + ", apiStrategy=" + - apiStrategy + ". Async Errors! See suppressed", errors); + apiStrategy + ", startTime=" + startTime + ", endTime=" + endTime + + ". Async Errors! See suppressed", errors); assertThat("Unexpected offload points recorded. " + invokingThreads, invokingThreads.size(), Matchers.is(ClientOffloadPoint.values().length)); } @@ -565,12 +587,9 @@ private enum ClientApi { ASYNC_AGGREGATE(EnumSet.of(ReceiveData)), ASYNC_STREAMING(EnumSet.allOf(ClientOffloadPoint.class)); - private final EnumSet offloads; private final HttpExecutionStrategy strategy; ClientApi(EnumSet offloads) { - this.offloads = offloads; - HttpExecutionStrategies.Builder builder = HttpExecutionStrategies.customStrategyBuilder(); if (offloads.contains(Send)) { @@ -586,10 +605,6 @@ private enum ClientApi { this.strategy = builder.build(); } - public EnumSet offloads() { - return offloads; - } - public HttpExecutionStrategy strategy() { return strategy; } From 60040b249cf64cc87545cd2ea7cdbb26b21a313a Mon Sep 17 00:00:00 2001 From: Idel Pivnitskiy Date: Fri, 22 Apr 2022 14:36:26 -0700 Subject: [PATCH 32/54] capture where context map created and assigned --- .../internal/DefaultContextMap.java | 18 +++- .../http/api/AbstractHttpMetaData.java | 20 ++++- .../netty/ClientEffectiveStrategyTest.java | 85 +++++++++++++------ 3 files changed, 90 insertions(+), 33 deletions(-) diff --git a/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java b/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java index ede00fd62a..00800a0c39 100644 --- a/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java +++ b/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java @@ -36,6 +36,8 @@ */ public final class DefaultContextMap implements ContextMap { + private static final Key CTOR_KEY = Key.newKey("CTOR_KEY", Object.class); + private final HashMap, Object> theMap; private final ConcurrentMap, List> stacktraces = new ConcurrentHashMap<>(); @@ -44,12 +46,24 @@ public final class DefaultContextMap implements ContextMap { */ public DefaultContextMap() { theMap = new HashMap<>(4); // start with a smaller table + List list = stacktraces.computeIfAbsent(CTOR_KEY, __ -> new CopyOnWriteArrayList<>()); + list.add(new Throwable("new DefaultContextMap() on " + Thread.currentThread().getName() + + " at " + System.nanoTime() + + " for " + Integer.toHexString(System.identityHashCode(this)))); } private DefaultContextMap(DefaultContextMap other) { theMap = new HashMap<>(other.theMap); } + public List stacktrace(Key key) { + return stacktraces.get(key); + } + + public List ctorStacktrace() { + return stacktraces.get(CTOR_KEY); + } + @Override public int size() { return theMap.size(); @@ -108,10 +122,6 @@ public T putIfAbsent(final Key key, @Nullable final T value) { return oldVal; } - public List stacktrace(Key key) { - return stacktraces.get(key); - } - @Nullable @Override @SuppressWarnings("unchecked") diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractHttpMetaData.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractHttpMetaData.java index adb1668ec7..da8c005793 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractHttpMetaData.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractHttpMetaData.java @@ -19,6 +19,8 @@ import io.servicetalk.context.api.ContextMap; import io.servicetalk.encoding.api.ContentCodec; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -27,7 +29,7 @@ /** * Abstract base class for {@link HttpMetaData}. */ -abstract class AbstractHttpMetaData implements HttpMetaData { +public abstract class AbstractHttpMetaData implements HttpMetaData { @Deprecated @Nullable private ContentCodec encoding; @@ -36,11 +38,19 @@ abstract class AbstractHttpMetaData implements HttpMetaData { @Nullable private ContextMap context; + public final List contextAssigned = new CopyOnWriteArrayList<>(); + AbstractHttpMetaData(final HttpProtocolVersion version, final HttpHeaders headers, @Nullable final ContextMap context) { this.version = requireNonNull(version); this.headers = requireNonNull(headers); this.context = context; + if (context != null) { + contextAssigned.add(new Throwable("context=" + context + + " on " + Thread.currentThread().getName() + + " at " + System.nanoTime() + + " for " + Integer.toHexString(System.identityHashCode(this)))); + } } @Override @@ -85,6 +95,10 @@ final ContextMap context0() { public final ContextMap context() { if (context == null) { context = new DefaultContextMap(); + contextAssigned.add(new Throwable("context=" + context + + " on " + Thread.currentThread().getName() + + " at " + System.nanoTime() + + " for " + Integer.toHexString(System.identityHashCode(this)))); } return context; } @@ -92,6 +106,10 @@ public final ContextMap context() { @Override public HttpMetaData context(final ContextMap context) { this.context = requireNonNull(context); + contextAssigned.add(new Throwable("context=" + context + + " on " + Thread.currentThread().getName() + + " at " + System.nanoTime() + + " for " + Integer.toHexString(System.identityHashCode(this)))); return this; } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 341c82b250..57f4dfb2d9 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -25,8 +25,10 @@ import io.servicetalk.concurrent.api.Single; import io.servicetalk.concurrent.internal.DefaultContextMap; import io.servicetalk.context.api.ContextMap; +import io.servicetalk.http.api.AbstractHttpMetaData; import io.servicetalk.http.api.BlockingHttpClient; import io.servicetalk.http.api.BlockingStreamingHttpClient; +import io.servicetalk.http.api.BlockingStreamingHttpRequest; import io.servicetalk.http.api.BlockingStreamingHttpResponse; import io.servicetalk.http.api.FilterableStreamingHttpClient; import io.servicetalk.http.api.FilterableStreamingHttpConnection; @@ -36,6 +38,8 @@ import io.servicetalk.http.api.HttpExecutionStrategies; import io.servicetalk.http.api.HttpExecutionStrategy; import io.servicetalk.http.api.HttpLoadBalancerFactory; +import io.servicetalk.http.api.HttpMetaData; +import io.servicetalk.http.api.HttpRequest; import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.api.MultiAddressHttpClientBuilder; import io.servicetalk.http.api.SingleAddressHttpClientBuilder; @@ -401,41 +405,49 @@ private static String getResponse(final ClientApi clientApi, final StreamingHttp switch (clientApi) { case BLOCKING_AGGREGATE: { BlockingHttpClient blockingClient = client.asBlockingClient(); - HttpResponse response = blockingClient.request(blockingClient.post(requestTarget) - .payloadBody(content(blockingClient.executionContext()))); - return response.payloadBody().toString(UTF_8); + final HttpRequest request = blockingClient.post(requestTarget) + .payloadBody(content(blockingClient.executionContext())); + HttpResponse response = blockingClient.request(request); + return response.payloadBody().toString(UTF_8) + + ", request=" + Integer.toHexString(System.identityHashCode(request)); } case BLOCKING_STREAMING: { BlockingStreamingHttpClient blockingStreamingClient = client.asBlockingStreamingClient(); + final BlockingStreamingHttpRequest request = blockingStreamingClient.post(requestTarget) + .payloadBody(singleton(content(blockingStreamingClient.executionContext()))); BlockingStreamingHttpResponse response = blockingStreamingClient.request( - blockingStreamingClient.post(requestTarget) - .payloadBody(singleton(content(blockingStreamingClient.executionContext())))); + request); Supplier supplier = blockingStreamingClient.executionContext().bufferAllocator()::newCompositeBuffer; return StreamSupport.stream(response.payloadBody().spliterator(), false) .reduce((Buffer base, Buffer buffer) -> (base instanceof CompositeBuffer ? (CompositeBuffer) base : supplier.get().addBuffer(base)).addBuffer(buffer)) .map(buffer -> buffer.toString(UTF_8)) - .orElseThrow(() -> new AssertionError("No payload in response")); + .orElseThrow(() -> new AssertionError("No payload in response")) + + ", request=" + Integer.toHexString(System.identityHashCode(request)); } case ASYNC_AGGREGATE: { HttpClient httpClient = client.asClient(); - HttpResponse response = httpClient.request(httpClient.post(requestTarget) - .payloadBody(content(httpClient.executionContext()))) + final HttpRequest request = httpClient.post(requestTarget) + .payloadBody(content(httpClient.executionContext())); + HttpResponse response = httpClient.request(request) .toFuture().get(); - return response.payloadBody().toString(UTF_8); + return response.payloadBody().toString(UTF_8) + + ", request=" + Integer.toHexString(System.identityHashCode(request)); } case ASYNC_STREAMING: { - CompositeBuffer responsePayload = client.request(client.post(requestTarget) - .payloadBody(Publisher.from(content(client.executionContext())))) + final StreamingHttpRequest request = client.post(requestTarget) + .payloadBody(Publisher.from(content(client.executionContext()))); + CompositeBuffer responsePayload = client.request(request) .flatMap(resp -> resp.payloadBody().collect(() -> client.executionContext().bufferAllocator().newCompositeBuffer(), CompositeBuffer::addBuffer)) .toFuture().get(); - return responsePayload.toString(UTF_8); + return responsePayload.toString(UTF_8) + + ", request=" + Integer.toHexString(System.identityHashCode(request)); } default: @@ -486,22 +498,19 @@ public StreamingHttpClientFilter create(final FilterableStreamingHttpClient clie protected Single request(final StreamingHttpRequester delegate, final StreamingHttpRequest request) { final HttpExecutionStrategy clientStrategy = delegate.executionContext().executionStrategy(); - final HttpExecutionStrategy requestStrategy = request.context().get(HTTP_EXECUTION_STRATEGY_KEY); return delegate.request(request.transformPayloadBody(payload -> - payload.beforeRequest(__ -> recordThread(Send, clientStrategy, requestStrategy, - request.context())))) - .beforeOnSuccess(__ -> recordThread(ReceiveMeta, clientStrategy, requestStrategy, - request.context())) + payload.beforeRequest(__ -> recordThread(Send, clientStrategy, request)))) + .beforeOnSuccess(__ -> recordThread(ReceiveMeta, clientStrategy, request)) .map(resp -> resp.transformPayloadBody(payload -> payload - .beforeOnNext(__ -> recordThread(ReceiveData, clientStrategy, requestStrategy, - request.context())))); + .beforeOnNext(__ -> recordThread(ReceiveData, clientStrategy, request)))); } }; } void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStrategy clientStrategy, - @Nullable final HttpExecutionStrategy requestStrategy, - final ContextMap reqCtx) { + final HttpMetaData metaData) { + final ContextMap reqCtx = metaData.context(); + final HttpExecutionStrategy requestStrategy = reqCtx.get(HTTP_EXECUTION_STRATEGY_KEY); invokingThreads.compute(offloadPoint, (ClientOffloadPoint offload, String recorded) -> { Thread current = Thread.currentThread(); boolean appThread = current == applicationThread; @@ -515,10 +524,20 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra ", but was running on " + current.getName() + ". clientStrategy=" + clientStrategy + ", requestStrategy=" + requestStrategy + ", timestamp=" + System.nanoTime() + + ", request=" + Integer.toHexString(System.identityHashCode(metaData)) + ", requestContext=" + Integer.toHexString(System.identityHashCode(reqCtx))); if (reqCtx instanceof DefaultContextMap) { - List stacktrace = ((DefaultContextMap) reqCtx) - .stacktrace(HTTP_EXECUTION_STRATEGY_KEY); + List stacktrace = ((DefaultContextMap) reqCtx).ctorStacktrace(); + for (Throwable t : stacktrace) { + e.addSuppressed(t); + } + stacktrace = ((DefaultContextMap) reqCtx).stacktrace(HTTP_EXECUTION_STRATEGY_KEY); + for (Throwable t : stacktrace) { + e.addSuppressed(t); + } + } + if (metaData instanceof AbstractHttpMetaData) { + List stacktrace = ((AbstractHttpMetaData) metaData).contextAssigned; for (Throwable t : stacktrace) { e.addSuppressed(t); } @@ -532,10 +551,20 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra ", but was running on an offloading executor thread: " + current.getName() + ". clientStrategy=" + clientStrategy + ", requestStrategy=" + requestStrategy + ", timestamp=" + System.nanoTime() + + ", request=" + Integer.toHexString(System.identityHashCode(metaData)) + ", requestContext=" + Integer.toHexString(System.identityHashCode(reqCtx))); if (reqCtx instanceof DefaultContextMap) { - List stacktrace = ((DefaultContextMap) reqCtx) - .stacktrace(HTTP_EXECUTION_STRATEGY_KEY); + List stacktrace = ((DefaultContextMap) reqCtx).ctorStacktrace(); + for (Throwable t : stacktrace) { + e.addSuppressed(t); + } + stacktrace = ((DefaultContextMap) reqCtx).stacktrace(HTTP_EXECUTION_STRATEGY_KEY); + for (Throwable t : stacktrace) { + e.addSuppressed(t); + } + } + if (metaData instanceof AbstractHttpMetaData) { + List stacktrace = ((AbstractHttpMetaData) metaData).contextAssigned; for (Throwable t : stacktrace) { e.addSuppressed(t); } @@ -548,10 +577,10 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra }); } - public void verifyOffloads(ClientApi clientApi, HttpExecutionStrategy clientStrategy, String apiStrategy, - long startTime, long endTime) { + public void verifyOffloads(ClientApi clientApi, HttpExecutionStrategy clientStrategy, + String apiStrategyAndResponseIdentity, long startTime, long endTime) { assertNoAsyncErrors("API=" + clientApi + ", clientStrategy=" + clientStrategy + ", apiStrategy=" + - apiStrategy + ", startTime=" + startTime + ", endTime=" + endTime + + apiStrategyAndResponseIdentity + ", startTime=" + startTime + ", endTime=" + endTime + ". Async Errors! See suppressed", errors); assertThat("Unexpected offload points recorded. " + invokingThreads, invokingThreads.size(), Matchers.is(ClientOffloadPoint.values().length)); From efaa5a5a399180aeadcda47a2e280e9472f04bdb Mon Sep 17 00:00:00 2001 From: Idel Pivnitskiy Date: Fri, 22 Apr 2022 15:07:22 -0700 Subject: [PATCH 33/54] capture requestContext id too --- .../http/netty/ClientEffectiveStrategyTest.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 57f4dfb2d9..8a06b6d60b 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -409,7 +409,8 @@ private static String getResponse(final ClientApi clientApi, final StreamingHttp .payloadBody(content(blockingClient.executionContext())); HttpResponse response = blockingClient.request(request); return response.payloadBody().toString(UTF_8) + - ", request=" + Integer.toHexString(System.identityHashCode(request)); + ", request=" + Integer.toHexString(System.identityHashCode(request)) + + ", requestContext=" + Integer.toHexString(System.identityHashCode(request.context())); } case BLOCKING_STREAMING: { @@ -425,7 +426,8 @@ private static String getResponse(final ClientApi clientApi, final StreamingHttp (CompositeBuffer) base : supplier.get().addBuffer(base)).addBuffer(buffer)) .map(buffer -> buffer.toString(UTF_8)) .orElseThrow(() -> new AssertionError("No payload in response")) + - ", request=" + Integer.toHexString(System.identityHashCode(request)); + ", request=" + Integer.toHexString(System.identityHashCode(request)) + + ", requestContext=" + Integer.toHexString(System.identityHashCode(request.context())); } case ASYNC_AGGREGATE: { @@ -435,7 +437,8 @@ private static String getResponse(final ClientApi clientApi, final StreamingHttp HttpResponse response = httpClient.request(request) .toFuture().get(); return response.payloadBody().toString(UTF_8) + - ", request=" + Integer.toHexString(System.identityHashCode(request)); + ", request=" + Integer.toHexString(System.identityHashCode(request)) + + ", requestContext=" + Integer.toHexString(System.identityHashCode(request.context())); } case ASYNC_STREAMING: { @@ -447,7 +450,8 @@ private static String getResponse(final ClientApi clientApi, final StreamingHttp CompositeBuffer::addBuffer)) .toFuture().get(); return responsePayload.toString(UTF_8) + - ", request=" + Integer.toHexString(System.identityHashCode(request)); + ", request=" + Integer.toHexString(System.identityHashCode(request)) + + ", requestContext=" + Integer.toHexString(System.identityHashCode(request.context())); } default: From 9d6e6a44fdaeace0a1859f8b57de25b76091b02b Mon Sep 17 00:00:00 2001 From: Idel Pivnitskiy Date: Fri, 22 Apr 2022 15:16:53 -0700 Subject: [PATCH 34/54] Remove ExecutionMode.CONCURRENT --- .../io/servicetalk/http/netty/ClientEffectiveStrategyTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 8a06b6d60b..6a88ab4095 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -62,8 +62,6 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.extension.RegisterExtension; -import org.junit.jupiter.api.parallel.Execution; -import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -101,7 +99,6 @@ import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.Matchers.not; -@Execution(ExecutionMode.CONCURRENT) class ClientEffectiveStrategyTest { @RegisterExtension From ef49a056a31cd09a8caa1a1888c3051559e4849d Mon Sep 17 00:00:00 2001 From: Idel Pivnitskiy Date: Fri, 22 Apr 2022 17:01:25 -0700 Subject: [PATCH 35/54] Revert intermediate debugging --- .../internal/DefaultContextMap.java | 31 +---- .../http/api/AbstractHttpMetaData.java | 20 +--- .../netty/ClientEffectiveStrategyTest.java | 113 ++++-------------- 3 files changed, 28 insertions(+), 136 deletions(-) diff --git a/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java b/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java index 00800a0c39..254347e77a 100644 --- a/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java +++ b/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java @@ -18,11 +18,7 @@ import io.servicetalk.context.api.ContextMap; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.BiPredicate; import java.util.function.Function; import javax.annotation.Nullable; @@ -36,34 +32,19 @@ */ public final class DefaultContextMap implements ContextMap { - private static final Key CTOR_KEY = Key.newKey("CTOR_KEY", Object.class); - private final HashMap, Object> theMap; - private final ConcurrentMap, List> stacktraces = new ConcurrentHashMap<>(); /** * Creates a new instance. */ public DefaultContextMap() { theMap = new HashMap<>(4); // start with a smaller table - List list = stacktraces.computeIfAbsent(CTOR_KEY, __ -> new CopyOnWriteArrayList<>()); - list.add(new Throwable("new DefaultContextMap() on " + Thread.currentThread().getName() + - " at " + System.nanoTime() + - " for " + Integer.toHexString(System.identityHashCode(this)))); } private DefaultContextMap(DefaultContextMap other) { theMap = new HashMap<>(other.theMap); } - public List stacktrace(Key key) { - return stacktraces.get(key); - } - - public List ctorStacktrace() { - return stacktraces.get(CTOR_KEY); - } - @Override public int size() { return theMap.size(); @@ -102,10 +83,6 @@ public T getOrDefault(final Key key, final T defaultValue) { @Override @SuppressWarnings("unchecked") public T put(final Key key, @Nullable final T value) { - List list = stacktraces.computeIfAbsent(key, __ -> new CopyOnWriteArrayList<>()); - list.add(new Throwable("put(" + value + ") on " + Thread.currentThread().getName() + - " at " + System.nanoTime() + - " for " + Integer.toHexString(System.identityHashCode(this)))); return (T) theMap.put(requireNonNull(key, "key"), value); } @@ -113,13 +90,7 @@ public T put(final Key key, @Nullable final T value) { @Override @SuppressWarnings("unchecked") public T putIfAbsent(final Key key, @Nullable final T value) { - final T oldVal = (T) theMap.putIfAbsent(requireNonNull(key, "key"), value); - List list = stacktraces.computeIfAbsent(key, __ -> new CopyOnWriteArrayList<>()); - list.add(new Throwable("putIfAbsent(" + value + ")=" + oldVal + - " on " + Thread.currentThread().getName() + - " at " + System.nanoTime() + - " for " + Integer.toHexString(System.identityHashCode(this)))); - return oldVal; + return (T) theMap.putIfAbsent(requireNonNull(key, "key"), value); } @Nullable diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractHttpMetaData.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractHttpMetaData.java index da8c005793..adb1668ec7 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractHttpMetaData.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractHttpMetaData.java @@ -19,8 +19,6 @@ import io.servicetalk.context.api.ContextMap; import io.servicetalk.encoding.api.ContentCodec; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -29,7 +27,7 @@ /** * Abstract base class for {@link HttpMetaData}. */ -public abstract class AbstractHttpMetaData implements HttpMetaData { +abstract class AbstractHttpMetaData implements HttpMetaData { @Deprecated @Nullable private ContentCodec encoding; @@ -38,19 +36,11 @@ public abstract class AbstractHttpMetaData implements HttpMetaData { @Nullable private ContextMap context; - public final List contextAssigned = new CopyOnWriteArrayList<>(); - AbstractHttpMetaData(final HttpProtocolVersion version, final HttpHeaders headers, @Nullable final ContextMap context) { this.version = requireNonNull(version); this.headers = requireNonNull(headers); this.context = context; - if (context != null) { - contextAssigned.add(new Throwable("context=" + context + - " on " + Thread.currentThread().getName() + - " at " + System.nanoTime() + - " for " + Integer.toHexString(System.identityHashCode(this)))); - } } @Override @@ -95,10 +85,6 @@ final ContextMap context0() { public final ContextMap context() { if (context == null) { context = new DefaultContextMap(); - contextAssigned.add(new Throwable("context=" + context + - " on " + Thread.currentThread().getName() + - " at " + System.nanoTime() + - " for " + Integer.toHexString(System.identityHashCode(this)))); } return context; } @@ -106,10 +92,6 @@ public final ContextMap context() { @Override public HttpMetaData context(final ContextMap context) { this.context = requireNonNull(context); - contextAssigned.add(new Throwable("context=" + context + - " on " + Thread.currentThread().getName() + - " at " + System.nanoTime() + - " for " + Integer.toHexString(System.identityHashCode(this)))); return this; } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 6a88ab4095..baf3879981 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -23,12 +23,8 @@ import io.servicetalk.client.api.ServiceDiscovererEvent; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; -import io.servicetalk.concurrent.internal.DefaultContextMap; -import io.servicetalk.context.api.ContextMap; -import io.servicetalk.http.api.AbstractHttpMetaData; import io.servicetalk.http.api.BlockingHttpClient; import io.servicetalk.http.api.BlockingStreamingHttpClient; -import io.servicetalk.http.api.BlockingStreamingHttpRequest; import io.servicetalk.http.api.BlockingStreamingHttpResponse; import io.servicetalk.http.api.FilterableStreamingHttpClient; import io.servicetalk.http.api.FilterableStreamingHttpConnection; @@ -38,8 +34,6 @@ import io.servicetalk.http.api.HttpExecutionStrategies; import io.servicetalk.http.api.HttpExecutionStrategy; import io.servicetalk.http.api.HttpLoadBalancerFactory; -import io.servicetalk.http.api.HttpMetaData; -import io.servicetalk.http.api.HttpRequest; import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.api.MultiAddressHttpClientBuilder; import io.servicetalk.http.api.SingleAddressHttpClientBuilder; @@ -300,22 +294,20 @@ public HttpExecutionStrategy requiredOffloads() { HttpExecutionStrategy effectiveStrategy = computeClientExecutionStrategy( builderType, builderStrategy, filterStrategy, lbStrategy, cfStrategy, clientApi); - long startTime = System.nanoTime(); invokingThreadsRecorder.reset(effectiveStrategy); String responseBody = getResponse(clientApi, client, requestTarget); - long endTime = System.nanoTime(); assertThat("Unexpected response: " + responseBody, responseBody, is(not(emptyString()))); invokingThreadsRecorder.verifyOffloads(clientApi, client.executionContext().executionStrategy(), - responseBody, startTime, endTime); + responseBody); - // Complete a second request because connection factory opening can offload strangely - startTime = System.nanoTime(); + // Execute request one more time to make sure we cover all paths: + // 1. When client opens a new connection, open connection callback comes on IoThread. + // 2. When client reuses connection, aggregation of request payload body can run on application thread. invokingThreadsRecorder.reset(effectiveStrategy); responseBody = getResponse(clientApi, client, requestTarget); - endTime = System.nanoTime(); assertThat("Unexpected response: " + responseBody, responseBody, is(not(emptyString()))); invokingThreadsRecorder.verifyOffloads(clientApi, client.executionContext().executionStrategy(), - responseBody, startTime, endTime); + responseBody); } } } @@ -402,53 +394,41 @@ private static String getResponse(final ClientApi clientApi, final StreamingHttp switch (clientApi) { case BLOCKING_AGGREGATE: { BlockingHttpClient blockingClient = client.asBlockingClient(); - final HttpRequest request = blockingClient.post(requestTarget) - .payloadBody(content(blockingClient.executionContext())); - HttpResponse response = blockingClient.request(request); - return response.payloadBody().toString(UTF_8) + - ", request=" + Integer.toHexString(System.identityHashCode(request)) + - ", requestContext=" + Integer.toHexString(System.identityHashCode(request.context())); + HttpResponse response = blockingClient.request(blockingClient.post(requestTarget) + .payloadBody(content(blockingClient.executionContext()))); + return response.payloadBody().toString(UTF_8); } case BLOCKING_STREAMING: { BlockingStreamingHttpClient blockingStreamingClient = client.asBlockingStreamingClient(); - final BlockingStreamingHttpRequest request = blockingStreamingClient.post(requestTarget) - .payloadBody(singleton(content(blockingStreamingClient.executionContext()))); BlockingStreamingHttpResponse response = blockingStreamingClient.request( - request); + blockingStreamingClient.post(requestTarget) + .payloadBody(singleton(content(blockingStreamingClient.executionContext())))); Supplier supplier = blockingStreamingClient.executionContext().bufferAllocator()::newCompositeBuffer; return StreamSupport.stream(response.payloadBody().spliterator(), false) .reduce((Buffer base, Buffer buffer) -> (base instanceof CompositeBuffer ? (CompositeBuffer) base : supplier.get().addBuffer(base)).addBuffer(buffer)) .map(buffer -> buffer.toString(UTF_8)) - .orElseThrow(() -> new AssertionError("No payload in response")) + - ", request=" + Integer.toHexString(System.identityHashCode(request)) + - ", requestContext=" + Integer.toHexString(System.identityHashCode(request.context())); + .orElseThrow(() -> new AssertionError("No payload in response")); } case ASYNC_AGGREGATE: { HttpClient httpClient = client.asClient(); - final HttpRequest request = httpClient.post(requestTarget) - .payloadBody(content(httpClient.executionContext())); - HttpResponse response = httpClient.request(request) + HttpResponse response = httpClient.request(httpClient.post(requestTarget) + .payloadBody(content(httpClient.executionContext()))) .toFuture().get(); - return response.payloadBody().toString(UTF_8) + - ", request=" + Integer.toHexString(System.identityHashCode(request)) + - ", requestContext=" + Integer.toHexString(System.identityHashCode(request.context())); + return response.payloadBody().toString(UTF_8); } case ASYNC_STREAMING: { - final StreamingHttpRequest request = client.post(requestTarget) - .payloadBody(Publisher.from(content(client.executionContext()))); - CompositeBuffer responsePayload = client.request(request) + CompositeBuffer responsePayload = client.request(client.post(requestTarget) + .payloadBody(Publisher.from(content(client.executionContext())))) .flatMap(resp -> resp.payloadBody().collect(() -> client.executionContext().bufferAllocator().newCompositeBuffer(), CompositeBuffer::addBuffer)) .toFuture().get(); - return responsePayload.toString(UTF_8) + - ", request=" + Integer.toHexString(System.identityHashCode(request)) + - ", requestContext=" + Integer.toHexString(System.identityHashCode(request.context())); + return responsePayload.toString(UTF_8); } default: @@ -499,19 +479,18 @@ public StreamingHttpClientFilter create(final FilterableStreamingHttpClient clie protected Single request(final StreamingHttpRequester delegate, final StreamingHttpRequest request) { final HttpExecutionStrategy clientStrategy = delegate.executionContext().executionStrategy(); + final HttpExecutionStrategy requestStrategy = request.context().get(HTTP_EXECUTION_STRATEGY_KEY); return delegate.request(request.transformPayloadBody(payload -> - payload.beforeRequest(__ -> recordThread(Send, clientStrategy, request)))) - .beforeOnSuccess(__ -> recordThread(ReceiveMeta, clientStrategy, request)) + payload.beforeRequest(__ -> recordThread(Send, clientStrategy, requestStrategy)))) + .beforeOnSuccess(__ -> recordThread(ReceiveMeta, clientStrategy, requestStrategy)) .map(resp -> resp.transformPayloadBody(payload -> payload - .beforeOnNext(__ -> recordThread(ReceiveData, clientStrategy, request)))); + .beforeOnNext(__ -> recordThread(ReceiveData, clientStrategy, requestStrategy)))); } }; } void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStrategy clientStrategy, - final HttpMetaData metaData) { - final ContextMap reqCtx = metaData.context(); - final HttpExecutionStrategy requestStrategy = reqCtx.get(HTTP_EXECUTION_STRATEGY_KEY); + @Nullable final HttpExecutionStrategy requestStrategy) { invokingThreads.compute(offloadPoint, (ClientOffloadPoint offload, String recorded) -> { Thread current = Thread.currentThread(); boolean appThread = current == applicationThread; @@ -523,26 +502,7 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra if (ioThread) { final AssertionError e = new AssertionError("Expected offloaded thread at " + offloadPoint + ", but was running on " + current.getName() + ". clientStrategy=" + clientStrategy + - ", requestStrategy=" + requestStrategy + - ", timestamp=" + System.nanoTime() + - ", request=" + Integer.toHexString(System.identityHashCode(metaData)) + - ", requestContext=" + Integer.toHexString(System.identityHashCode(reqCtx))); - if (reqCtx instanceof DefaultContextMap) { - List stacktrace = ((DefaultContextMap) reqCtx).ctorStacktrace(); - for (Throwable t : stacktrace) { - e.addSuppressed(t); - } - stacktrace = ((DefaultContextMap) reqCtx).stacktrace(HTTP_EXECUTION_STRATEGY_KEY); - for (Throwable t : stacktrace) { - e.addSuppressed(t); - } - } - if (metaData instanceof AbstractHttpMetaData) { - List stacktrace = ((AbstractHttpMetaData) metaData).contextAssigned; - for (Throwable t : stacktrace) { - e.addSuppressed(t); - } - } + ", requestStrategy=" + requestStrategy); errors.add(e); } } else { @@ -550,26 +510,7 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra final AssertionError e = new AssertionError("Expected IoThread or " + applicationThread.getName() + " at " + offloadPoint + ", but was running on an offloading executor thread: " + current.getName() + - ". clientStrategy=" + clientStrategy + ", requestStrategy=" + requestStrategy + - ", timestamp=" + System.nanoTime() + - ", request=" + Integer.toHexString(System.identityHashCode(metaData)) + - ", requestContext=" + Integer.toHexString(System.identityHashCode(reqCtx))); - if (reqCtx instanceof DefaultContextMap) { - List stacktrace = ((DefaultContextMap) reqCtx).ctorStacktrace(); - for (Throwable t : stacktrace) { - e.addSuppressed(t); - } - stacktrace = ((DefaultContextMap) reqCtx).stacktrace(HTTP_EXECUTION_STRATEGY_KEY); - for (Throwable t : stacktrace) { - e.addSuppressed(t); - } - } - if (metaData instanceof AbstractHttpMetaData) { - List stacktrace = ((AbstractHttpMetaData) metaData).contextAssigned; - for (Throwable t : stacktrace) { - e.addSuppressed(t); - } - } + ". clientStrategy=" + clientStrategy + ", requestStrategy=" + requestStrategy); errors.add(e); } } @@ -578,11 +519,9 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra }); } - public void verifyOffloads(ClientApi clientApi, HttpExecutionStrategy clientStrategy, - String apiStrategyAndResponseIdentity, long startTime, long endTime) { + public void verifyOffloads(ClientApi clientApi, HttpExecutionStrategy clientStrategy, String apiStrategy) { assertNoAsyncErrors("API=" + clientApi + ", clientStrategy=" + clientStrategy + ", apiStrategy=" + - apiStrategyAndResponseIdentity + ", startTime=" + startTime + ", endTime=" + endTime + - ". Async Errors! See suppressed", errors); + apiStrategy + ". Async Errors! See suppressed", errors); assertThat("Unexpected offload points recorded. " + invokingThreads, invokingThreads.size(), Matchers.is(ClientOffloadPoint.values().length)); } From d670415e005395410add63b6c1d3be1735ef6373 Mon Sep 17 00:00:00 2001 From: Idel Pivnitskiy Date: Fri, 22 Apr 2022 17:31:19 -0700 Subject: [PATCH 36/54] Remove intermediate debug logging --- servicetalk-test-resources/src/main/resources/log4j2.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/servicetalk-test-resources/src/main/resources/log4j2.xml b/servicetalk-test-resources/src/main/resources/log4j2.xml index bf12238cb1..689b84662d 100644 --- a/servicetalk-test-resources/src/main/resources/log4j2.xml +++ b/servicetalk-test-resources/src/main/resources/log4j2.xml @@ -30,7 +30,6 @@ - From a33671b56ccd56b04d0cec30d8f39282250ba328 Mon Sep 17 00:00:00 2001 From: Idel Pivnitskiy Date: Fri, 22 Apr 2022 17:49:55 -0700 Subject: [PATCH 37/54] reorder client api --- .../http/netty/ClientEffectiveStrategyTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index baf3879981..49655ab25c 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -392,7 +392,7 @@ private static HttpExecutionStrategy mergeStrategies(@Nullable HttpExecutionStra private static String getResponse(final ClientApi clientApi, final StreamingHttpClient client, final String requestTarget) throws Exception { switch (clientApi) { - case BLOCKING_AGGREGATE: { + case BLOCKING_AGGREGATED: { BlockingHttpClient blockingClient = client.asBlockingClient(); HttpResponse response = blockingClient.request(blockingClient.post(requestTarget) .payloadBody(content(blockingClient.executionContext()))); @@ -413,7 +413,7 @@ private static String getResponse(final ClientApi clientApi, final StreamingHttp .orElseThrow(() -> new AssertionError("No payload in response")); } - case ASYNC_AGGREGATE: { + case ASYNC_AGGREGATED: { HttpClient httpClient = client.asClient(); HttpResponse response = httpClient.request(httpClient.post(requestTarget) .payloadBody(content(httpClient.executionContext()))) @@ -551,10 +551,10 @@ public ExecutionStrategy requiredOffloads() { * Which API flavor will be used. */ private enum ClientApi { - BLOCKING_STREAMING(EnumSet.of(Send)), - BLOCKING_AGGREGATE(EnumSet.noneOf(ClientOffloadPoint.class)), - ASYNC_AGGREGATE(EnumSet.of(ReceiveData)), - ASYNC_STREAMING(EnumSet.allOf(ClientOffloadPoint.class)); + ASYNC_AGGREGATED(EnumSet.of(ReceiveData)), + ASYNC_STREAMING(EnumSet.allOf(ClientOffloadPoint.class)), + BLOCKING_AGGREGATED(EnumSet.noneOf(ClientOffloadPoint.class)), + BLOCKING_STREAMING(EnumSet.of(Send)); private final HttpExecutionStrategy strategy; From 312c836a73ea3ae996b9e3efee82687e8fcaf857 Mon Sep 17 00:00:00 2001 From: Idel Pivnitskiy Date: Fri, 22 Apr 2022 21:27:42 -0700 Subject: [PATCH 38/54] revert `ExecutionMode.CONCURRENT` --- .../io/servicetalk/http/netty/ClientEffectiveStrategyTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 49655ab25c..9079356437 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -56,6 +56,8 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -93,6 +95,7 @@ import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.Matchers.not; +@Execution(ExecutionMode.CONCURRENT) class ClientEffectiveStrategyTest { @RegisterExtension From 34a468d024530e8dfd8cf96491aa10b750e8e175 Mon Sep 17 00:00:00 2001 From: Idel Pivnitskiy Date: Fri, 22 Apr 2022 22:02:25 -0700 Subject: [PATCH 39/54] small refactoring --- .../netty/ClientEffectiveStrategyTest.java | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 9079356437..17a4a0809b 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -117,8 +117,8 @@ class ClientEffectiveStrategyTest { private enum BuilderType { SINGLE_BUILDER, MULTI_BUILDER, - MULTI_DEFAULT_SINGLE_BUILDER, - MULTI_NONE_SINGLE_BUILDER + MULTI_DEFAULT_STRATEGY_SINGLE_BUILDER, + MULTI_OFFLOAD_NONE_SINGLE_BUILDER } private static final HttpExecutionStrategy[] BUILDER_STRATEGIES = { @@ -180,7 +180,7 @@ static Stream casesSupplier() { List arguments = new ArrayList<>(); for (BuilderType builderType : BuilderType.values()) { for (HttpExecutionStrategy builderStrategy : BUILDER_STRATEGIES) { - if (BuilderType.MULTI_NONE_SINGLE_BUILDER == builderType && + if (BuilderType.MULTI_OFFLOAD_NONE_SINGLE_BUILDER == builderType && null == builderStrategy) { // null builderStrategy won't actually override, so skip. continue; @@ -269,8 +269,8 @@ public HttpExecutionStrategy requiredOffloads() { clientBuilder = singleClientBuilder::buildStreaming; break; case MULTI_BUILDER: - case MULTI_DEFAULT_SINGLE_BUILDER: - case MULTI_NONE_SINGLE_BUILDER: + case MULTI_DEFAULT_STRATEGY_SINGLE_BUILDER: + case MULTI_OFFLOAD_NONE_SINGLE_BUILDER: requestTarget = SCHEME + "://" + serverHostAndPort(context) + PATH; MultiAddressHttpClientBuilder multiClientBuilder = HttpClients.forMultiAddressUrl() @@ -280,7 +280,7 @@ public HttpExecutionStrategy requiredOffloads() { if (BuilderType.MULTI_BUILDER == builderType && null != builderStrategy) { multiClientBuilder.executionStrategy(builderStrategy); } - if (BuilderType.MULTI_NONE_SINGLE_BUILDER == builderType && + if (BuilderType.MULTI_OFFLOAD_NONE_SINGLE_BUILDER == builderType && null != builderStrategy) { // This is expected to ALWAYS be overridden in initializer. multiClientBuilder.executionStrategy(offloadNone()); @@ -357,18 +357,18 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde builder; switch (builderType) { - case SINGLE_BUILDER: + case SINGLE_BUILDER: return defaultStrategy() == merged ? clientApi.strategy() : merged; case MULTI_BUILDER: return null == builder || defaultStrategy() == builder ? defaultStrategy() == merged ? clientApi.strategy() : clientApi.strategy().merge(merged) : merged; - case MULTI_DEFAULT_SINGLE_BUILDER: + case MULTI_DEFAULT_STRATEGY_SINGLE_BUILDER: if (defaultStrategy() == merged || (null != builder && !builder.hasOffloads())) { merged = offloadNone(); } return clientApi.strategy().merge(merged); - case MULTI_NONE_SINGLE_BUILDER: + case MULTI_OFFLOAD_NONE_SINGLE_BUILDER: return defaultStrategy() == merged ? offloadNone() : merged; default: throw new AssertionError("Unexpected builder type: " + builderType); @@ -378,9 +378,16 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde @Nullable private static HttpExecutionStrategy mergeStrategies(@Nullable HttpExecutionStrategy first, @Nullable HttpExecutionStrategy second) { - first = offloadNever() != first ? defaultStrategy() != first ? first : offloadAll() : offloadNone(); - second = offloadNever() != second ? defaultStrategy() != second ? second : offloadAll() : offloadNone(); - return null == first ? second : null == second ? first : first.merge(second); + first = replaceSpecialStrategies(first); + second = replaceSpecialStrategies(second); + return null == first ? second : + null == second ? first : first.merge(second); + } + + @Nullable + private static HttpExecutionStrategy replaceSpecialStrategies(@Nullable final HttpExecutionStrategy strategy) { + return offloadNever() == strategy ? offloadNone() : + defaultStrategy() == strategy ? offloadAll() : strategy; } /** From 3079781f425b65f943364d6a89c58812c80e3f62 Mon Sep 17 00:00:00 2001 From: Idel Pivnitskiy Date: Fri, 22 Apr 2022 22:03:52 -0700 Subject: [PATCH 40/54] small refactoring 2 --- ...DefaultMultiAddressUrlHttpClientBuilder.java | 17 +++++++++-------- .../http/netty/FilterableClientToClient.java | 3 +-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index 99ff1a0579..a5fd72e9b3 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -93,8 +93,6 @@ final class DefaultMultiAddressUrlHttpClientBuilder private static final String HTTPS_SCHEME = HTTPS.toString(); - private static final MultiAddressStrategyWrapper MULTI_ADDRESS_STRATEGY_WRAPPER = new MultiAddressStrategyWrapper(); - private final Function> builderFactory; private final HttpExecutionContextBuilder executionContextBuilder = new HttpExecutionContextBuilder(); @@ -115,8 +113,7 @@ public StreamingHttpClient buildStreaming() { final CompositeCloseable closeables = newCompositeCloseable(); try { final HttpExecutionContext executionContext = executionContextBuilder.build(); - final ClientFactory clientFactory = new ClientFactory(builderFactory, - executionContext, + final ClientFactory clientFactory = new ClientFactory(builderFactory, executionContext, singleAddressInitializer); final CachingKeyFactory keyFactory = closeables.prepend(new CachingKeyFactory()); final HttpHeadersFactory headersFactory = this.headersFactory; @@ -250,20 +247,24 @@ public StreamingHttpClient apply(final UrlKey urlKey) { builder.sslConfig(DEFAULT_CLIENT_SSL_CONFIG); } - builder.appendClientFilter(MULTI_ADDRESS_STRATEGY_WRAPPER); + builder.appendClientFilter(MultiAddressStrategyWrapper.INSTANCE); if (singleAddressInitializer != null) { singleAddressInitializer.initialize(urlKey.scheme, urlKey.hostAndPort, builder); } - StreamingHttpClient singleClient = builder.buildStreaming(); - - return singleClient; + return builder.buildStreaming(); } } private static final class MultiAddressStrategyWrapper implements StreamingHttpClientFilterFactory { + private static final StreamingHttpClientFilterFactory INSTANCE = new MultiAddressStrategyWrapper(); + + private MultiAddressStrategyWrapper() { + // Singleton + } + @Override public StreamingHttpClientFilter create(final FilterableStreamingHttpClient client) { return new StreamingHttpClientFilter(client) { diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/FilterableClientToClient.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/FilterableClientToClient.java index 3f24c5f9d5..cec3521123 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/FilterableClientToClient.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/FilterableClientToClient.java @@ -115,8 +115,7 @@ public Single request(final StreamingHttpRequest request) // created and hence could have an incorrect default strategy. Doing this makes sure we never call // the method without strategy just as we do for the regular connection. return Single.defer(() -> { - request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, - FilterableClientToClient.this.executionContext().executionStrategy()); + request.context().putIfAbsent(HTTP_EXECUTION_STRATEGY_KEY, clientstrategy); return rc.request(request).shareContextOnSubscribe(); }); } From f591a1c42df28edd2accaee319c4263e2bdd2c95 Mon Sep 17 00:00:00 2001 From: Idel Pivnitskiy Date: Fri, 22 Apr 2022 22:05:27 -0700 Subject: [PATCH 41/54] small refactoring 3 --- .../http/netty/DefaultMultiAddressUrlHttpClientBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index a5fd72e9b3..bf39a628c1 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -259,7 +259,7 @@ public StreamingHttpClient apply(final UrlKey urlKey) { private static final class MultiAddressStrategyWrapper implements StreamingHttpClientFilterFactory { - private static final StreamingHttpClientFilterFactory INSTANCE = new MultiAddressStrategyWrapper(); + static final StreamingHttpClientFilterFactory INSTANCE = new MultiAddressStrategyWrapper(); private MultiAddressStrategyWrapper() { // Singleton From 79b922cfdfdba257e8783656f812f96cf252d945 Mon Sep 17 00:00:00 2001 From: Idel Pivnitskiy Date: Fri, 22 Apr 2022 22:17:14 -0700 Subject: [PATCH 42/54] rename filter, add javadoc to clarify its behavior --- ...efaultMultiAddressUrlHttpClientBuilder.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index bf39a628c1..1a877b7658 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -28,7 +28,9 @@ import io.servicetalk.http.api.DefaultStreamingHttpRequestResponseFactory; import io.servicetalk.http.api.FilterableReservedStreamingHttpConnection; import io.servicetalk.http.api.FilterableStreamingHttpClient; +import io.servicetalk.http.api.HttpContextKeys; import io.servicetalk.http.api.HttpExecutionContext; +import io.servicetalk.http.api.HttpExecutionStrategies; import io.servicetalk.http.api.HttpExecutionStrategy; import io.servicetalk.http.api.HttpHeadersFactory; import io.servicetalk.http.api.HttpRequestMetaData; @@ -247,7 +249,7 @@ public StreamingHttpClient apply(final UrlKey urlKey) { builder.sslConfig(DEFAULT_CLIENT_SSL_CONFIG); } - builder.appendClientFilter(MultiAddressStrategyWrapper.INSTANCE); + builder.appendClientFilter(HttpExecutionStrategyUpdater.INSTANCE); if (singleAddressInitializer != null) { singleAddressInitializer.initialize(urlKey.scheme, urlKey.hostAndPort, builder); @@ -257,11 +259,19 @@ public StreamingHttpClient apply(final UrlKey urlKey) { } } - private static final class MultiAddressStrategyWrapper implements StreamingHttpClientFilterFactory { + /** + * When request transitions from the multi-address level to the single-address level, this filter will make sure + * that any missing offloading required by the selected single-address client will be applied for the request + * execution. This filter never reduces offloading, it can only add missing offloading flags. Users who want to + * execute a request without offloading must specify {@link HttpExecutionStrategies#offloadNone()} strategy at the + * {@link MultiAddressHttpClientBuilder} or explicitly set the required strategy at request context with + * {@link HttpContextKeys#HTTP_EXECUTION_STRATEGY_KEY}. + */ + private static final class HttpExecutionStrategyUpdater implements StreamingHttpClientFilterFactory { - static final StreamingHttpClientFilterFactory INSTANCE = new MultiAddressStrategyWrapper(); + static final StreamingHttpClientFilterFactory INSTANCE = new HttpExecutionStrategyUpdater(); - private MultiAddressStrategyWrapper() { + private HttpExecutionStrategyUpdater() { // Singleton } From da80a7e84f96a0963f918520ef10e45e1f7812eb Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Mon, 25 Apr 2022 15:04:30 -0700 Subject: [PATCH 43/54] Add assertion checking for conversion reducing offloading --- ...reamingHttpClientToBlockingHttpClient.java | 2 ++ ...tpClientToBlockingStreamingHttpClient.java | 2 ++ .../api/StreamingHttpClientToHttpClient.java | 2 ++ ...tionToBlockingStreamingHttpConnection.java | 2 ++ ...actBlockingStreamingHttpRequesterTest.java | 2 ++ .../api/AbstractHttpRequesterFilterTest.java | 2 ++ ...faultMultiAddressUrlHttpClientBuilder.java | 27 ++++++++++++++-- .../DefaultPartitionedHttpClientBuilder.java | 2 +- ...DefaultSingleAddressHttpClientBuilder.java | 7 ++-- .../http/netty/FilterableClientToClient.java | 10 ++---- .../netty/ClientEffectiveStrategyTest.java | 32 +++++++++++++++++-- 11 files changed, 72 insertions(+), 18 deletions(-) diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingHttpClient.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingHttpClient.java index b2f746b5e1..c937d9727f 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingHttpClient.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingHttpClient.java @@ -31,6 +31,8 @@ final class StreamingHttpClientToBlockingHttpClient implements BlockingHttpClien private final HttpRequestResponseFactory reqRespFactory; StreamingHttpClientToBlockingHttpClient(final StreamingHttpClient client, final HttpExecutionStrategy strategy) { + assert client.executionContext().executionStrategy().hasOffloads() || !strategy.hasOffloads() : + "Incompatible client strategy : " + strategy; this.strategy = defaultStrategy() == strategy ? DEFAULT_BLOCKING_CONNECTION_STRATEGY : strategy; this.client = client; context = new DelegatingHttpExecutionContext(client.executionContext()) { diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingStreamingHttpClient.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingStreamingHttpClient.java index a55b971893..ff0b2bbf85 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingStreamingHttpClient.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingStreamingHttpClient.java @@ -32,6 +32,8 @@ final class StreamingHttpClientToBlockingStreamingHttpClient implements Blocking StreamingHttpClientToBlockingStreamingHttpClient(final StreamingHttpClient client, final HttpExecutionStrategy strategy) { + assert client.executionContext().executionStrategy().hasOffloads() || !strategy.hasOffloads() : + "Incompatible client strategy : " + strategy; this.strategy = defaultStrategy() == strategy ? DEFAULT_BLOCKING_STREAMING_CONNECTION_STRATEGY : strategy; this.client = client; context = new DelegatingHttpExecutionContext(client.executionContext()) { diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToHttpClient.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToHttpClient.java index 790e0d019f..09437e8eea 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToHttpClient.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToHttpClient.java @@ -32,6 +32,8 @@ final class StreamingHttpClientToHttpClient implements HttpClient { private final HttpRequestResponseFactory reqRespFactory; StreamingHttpClientToHttpClient(final StreamingHttpClient client, final HttpExecutionStrategy strategy) { + assert client.executionContext().executionStrategy().hasOffloads() || !strategy.hasOffloads() : + "Incompatible client strategy : " + strategy; this.strategy = defaultStrategy() == strategy ? DEFAULT_ASYNC_CONNECTION_STRATEGY : strategy; this.client = client; context = new DelegatingHttpExecutionContext(client.executionContext()) { diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpConnectionToBlockingStreamingHttpConnection.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpConnectionToBlockingStreamingHttpConnection.java index 5642affa1f..f4a71458fb 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpConnectionToBlockingStreamingHttpConnection.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpConnectionToBlockingStreamingHttpConnection.java @@ -32,6 +32,8 @@ final class StreamingHttpConnectionToBlockingStreamingHttpConnection implements StreamingHttpConnectionToBlockingStreamingHttpConnection(final StreamingHttpConnection connection, final HttpExecutionStrategy strategy) { + assert connection.executionContext().executionStrategy().hasOffloads() || !strategy.hasOffloads() : + "Incompatible client strategy : " + strategy; this.strategy = defaultStrategy() == strategy ? DEFAULT_BLOCKING_STREAMING_CONNECTION_STRATEGY : strategy; this.connection = connection; final HttpConnectionContext originalCtx = connection.connectionContext(); diff --git a/servicetalk-http-api/src/test/java/io/servicetalk/http/api/AbstractBlockingStreamingHttpRequesterTest.java b/servicetalk-http-api/src/test/java/io/servicetalk/http/api/AbstractBlockingStreamingHttpRequesterTest.java index b977a8d042..3f0beaa2c5 100644 --- a/servicetalk-http-api/src/test/java/io/servicetalk/http/api/AbstractBlockingStreamingHttpRequesterTest.java +++ b/servicetalk-http-api/src/test/java/io/servicetalk/http/api/AbstractBlockingStreamingHttpRequesterTest.java @@ -36,6 +36,7 @@ import static io.servicetalk.concurrent.api.Publisher.from; import static io.servicetalk.concurrent.api.Single.failed; import static io.servicetalk.concurrent.api.Single.succeeded; +import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static io.servicetalk.http.api.HttpResponseStatus.OK; import static java.nio.charset.StandardCharsets.US_ASCII; @@ -75,6 +76,7 @@ protected interface TestHttpRequester { void setup() { MockitoAnnotations.initMocks(this); when(mockExecutionCtx.executor()).thenReturn(immediate()); + when(mockExecutionCtx.executionStrategy()).thenReturn(defaultStrategy()); when(mockCtx.executionContext()).thenReturn(mockExecutionCtx); when(mockIterable.iterator()).thenReturn(mockIterator); } diff --git a/servicetalk-http-api/src/testFixtures/java/io/servicetalk/http/api/AbstractHttpRequesterFilterTest.java b/servicetalk-http-api/src/testFixtures/java/io/servicetalk/http/api/AbstractHttpRequesterFilterTest.java index 94829c319a..b013ba8704 100644 --- a/servicetalk-http-api/src/testFixtures/java/io/servicetalk/http/api/AbstractHttpRequesterFilterTest.java +++ b/servicetalk-http-api/src/testFixtures/java/io/servicetalk/http/api/AbstractHttpRequesterFilterTest.java @@ -44,6 +44,7 @@ import static io.servicetalk.http.api.AbstractHttpRequesterFilterTest.SecurityType.Secure; import static io.servicetalk.http.api.FilterFactoryUtils.appendClientFilterFactory; import static io.servicetalk.http.api.FilterFactoryUtils.appendConnectionFilterFactory; +import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; @@ -90,6 +91,7 @@ final void setupContext() { } protected void setUp(SecurityType security) { + lenient().when(mockExecutionContext.executionStrategy()).thenReturn(defaultStrategy()); lenient().when(mockConnectionContext.sslSession()).thenAnswer(__ -> { switch (security) { case Secure: diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index 1a877b7658..f1410a404b 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -71,6 +71,7 @@ import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.HttpExecutionStrategies.offloadAll; +import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNever; import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNone; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static io.servicetalk.http.netty.DefaultSingleAddressHttpClientBuilder.setExecutionContext; @@ -130,7 +131,7 @@ public StreamingHttpClient buildStreaming() { new RedirectingHttpRequesterFilter(redirectConfig).create(urlClient); LOGGER.debug("Multi-address client created with base strategy {}", executionContext.executionStrategy()); - return new FilterableClientToClient(urlClient, executionContext.executionStrategy()); + return new FilterableClientToClient(urlClient, executionContext); } catch (final Throwable t) { closeables.closeAsync().subscribe(); throw t; @@ -341,7 +342,17 @@ public Single reserveConnec final HttpRequestMetaData metaData) { return defer(() -> { try { - return selectClient(metaData).reserveConnection(metaData).shareContextOnSubscribe(); + FilterableStreamingHttpClient singleClient = selectClient(metaData); + HttpExecutionStrategy requestStrategy = metaData.context().get(HTTP_EXECUTION_STRATEGY_KEY); + HttpExecutionStrategy clientStrategy = singleClient.executionContext().executionStrategy(); + HttpExecutionStrategy useStrategy = defaultStrategy() == clientStrategy ? + requestStrategy : + clientStrategy.merge(offloadNever() == requestStrategy ? offloadNone() : requestStrategy); + if (requestStrategy != useStrategy) { + // single client overrides request strategy; + metaData.context().put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); + } + return singleClient.reserveConnection(metaData).shareContextOnSubscribe(); } catch (Throwable t) { return Single.failed(t).shareContextOnSubscribe(); } @@ -352,7 +363,17 @@ public Single reserveConnec public Single request(final StreamingHttpRequest request) { return defer(() -> { try { - return selectClient(request).request(request).shareContextOnSubscribe(); + FilterableStreamingHttpClient singleClient = selectClient(request); + HttpExecutionStrategy requestStrategy = request.context().get(HTTP_EXECUTION_STRATEGY_KEY); + HttpExecutionStrategy clientStrategy = singleClient.executionContext().executionStrategy(); + HttpExecutionStrategy useStrategy = defaultStrategy() == clientStrategy ? + requestStrategy : + clientStrategy.merge(offloadNever() == requestStrategy ? offloadNone() : requestStrategy); + if (requestStrategy != useStrategy) { + // single client overrides request strategy; + request.context().put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); + } + return singleClient.request(request).shareContextOnSubscribe(); } catch (Throwable t) { return Single.failed(t).shareContextOnSubscribe(); } diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultPartitionedHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultPartitionedHttpClientBuilder.java index 0fba847dca..b16f8e3e16 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultPartitionedHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultPartitionedHttpClientBuilder.java @@ -132,7 +132,7 @@ public StreamingHttpClient buildStreaming() { executionContext, partitionMapFactory); LOGGER.debug("Partitioned client created with base strategy {}", executionContext.executionStrategy()); - return new FilterableClientToClient(partitionedClient, executionContext.executionStrategy()); + return new FilterableClientToClient(partitionedClient, executionContext); } private static final class DefaultPartitionedStreamingHttpClientFilter implements diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java index c71be4ce6d..b799857b15 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultSingleAddressHttpClientBuilder.java @@ -298,12 +298,13 @@ ctx.builder.connectionFilterFactory, new AlpnReqRespFactoryFunc( currClientFilterFactory = appendFilter(currClientFilterFactory, ctx.builder.retryingHttpRequesterFilter); } + FilterableStreamingHttpClient wrappedClient = currClientFilterFactory != null ? + currClientFilterFactory.create(lbClient, lb.eventStream(), ctx.sdStatus) : + lbClient; LOGGER.debug("Client for {} created with base strategy {} → computed strategy {}", targetAddress(ctx), builderExecutionContext.executionStrategy(), computedStrategy); - return new FilterableClientToClient(currClientFilterFactory != null ? - currClientFilterFactory.create(lbClient, lb.eventStream(), ctx.sdStatus) : - lbClient, computedStrategy); + return new FilterableClientToClient(wrappedClient, executionContext); } catch (final Throwable t) { closeOnException.closeAsync().subscribe(); throw t; diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/FilterableClientToClient.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/FilterableClientToClient.java index cec3521123..f870552c2b 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/FilterableClientToClient.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/FilterableClientToClient.java @@ -20,7 +20,6 @@ import io.servicetalk.concurrent.api.Single; import io.servicetalk.http.api.BlockingHttpClient; import io.servicetalk.http.api.BlockingStreamingHttpClient; -import io.servicetalk.http.api.DelegatingHttpExecutionContext; import io.servicetalk.http.api.FilterableStreamingHttpClient; import io.servicetalk.http.api.HttpClient; import io.servicetalk.http.api.HttpConnectionContext; @@ -50,14 +49,9 @@ final class FilterableClientToClient implements StreamingHttpClient { private final FilterableStreamingHttpClient client; private final HttpExecutionContext executionContext; - FilterableClientToClient(FilterableStreamingHttpClient filteredClient, HttpExecutionStrategy strategy) { + FilterableClientToClient(FilterableStreamingHttpClient filteredClient, HttpExecutionContext executionContext) { client = filteredClient; - this.executionContext = new DelegatingHttpExecutionContext(client.executionContext()) { - @Override - public HttpExecutionStrategy executionStrategy() { - return strategy; - } - }; + this.executionContext = executionContext; } @Override diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 17a4a0809b..c1e02ba52c 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -112,12 +112,38 @@ class ClientEffectiveStrategyTest { private static final String PATH = TestServiceStreaming.SVC_ECHO; /** - * Which builder API will be used and where will ExecutionStrategy be initialized. + * Which builder API will be used and where will test {@link HttpExecutionStrategy} be applied. */ private enum BuilderType { + /** + * Test execution strategy applied to single client builder, + * {@link SingleAddressHttpClientBuilder#executionStrategy(HttpExecutionStrategy)}. + */ SINGLE_BUILDER, + + /** + * Test execution strategy applied to multi client builder, + * {@link MultiAddressHttpClientBuilder#executionStrategy(HttpExecutionStrategy)}. + * Single client builder inherits multi client execution context strategy. + */ MULTI_BUILDER, + + /** + * Multi client builder uses default strategy ({@link HttpExecutionStrategies#defaultStrategy()}). + * Single client builder inherits multi client builder execution context strategy, + * {@link HttpExecutionStrategies#defaultStrategy()}. + * Test execution strategy applied to single client builder, + * {@link SingleAddressHttpClientBuilder#executionStrategy(HttpExecutionStrategy)}. + */ MULTI_DEFAULT_STRATEGY_SINGLE_BUILDER, + + /** + * Multi client builder uses {@link HttpExecutionStrategies#offloadNone()} strategy. + * Single client builder inherits multi client builder execution context strategy, + * {@link HttpExecutionStrategies#offloadNone()}. + * Test execution strategy applied to single client builder, + * {@link SingleAddressHttpClientBuilder#executionStrategy(HttpExecutionStrategy)}. + */ MULTI_OFFLOAD_NONE_SINGLE_BUILDER } @@ -361,7 +387,7 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde return defaultStrategy() == merged ? clientApi.strategy() : merged; case MULTI_BUILDER: return null == builder || defaultStrategy() == builder ? - defaultStrategy() == merged ? clientApi.strategy() : clientApi.strategy().merge(merged) : + defaultStrategy() == merged ? offloadAll() : clientApi.strategy().merge(merged) : merged; case MULTI_DEFAULT_STRATEGY_SINGLE_BUILDER: if (defaultStrategy() == merged || (null != builder && !builder.hasOffloads())) { @@ -369,7 +395,7 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde } return clientApi.strategy().merge(merged); case MULTI_OFFLOAD_NONE_SINGLE_BUILDER: - return defaultStrategy() == merged ? offloadNone() : merged; + return merged; default: throw new AssertionError("Unexpected builder type: " + builderType); } From b45b45da07ff1f4b6194b12eb6b02e5dcab3933c Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Tue, 26 Apr 2022 18:17:19 -0700 Subject: [PATCH 44/54] improve executionStrategy() documentation --- .../api/MultiAddressHttpClientBuilder.java | 44 +++++++++++++------ .../api/SingleAddressHttpClientBuilder.java | 35 ++++++++------- 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java index 95c820901b..f44a8f596b 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java @@ -16,6 +16,7 @@ package io.servicetalk.http.api; import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.client.api.ConnectionFactoryFilter; import io.servicetalk.client.api.ServiceDiscovererEvent; import io.servicetalk.concurrent.api.Executor; import io.servicetalk.transport.api.IoExecutor; @@ -75,23 +76,37 @@ default SingleAddressInitializer append(SingleAddressInitializer toA *

Provides the base execution strategy for all clients created from this builder and the default strategy for * the {@link SingleAddressHttpClientBuilder} used to construct client instances. The * {@link #initializer(SingleAddressInitializer)} may be used for some customization of the execution strategy for a - * specific client. Unless {@link HttpExecutionStrategies#offloadNone()} is specified as the execution strategy on - * this builder then the single client computed execution strategy will be merged with this builder strategy. + * specific single address client instance, but may not reduce the offloading to be performed. Specifically, the + * initializer may introduce additional offloading via + * {@link SingleAddressHttpClientBuilder#executionStrategy(HttpExecutionStrategy)} and may add filters which + * influence the computed execution strategy. * - *

- *
unspecified or {@link HttpExecutionStrategies#defaultStrategy()} - *
Effective execution strategy will be appropriate for the API (async/blocking streaming/aggregate) of the - * client used and will be be merged with the computed execution strategy of the single address client produced - * by {@link SingleAddressHttpClientBuilder}. + *

Specifying an execution strategy will affect the offloading used during the execution of client requests: * + *

+ *
Unspecified or {@link HttpExecutionStrategies#defaultStrategy()} + *
The resulting client instances will use the default safe strategy for each API variant and + * {@link SingleAddressHttpClientBuilder} instances generated will also have + * default strategy. The computed strategy MAY NOT reduce + * the offloads used for client request execution from the client API safe default. * *
{@link HttpExecutionStrategies#offloadNone()} - * (or deprecated {@link HttpExecutionStrategies#offloadNever()}), - * a custom execution strategy ({@link HttpExecutionStrategies#customStrategyBuilder()}), or + * (or deprecated {@link HttpExecutionStrategies#offloadNever()}) + *
{@link SingleAddressHttpClientBuilder} instances created by the client will have a default strategy of + * {@link HttpExecutionStrategies#offloadNone()}. An + * {@link #initializer(SingleAddressInitializer) initializer} may override to add offloads using + * {@link SingleAddressHttpClientBuilder#executionStrategy(HttpExecutionStrategy)}. Overriding the execution + * strategy to require offloads will also allowed filters added to influence the computed execution strategy. + * If not overridden by initializer, the resulting computed {@link HttpExecutionStrategies#offloadNone()} + * execution strategy requires that filters + * and asynchronous callbacks must not ever block during + * the execution of client requests. + * + *
A custom execution strategy ({@link HttpExecutionStrategies#customStrategyBuilder()}) or * {@link HttpExecutionStrategies#offloadAll()} - *
Will be used, as specified, without regard to the API of the client used and will be merged with the - * computed execution strategy of the single address client produced by - * {@link SingleAddressHttpClientBuilder}. + *
{@link SingleAddressHttpClientBuilder} instances created by the client will have a default strategy of + * the provided strategy and must result in a computed execution strategy with at least the same offloads as the + * provided. *
* * @param strategy {@inheritDoc} @@ -118,7 +133,10 @@ default MultiAddressHttpClientBuilder headersFactory(HttpHeadersFactory he /** * Set a function which can customize options for each {@link StreamingHttpClient} that is built. * @param initializer Initializes the {@link SingleAddressHttpClientBuilder} used to build new - * {@link StreamingHttpClient}s. + * {@link StreamingHttpClient}s. See {@link #executionStrategy(HttpExecutionStrategy)} for discussion of + * restrictions on the use of {@link SingleAddressHttpClientBuilder#executionStrategy(HttpExecutionStrategy)} + * within an initializer. + * * @return {@code this} */ MultiAddressHttpClientBuilder initializer(SingleAddressInitializer initializer); diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java index 2b064d53e8..a71a73449b 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java @@ -16,11 +16,7 @@ package io.servicetalk.http.api; import io.servicetalk.buffer.api.BufferAllocator; -import io.servicetalk.client.api.ConnectionFactory; -import io.servicetalk.client.api.ConnectionFactoryFilter; -import io.servicetalk.client.api.LoadBalancer; -import io.servicetalk.client.api.ServiceDiscoverer; -import io.servicetalk.client.api.ServiceDiscovererEvent; +import io.servicetalk.client.api.*; import io.servicetalk.concurrent.api.BiIntFunction; import io.servicetalk.concurrent.api.Completable; import io.servicetalk.concurrent.api.Executor; @@ -175,26 +171,31 @@ SingleAddressHttpClientBuilder appendConnectionFilter( /** * {@inheritDoc} * - *

Unless {@link HttpExecutionStrategies#offloadNone()} is specified as the execution strategy on - * this builder, the actual execution strategy used will be influenced by the execution strategy required by filters - * added via {@link #appendClientFilter(StreamingHttpClientFilterFactory)}, - * {@link #appendConnectionFilter(StreamingHttpConnectionFilterFactory)}, and - * {@link #appendConnectionFactoryFilter(ConnectionFactoryFilter)}

, etc. + *

Specifying an execution strategy will affect the offloading used during the execution of client requests: * *

- *
unspecified or {@link HttpExecutionStrategies#defaultStrategy()} - *
Effective execution strategy will be appropriate for the API (async/blocking streaming/aggregate) of the - * client used and will be {@linkplain HttpExecutionStrategyInfluencer influenced} by required strategies of - * any filters. + *
Unspecified or {@link HttpExecutionStrategies#defaultStrategy()} + *
A safe execution strategy appropriate for the client API and the filters added will be computed and used. + * Each client API variant (async/blocking streaming/aggregate) requires a specific set of offloads to avoid + * blocking the event-loop. In addition, filters added via + * {@link #appendClientFilter(StreamingHttpClientFilterFactory)}, + * {@link #appendConnectionFilter(StreamingHttpConnectionFilterFactory)}, or + * {@link #appendConnectionFactoryFilter(ConnectionFactoryFilter)}

, etc. may also require offloading. + * The execution strategy used will be computed using the client API required strategy + * {@linkplain HttpExecutionStrategyInfluencer influenced} by the required strategies of added filters. * *
{@link HttpExecutionStrategies#offloadNone()} * (or deprecated {@link HttpExecutionStrategies#offloadNever()}) - *
No offloading will be used regardless of the client API used or the influence of the filters. + *
No offloading will be used regardless of the client API used or the influence of added filters. Filters + * and asynchronous callbacks must not ever block during + * the execution of client requests. * *
A custom execution strategy ({@link HttpExecutionStrategies#customStrategyBuilder()}) or * {@link HttpExecutionStrategies#offloadAll()} - *
Will be used, as specified, without regard to the API of the client used. Effective execution strategy - * will be specified value plus any additional offloads required by any filters. + *
The specified execution strategy will be used rather than the client API's default safe strategy. Like + * with the default strategy, the actual execution strategy used is computed from the provided strategy and + * the added filters. Filters and asynchronous callbacks MAY + * only block during the offloaded portions of the client request execution. *
* * @param strategy {@inheritDoc} From 09e63bd7156bd7da571ca65c3cce236af8baeffb Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Thu, 28 Apr 2022 18:11:36 -0700 Subject: [PATCH 45/54] improved javadoc --- .../api/SingleAddressHttpClientBuilder.java | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java index a71a73449b..b414aaca93 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java @@ -16,7 +16,11 @@ package io.servicetalk.http.api; import io.servicetalk.buffer.api.BufferAllocator; -import io.servicetalk.client.api.*; +import io.servicetalk.client.api.ConnectionFactory; +import io.servicetalk.client.api.ConnectionFactoryFilter; +import io.servicetalk.client.api.LoadBalancer; +import io.servicetalk.client.api.ServiceDiscoverer; +import io.servicetalk.client.api.ServiceDiscovererEvent; import io.servicetalk.concurrent.api.BiIntFunction; import io.servicetalk.concurrent.api.Completable; import io.servicetalk.concurrent.api.Executor; @@ -171,31 +175,33 @@ SingleAddressHttpClientBuilder appendConnectionFilter( /** * {@inheritDoc} * - *

Specifying an execution strategy will affect the offloading used during the execution of client requests: + *

Specifying an execution strategy affects the offloading used during execution of client requests: * *

*
Unspecified or {@link HttpExecutionStrategies#defaultStrategy()} - *
A safe execution strategy appropriate for the client API and the filters added will be computed and used. - * Each client API variant (async/blocking streaming/aggregate) requires a specific set of offloads to avoid - * blocking the event-loop. In addition, filters added via + *
Execution of client requests will use a safe (non-blocking) execution strategy appropriate for the + * client API and the filters added. Each client API variant (async/blocking streaming/aggregate) requires a + * specific execution strategy to avoid blocking the event-loop and filters added via * {@link #appendClientFilter(StreamingHttpClientFilterFactory)}, * {@link #appendConnectionFilter(StreamingHttpConnectionFilterFactory)}, or * {@link #appendConnectionFactoryFilter(ConnectionFactoryFilter)}

, etc. may also require offloading. - * The execution strategy used will be computed using the client API required strategy - * {@linkplain HttpExecutionStrategyInfluencer influenced} by the required strategies of added filters. + * The execution strategy for execution of client requests will be computed based on the client API in use and + * {@link HttpExecutionStrategyInfluencer#requiredOffloads()} of added the filters. * *
{@link HttpExecutionStrategies#offloadNone()} * (or deprecated {@link HttpExecutionStrategies#offloadNever()}) - *
No offloading will be used regardless of the client API used or the influence of added filters. Filters - * and asynchronous callbacks must not ever block during - * the execution of client requests. + *
No offloading will be used during execution of client requests regardless of the client API used or the + * influence of added filters. Filters and asynchronous callbacks + * must not ever block during the execution of client + * requests. * *
A custom execution strategy ({@link HttpExecutionStrategies#customStrategyBuilder()}) or * {@link HttpExecutionStrategies#offloadAll()} - *
The specified execution strategy will be used rather than the client API's default safe strategy. Like - * with the default strategy, the actual execution strategy used is computed from the provided strategy and - * the added filters. Filters and asynchronous callbacks MAY - * only block during the offloaded portions of the client request execution. + *
The specified execution strategy will be used for executing client requests rather than the client + * API's default safe strategy. Like with the default strategy, the actual execution strategy used is computed + * from the provided strategy and the execution strategies required by added filters. Filters and asynchronous + * callbacks MAY only block during the offloaded portions of + * the client request execution. *
* * @param strategy {@inheritDoc} From d5a83f010320fb6114b7dc62436a9d676b158dac Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Wed, 18 May 2022 13:32:26 -0700 Subject: [PATCH 46/54] documentation refinement for discussion --- .../http/api/HttpClientBuilder.java | 7 +++- .../api/SingleAddressHttpClientBuilder.java | 40 +++++++++---------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpClientBuilder.java index 0647b6c3a3..b4e95ddca1 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpClientBuilder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpClientBuilder.java @@ -54,9 +54,12 @@ interface HttpClientBuilder> { HttpClientBuilder bufferAllocator(BufferAllocator allocator); /** - * Sets the {@link HttpExecutionStrategy} for all clients created from this builder. + * Sets the {@link HttpExecutionStrategy} to be used for client callbacks when executing client requests for all + * clients created from this builder. + * + * @param strategy {@link HttpExecutionStrategy} to use. If callbacks to the application code may block then those + * callbacks must request to be offloaded. * - * @param strategy {@link HttpExecutionStrategy} to use. * @return {@code this}. * @see HttpExecutionStrategies */ diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java index b414aaca93..9ba40a860e 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java @@ -175,33 +175,29 @@ SingleAddressHttpClientBuilder appendConnectionFilter( /** * {@inheritDoc} * - *

Specifying an execution strategy affects the offloading used during execution of client requests: + *

Specifying an execution strategy affects the offloading used during execution of client requests. The + * offloading to applied is computed using the provided strategy, the strategies required by any filters + * which have been added and the strategy required by the client API variant (async/blocking streaming/aggregate). + * Each client API variant (async/blocking streaming/aggregate) requires a specific execution strategy to avoid + * blocking the event-loop. Filters added via + * {@link #appendClientFilter(StreamingHttpClientFilterFactory)}, + * {@link #appendConnectionFilter(StreamingHttpConnectionFilterFactory)}, or + * {@link #appendConnectionFactoryFilter(ConnectionFactoryFilter)}, etc. may specify via + * {@link HttpExecutionStrategyInfluencer#requiredOffloads()} that require offloading. * *

*
Unspecified or {@link HttpExecutionStrategies#defaultStrategy()} *
Execution of client requests will use a safe (non-blocking) execution strategy appropriate for the - * client API and the filters added. Each client API variant (async/blocking streaming/aggregate) requires a - * specific execution strategy to avoid blocking the event-loop and filters added via - * {@link #appendClientFilter(StreamingHttpClientFilterFactory)}, - * {@link #appendConnectionFilter(StreamingHttpConnectionFilterFactory)}, or - * {@link #appendConnectionFactoryFilter(ConnectionFactoryFilter)}

, etc. may also require offloading. - * The execution strategy for execution of client requests will be computed based on the client API in use and - * {@link HttpExecutionStrategyInfluencer#requiredOffloads()} of added the filters. + * client API used and the filters added. Blocking is always safe as all potentially blocking paths are + * offloaded. * - *
{@link HttpExecutionStrategies#offloadNone()} - * (or deprecated {@link HttpExecutionStrategies#offloadNever()}) - *
No offloading will be used during execution of client requests regardless of the client API used or the - * influence of added filters. Filters and asynchronous callbacks - * must not ever block during the execution of client - * requests. - * - *
A custom execution strategy ({@link HttpExecutionStrategies#customStrategyBuilder()}) or - * {@link HttpExecutionStrategies#offloadAll()} - *
The specified execution strategy will be used for executing client requests rather than the client - * API's default safe strategy. Like with the default strategy, the actual execution strategy used is computed - * from the provided strategy and the execution strategies required by added filters. Filters and asynchronous - * callbacks MAY only block during the offloaded portions of - * the client request execution. + *
Any other execution strategy + *
Similar to the default case, client request execution will be completed using a computed execution + * strategy. The computed execution strategy will use the specified execution strategy rather than the + * client API's default safe strategy but will still combine with the required execution strategy of any added + * filters. Filters and asynchronous callbacks MAY block + * only during the offloaded portions of the client request execution. Blocking + * MUST NOT be used during other callbacks. *
* * @param strategy {@inheritDoc} From 765285951d6daf95ac1145a211d575d01c4e6142 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Thu, 9 Jun 2022 13:30:04 -0700 Subject: [PATCH 47/54] Use current strategy for merging with offloadNone() --- .../DefaultMultiAddressUrlHttpClientBuilder.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index f1410a404b..96197d4672 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -292,8 +292,11 @@ protected Single request( defaultStrategy() == singleStrategy || !singleStrategy.hasOffloads() ? // single client is default or has no *additional* offloads requestStrategy : - // add single client offloads to existing strategy - requestStrategy.merge(singleStrategy); + requestStrategy.hasOffloads() ? + // add single client offloads to existing strategy + requestStrategy.merge(singleStrategy) : + // Ignore single client offloads to preserve no-offloads + requestStrategy; if (useStrategy != requestStrategy) { LOGGER.debug("Request strategy {} changes to {}. SingleAddressClient strategy: {}", @@ -347,7 +350,11 @@ public Single reserveConnec HttpExecutionStrategy clientStrategy = singleClient.executionContext().executionStrategy(); HttpExecutionStrategy useStrategy = defaultStrategy() == clientStrategy ? requestStrategy : - clientStrategy.merge(offloadNever() == requestStrategy ? offloadNone() : requestStrategy); + requestStrategy.hasOffloads() ? + // add single client offloads to existing strategy + clientStrategy.merge(requestStrategy) : + // IGNORE single client offloads to preserve offloadNone() + requestStrategy; if (requestStrategy != useStrategy) { // single client overrides request strategy; metaData.context().put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); From c52950cfe2f2bfa6e5b777e4ac8f6fe39ed44b12 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Fri, 10 Jun 2022 10:44:27 -0700 Subject: [PATCH 48/54] adjust everything to current "builder takes precedece" rule --- ...faultMultiAddressUrlHttpClientBuilder.java | 57 +++++++++++-------- .../netty/ClientEffectiveStrategyTest.java | 29 +++++----- 2 files changed, 49 insertions(+), 37 deletions(-) diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index 96197d4672..94b9a01788 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -71,7 +71,6 @@ import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.HttpExecutionStrategies.offloadAll; -import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNever; import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNone; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static io.servicetalk.http.netty.DefaultSingleAddressHttpClientBuilder.setExecutionContext; @@ -286,17 +285,16 @@ protected Single request( HttpExecutionStrategy singleStrategy = client.executionContext().executionStrategy(); HttpExecutionStrategy requestStrategy = request.context().getOrDefault(HTTP_EXECUTION_STRATEGY_KEY, defaultStrategy()); - HttpExecutionStrategy useStrategy = - null == requestStrategy || defaultStrategy() == requestStrategy ? - offloadAll() : - defaultStrategy() == singleStrategy || !singleStrategy.hasOffloads() ? - // single client is default or has no *additional* offloads - requestStrategy : - requestStrategy.hasOffloads() ? - // add single client offloads to existing strategy - requestStrategy.merge(singleStrategy) : - // Ignore single client offloads to preserve no-offloads - requestStrategy; + HttpExecutionStrategy useStrategy = defaultStrategy() == requestStrategy ? + offloadAll() : + defaultStrategy() == singleStrategy || !singleStrategy.hasOffloads() ? + // single client is default or has no *additional* offloads + requestStrategy : + requestStrategy.hasOffloads() ? + // add single client offloads to existing strategy + requestStrategy.merge(singleStrategy) : + // IGNORE single client offloads to preserve no-offloads + requestStrategy; if (useStrategy != requestStrategy) { LOGGER.debug("Request strategy {} changes to {}. SingleAddressClient strategy: {}", @@ -347,14 +345,18 @@ public Single reserveConnec try { FilterableStreamingHttpClient singleClient = selectClient(metaData); HttpExecutionStrategy requestStrategy = metaData.context().get(HTTP_EXECUTION_STRATEGY_KEY); - HttpExecutionStrategy clientStrategy = singleClient.executionContext().executionStrategy(); - HttpExecutionStrategy useStrategy = defaultStrategy() == clientStrategy ? - requestStrategy : - requestStrategy.hasOffloads() ? - // add single client offloads to existing strategy - clientStrategy.merge(requestStrategy) : - // IGNORE single client offloads to preserve offloadNone() - requestStrategy; + HttpExecutionStrategy singleStrategy = singleClient.executionContext().executionStrategy(); + HttpExecutionStrategy useStrategy = defaultStrategy() == requestStrategy ? + offloadAll() : + defaultStrategy() == singleStrategy || !singleStrategy.hasOffloads() ? + // single client is default or has no *additional* offloads + requestStrategy : + requestStrategy.hasOffloads() ? + // add single client offloads to existing strategy + requestStrategy.merge(singleStrategy) : + // IGNORE single client offloads to preserve no-offloads + requestStrategy; + if (requestStrategy != useStrategy) { // single client overrides request strategy; metaData.context().put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); @@ -372,10 +374,17 @@ public Single request(final StreamingHttpRequest request) try { FilterableStreamingHttpClient singleClient = selectClient(request); HttpExecutionStrategy requestStrategy = request.context().get(HTTP_EXECUTION_STRATEGY_KEY); - HttpExecutionStrategy clientStrategy = singleClient.executionContext().executionStrategy(); - HttpExecutionStrategy useStrategy = defaultStrategy() == clientStrategy ? - requestStrategy : - clientStrategy.merge(offloadNever() == requestStrategy ? offloadNone() : requestStrategy); + HttpExecutionStrategy singleStrategy = singleClient.executionContext().executionStrategy(); + HttpExecutionStrategy useStrategy = defaultStrategy() == requestStrategy ? + offloadAll() : + defaultStrategy() == singleStrategy || !singleStrategy.hasOffloads() ? + // single client is default or has no *additional* offloads + requestStrategy : + requestStrategy.hasOffloads() ? + // add single client offloads to existing strategy + requestStrategy.merge(singleStrategy) : + // IGNORE single client offloads to preserve no-offloads + requestStrategy; if (requestStrategy != useStrategy) { // single client overrides request strategy; request.context().put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index c1e02ba52c..6761689b8f 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -95,7 +95,7 @@ import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.Matchers.not; -@Execution(ExecutionMode.CONCURRENT) +@Execution(ExecutionMode.SAME_THREAD) class ClientEffectiveStrategyTest { @RegisterExtension @@ -306,8 +306,7 @@ public HttpExecutionStrategy requiredOffloads() { if (BuilderType.MULTI_BUILDER == builderType && null != builderStrategy) { multiClientBuilder.executionStrategy(builderStrategy); } - if (BuilderType.MULTI_OFFLOAD_NONE_SINGLE_BUILDER == builderType && - null != builderStrategy) { + if (BuilderType.MULTI_OFFLOAD_NONE_SINGLE_BUILDER == builderType && null != builderStrategy) { // This is expected to ALWAYS be overridden in initializer. multiClientBuilder.executionStrategy(offloadNone()); } @@ -387,15 +386,15 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde return defaultStrategy() == merged ? clientApi.strategy() : merged; case MULTI_BUILDER: return null == builder || defaultStrategy() == builder ? - defaultStrategy() == merged ? offloadAll() : clientApi.strategy().merge(merged) : - merged; + clientApi.strategy().merge(merged) : + builder.hasOffloads() ? merged : builder; case MULTI_DEFAULT_STRATEGY_SINGLE_BUILDER: if (defaultStrategy() == merged || (null != builder && !builder.hasOffloads())) { merged = offloadNone(); } return clientApi.strategy().merge(merged); case MULTI_OFFLOAD_NONE_SINGLE_BUILDER: - return merged; + return offloadNone(); default: throw new AssertionError("Unexpected builder type: " + builderType); } @@ -479,24 +478,26 @@ private static Buffer content(HttpExecutionContext ctx) { private static final class ClientInvokingThreadRecorder implements StreamingHttpClientFilterFactory { private Thread applicationThread = Thread.currentThread(); + private HttpExecutionStrategy expectedStrategy; private final EnumSet offloadPoints = EnumSet.noneOf(ClientOffloadPoint.class); private final ConcurrentMap invokingThreads = new ConcurrentHashMap<>(); private final Queue errors = new LinkedBlockingQueue<>(); - void reset(HttpExecutionStrategy streamingAsyncStrategy) { + void reset(HttpExecutionStrategy expectedStrategy) { invokingThreads.clear(); errors.clear(); offloadPoints.clear(); applicationThread = Thread.currentThread(); + this.expectedStrategy = expectedStrategy; // adjust expected offloads for specific execution strategy - if (streamingAsyncStrategy.isSendOffloaded()) { + if (expectedStrategy.isSendOffloaded()) { offloadPoints.add(Send); } - if (streamingAsyncStrategy.isMetadataReceiveOffloaded()) { + if (expectedStrategy.isMetadataReceiveOffloaded()) { offloadPoints.add(ReceiveMeta); } - if (streamingAsyncStrategy.isDataReceiveOffloaded()) { + if (expectedStrategy.isDataReceiveOffloaded()) { offloadPoints.add(ReceiveData); } } @@ -546,7 +547,8 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra final AssertionError e = new AssertionError("Expected IoThread or " + applicationThread.getName() + " at " + offloadPoint + ", but was running on an offloading executor thread: " + current.getName() + - ". clientStrategy=" + clientStrategy + ", requestStrategy=" + requestStrategy); + ". clientStrategy=" + clientStrategy + ", expectedStrategy=" + expectedStrategy + + ", requestStrategy=" + requestStrategy); errors.add(e); } } @@ -556,8 +558,9 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra } public void verifyOffloads(ClientApi clientApi, HttpExecutionStrategy clientStrategy, String apiStrategy) { - assertNoAsyncErrors("API=" + clientApi + ", clientStrategy=" + clientStrategy + ", apiStrategy=" + - apiStrategy + ". Async Errors! See suppressed", errors); + assertNoAsyncErrors("API=" + clientApi + ", apiStrategy=" + apiStrategy + + ", clientStrategy=" + clientStrategy + + ", expectedStrategy=" + expectedStrategy + ". Async Errors! See suppressed", errors); assertThat("Unexpected offload points recorded. " + invokingThreads, invokingThreads.size(), Matchers.is(ClientOffloadPoint.values().length)); } From aebccc90fa154d1a08794579be2fcb6fa840e98a Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Fri, 10 Jun 2022 11:22:02 -0700 Subject: [PATCH 49/54] documentation matches behaviour and fix multi-offloadNone overrid --- .../api/SingleAddressHttpClientBuilder.java | 39 +++++++++++-------- ...faultMultiAddressUrlHttpClientBuilder.java | 21 +++------- .../netty/ClientEffectiveStrategyTest.java | 3 +- 3 files changed, 30 insertions(+), 33 deletions(-) diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java index 49d5f540d8..142a866d0e 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java @@ -175,29 +175,34 @@ SingleAddressHttpClientBuilder appendConnectionFilter( /** * {@inheritDoc} * - *

Specifying an execution strategy affects the offloading used during execution of client requests. The - * offloading to applied is computed using the provided strategy, the strategies required by any filters - * which have been added and the strategy required by the client API variant (async/blocking streaming/aggregate). - * Each client API variant (async/blocking streaming/aggregate) requires a specific execution strategy to avoid - * blocking the event-loop. Filters added via - * {@link #appendClientFilter(StreamingHttpClientFilterFactory)}, - * {@link #appendConnectionFilter(StreamingHttpConnectionFilterFactory)}, or - * {@link #appendConnectionFactoryFilter(ConnectionFactoryFilter)}, etc. may specify via - * {@link HttpExecutionStrategyInfluencer#requiredOffloads()} that require offloading. + *

Specifying an execution strategy affects the offloading used during execution of client requests: * *

*
Unspecified or {@link HttpExecutionStrategies#defaultStrategy()} *
Execution of client requests will use a safe (non-blocking) execution strategy appropriate for the * client API used and the filters added. Blocking is always safe as all potentially blocking paths are - * offloaded. + * offloaded. Each client API variant (async/blocking streaming/aggregate) requires a specific execution + * strategy to avoid blocking the event-loop and filters added via + * {@link #appendClientFilter(StreamingHttpClientFilterFactory)}, + * {@link #appendConnectionFilter(StreamingHttpConnectionFilterFactory)}, or + * {@link #appendConnectionFactoryFilter(ConnectionFactoryFilter)}

, etc. may also require offloading. + * The execution strategy for execution of client requests will be computed based on the client API in use and + * {@link HttpExecutionStrategyInfluencer#requiredOffloads()} of added the filters. * - *
Any other execution strategy - *
Similar to the default case, client request execution will be completed using a computed execution - * strategy. The computed execution strategy will use the specified execution strategy rather than the - * client API's default safe strategy but will still combine with the required execution strategy of any added - * filters. Filters and asynchronous callbacks MAY block - * only during the offloaded portions of the client request execution. Blocking - * MUST NOT be used during other callbacks. + *
{@link HttpExecutionStrategies#offloadNone()} + * (or deprecated {@link HttpExecutionStrategies#offloadNever()}) + *
No offloading will be used during execution of client requests regardless of the client API used or the + * influence of added filters. Filters and asynchronous callbacks + * must not ever block during the execution of client + * requests. + * + *
A custom execution strategy ({@link HttpExecutionStrategies#customStrategyBuilder()}) or + * {@link HttpExecutionStrategies#offloadAll()} + *
The specified execution strategy will be used for executing client requests rather than the client + * API's default safe strategy. Like with the default strategy, the actual execution strategy used is computed + * from the provided strategy and the execution strategies required by added filters. Filters and asynchronous + * callbacks MAY only block during the offloaded portions of + * the client request execution. *
* * @param strategy {@inheritDoc} diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index 94b9a01788..3c4906b561 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -290,11 +290,8 @@ protected Single request( defaultStrategy() == singleStrategy || !singleStrategy.hasOffloads() ? // single client is default or has no *additional* offloads requestStrategy : - requestStrategy.hasOffloads() ? - // add single client offloads to existing strategy - requestStrategy.merge(singleStrategy) : - // IGNORE single client offloads to preserve no-offloads - requestStrategy; + // add single client offloads to existing strategy + requestStrategy.merge(singleStrategy); if (useStrategy != requestStrategy) { LOGGER.debug("Request strategy {} changes to {}. SingleAddressClient strategy: {}", @@ -351,11 +348,8 @@ public Single reserveConnec defaultStrategy() == singleStrategy || !singleStrategy.hasOffloads() ? // single client is default or has no *additional* offloads requestStrategy : - requestStrategy.hasOffloads() ? - // add single client offloads to existing strategy - requestStrategy.merge(singleStrategy) : - // IGNORE single client offloads to preserve no-offloads - requestStrategy; + // add single client offloads to existing strategy + requestStrategy.merge(singleStrategy); if (requestStrategy != useStrategy) { // single client overrides request strategy; @@ -380,11 +374,8 @@ public Single request(final StreamingHttpRequest request) defaultStrategy() == singleStrategy || !singleStrategy.hasOffloads() ? // single client is default or has no *additional* offloads requestStrategy : - requestStrategy.hasOffloads() ? - // add single client offloads to existing strategy - requestStrategy.merge(singleStrategy) : - // IGNORE single client offloads to preserve no-offloads - requestStrategy; + // add single client offloads to existing strategy + requestStrategy.merge(singleStrategy); if (requestStrategy != useStrategy) { // single client overrides request strategy; request.context().put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 6761689b8f..943c754eed 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -394,7 +394,8 @@ private static HttpExecutionStrategy computeClientExecutionStrategy(final Builde } return clientApi.strategy().merge(merged); case MULTI_OFFLOAD_NONE_SINGLE_BUILDER: - return offloadNone(); + return builder == null ? + offloadNone() : defaultStrategy() == merged ? offloadNone() : merged; default: throw new AssertionError("Unexpected builder type: " + builderType); } From 55c5095c35626ef290e24864ec76cbab25d97a31 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Fri, 10 Jun 2022 11:40:16 -0700 Subject: [PATCH 50/54] fix javadoc nit --- .../http/api/SingleAddressHttpClientBuilder.java | 2 +- .../netty/DefaultMultiAddressUrlHttpClientBuilder.java | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java index 142a866d0e..e9ce2cd7dc 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/SingleAddressHttpClientBuilder.java @@ -185,7 +185,7 @@ SingleAddressHttpClientBuilder appendConnectionFilter( * strategy to avoid blocking the event-loop and filters added via * {@link #appendClientFilter(StreamingHttpClientFilterFactory)}, * {@link #appendConnectionFilter(StreamingHttpConnectionFilterFactory)}, or - * {@link #appendConnectionFactoryFilter(ConnectionFactoryFilter)}

, etc. may also require offloading. + * {@link #appendConnectionFactoryFilter(ConnectionFactoryFilter)}, etc. may also require offloading. * The execution strategy for execution of client requests will be computed based on the client API in use and * {@link HttpExecutionStrategyInfluencer#requiredOffloads()} of added the filters. * diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index 3c4906b561..7a8cdaf0d6 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -115,12 +115,12 @@ public StreamingHttpClient buildStreaming() { final CompositeCloseable closeables = newCompositeCloseable(); try { final HttpExecutionContext executionContext = executionContextBuilder.build(); - final ClientFactory clientFactory = new ClientFactory(builderFactory, executionContext, - singleAddressInitializer); + final ClientFactory clientFactory = + new ClientFactory(builderFactory, executionContext, singleAddressInitializer); final CachingKeyFactory keyFactory = closeables.prepend(new CachingKeyFactory()); final HttpHeadersFactory headersFactory = this.headersFactory; FilterableStreamingHttpClient urlClient = closeables.prepend( - new StreamingUrlHttpClient(executionContext, clientFactory, keyFactory, + new StreamingUrlHttpClient(executionContext, keyFactory, clientFactory, new DefaultStreamingHttpRequestResponseFactory(executionContext.bufferAllocator(), headersFactory != null ? headersFactory : DefaultHttpHeadersFactory.INSTANCE, HTTP_1_1))); @@ -318,8 +318,7 @@ private static final class StreamingUrlHttpClient implements FilterableStreaming private final ListenableAsyncCloseable closeable; StreamingUrlHttpClient(final HttpExecutionContext executionContext, - final Function clientFactory, - final CachingKeyFactory keyFactory, + final CachingKeyFactory keyFactory, final ClientFactory clientFactory, final StreamingHttpRequestResponseFactory reqRespFactory) { this.reqRespFactory = requireNonNull(reqRespFactory); this.group = ClientGroup.from(clientFactory); From 239b3aa4833fac95ea4681d4dcdaebbefb347a90 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Fri, 10 Jun 2022 12:01:11 -0700 Subject: [PATCH 51/54] remove redundant modification of the request execution strategy --- .../DefaultMultiAddressUrlHttpClientBuilder.java | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index 7a8cdaf0d6..1f7cf2a615 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -365,21 +365,7 @@ public Single reserveConnec public Single request(final StreamingHttpRequest request) { return defer(() -> { try { - FilterableStreamingHttpClient singleClient = selectClient(request); - HttpExecutionStrategy requestStrategy = request.context().get(HTTP_EXECUTION_STRATEGY_KEY); - HttpExecutionStrategy singleStrategy = singleClient.executionContext().executionStrategy(); - HttpExecutionStrategy useStrategy = defaultStrategy() == requestStrategy ? - offloadAll() : - defaultStrategy() == singleStrategy || !singleStrategy.hasOffloads() ? - // single client is default or has no *additional* offloads - requestStrategy : - // add single client offloads to existing strategy - requestStrategy.merge(singleStrategy); - if (requestStrategy != useStrategy) { - // single client overrides request strategy; - request.context().put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); - } - return singleClient.request(request).shareContextOnSubscribe(); + return selectClient(request).request(request).shareContextOnSubscribe(); } catch (Throwable t) { return Single.failed(t).shareContextOnSubscribe(); } From b6a4c151ff5e96d5510b6a8acda57fdf3b2ce79e Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Fri, 10 Jun 2022 12:02:17 -0700 Subject: [PATCH 52/54] checkstyle nit --- .../io/servicetalk/http/netty/ClientEffectiveStrategyTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java index 943c754eed..9cb3135907 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientEffectiveStrategyTest.java @@ -560,7 +560,7 @@ void recordThread(final ClientOffloadPoint offloadPoint, final HttpExecutionStra public void verifyOffloads(ClientApi clientApi, HttpExecutionStrategy clientStrategy, String apiStrategy) { assertNoAsyncErrors("API=" + clientApi + ", apiStrategy=" + apiStrategy + - ", clientStrategy=" + clientStrategy + + ", clientStrategy=" + clientStrategy + ", expectedStrategy=" + expectedStrategy + ". Async Errors! See suppressed", errors); assertThat("Unexpected offload points recorded. " + invokingThreads, invokingThreads.size(), Matchers.is(ClientOffloadPoint.values().length)); From c38589769120ebdb51f31df12e12da82735eb14b Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Wed, 15 Jun 2022 10:21:44 -0700 Subject: [PATCH 53/54] remove assertion of incompatible behavior --- .../http/api/StreamingHttpClientToBlockingHttpClient.java | 2 -- .../api/StreamingHttpClientToBlockingStreamingHttpClient.java | 2 -- .../servicetalk/http/api/StreamingHttpClientToHttpClient.java | 2 -- ...treamingHttpConnectionToBlockingStreamingHttpConnection.java | 2 -- 4 files changed, 8 deletions(-) diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingHttpClient.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingHttpClient.java index 51ce0610ba..bdc4e24f7c 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingHttpClient.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingHttpClient.java @@ -31,8 +31,6 @@ final class StreamingHttpClientToBlockingHttpClient implements BlockingHttpClien private final HttpRequestResponseFactory reqRespFactory; StreamingHttpClientToBlockingHttpClient(final StreamingHttpClient client, final HttpExecutionStrategy strategy) { - assert client.executionContext().executionStrategy().hasOffloads() || !strategy.hasOffloads() : - "Incompatible client strategy : " + strategy; this.strategy = defaultStrategy() == strategy ? DEFAULT_BLOCKING_CONNECTION_STRATEGY : strategy; this.client = client; context = new DelegatingHttpExecutionContext(client.executionContext()) { diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingStreamingHttpClient.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingStreamingHttpClient.java index b79cbb6ea3..7841b60032 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingStreamingHttpClient.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToBlockingStreamingHttpClient.java @@ -32,8 +32,6 @@ final class StreamingHttpClientToBlockingStreamingHttpClient implements Blocking StreamingHttpClientToBlockingStreamingHttpClient(final StreamingHttpClient client, final HttpExecutionStrategy strategy) { - assert client.executionContext().executionStrategy().hasOffloads() || !strategy.hasOffloads() : - "Incompatible client strategy : " + strategy; this.strategy = defaultStrategy() == strategy ? DEFAULT_BLOCKING_STREAMING_CONNECTION_STRATEGY : strategy; this.client = client; context = new DelegatingHttpExecutionContext(client.executionContext()) { diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToHttpClient.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToHttpClient.java index 09437e8eea..790e0d019f 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToHttpClient.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpClientToHttpClient.java @@ -32,8 +32,6 @@ final class StreamingHttpClientToHttpClient implements HttpClient { private final HttpRequestResponseFactory reqRespFactory; StreamingHttpClientToHttpClient(final StreamingHttpClient client, final HttpExecutionStrategy strategy) { - assert client.executionContext().executionStrategy().hasOffloads() || !strategy.hasOffloads() : - "Incompatible client strategy : " + strategy; this.strategy = defaultStrategy() == strategy ? DEFAULT_ASYNC_CONNECTION_STRATEGY : strategy; this.client = client; context = new DelegatingHttpExecutionContext(client.executionContext()) { diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpConnectionToBlockingStreamingHttpConnection.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpConnectionToBlockingStreamingHttpConnection.java index 8f49f3a936..6733846bed 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpConnectionToBlockingStreamingHttpConnection.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpConnectionToBlockingStreamingHttpConnection.java @@ -32,8 +32,6 @@ final class StreamingHttpConnectionToBlockingStreamingHttpConnection implements StreamingHttpConnectionToBlockingStreamingHttpConnection(final StreamingHttpConnection connection, final HttpExecutionStrategy strategy) { - assert connection.executionContext().executionStrategy().hasOffloads() || !strategy.hasOffloads() : - "Incompatible client strategy : " + strategy; this.strategy = defaultStrategy() == strategy ? DEFAULT_BLOCKING_STREAMING_CONNECTION_STRATEGY : strategy; this.connection = connection; final HttpConnectionContext originalCtx = connection.connectionContext(); From ee1ba16c258cfc402a0130ee70042bd13b494ae1 Mon Sep 17 00:00:00 2001 From: Mike Duigou Date: Wed, 15 Jun 2022 11:18:11 -0700 Subject: [PATCH 54/54] final review feedback --- .../api/MultiAddressHttpClientBuilder.java | 24 +++----- ...faultMultiAddressUrlHttpClientBuilder.java | 55 +++++++++---------- 2 files changed, 34 insertions(+), 45 deletions(-) diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java index fc96478ebb..934dce1830 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/MultiAddressHttpClientBuilder.java @@ -85,27 +85,21 @@ default SingleAddressInitializer append(SingleAddressInitializer toA *
*
Unspecified or {@link HttpExecutionStrategies#defaultStrategy()} *
The resulting client instances will use the default safe strategy for each API variant and - * {@link SingleAddressHttpClientBuilder} instances generated will also have - * default strategy. The computed strategy MAY NOT reduce - * the offloads used for client request execution from the client API safe default. + * {@link SingleAddressHttpClientBuilder} instances generated will also have default strategy. * *
{@link HttpExecutionStrategies#offloadNone()} * (or deprecated {@link HttpExecutionStrategies#offloadNever()}) - *
{@link SingleAddressHttpClientBuilder} instances created by the client will have a default strategy of - * {@link HttpExecutionStrategies#offloadNone()}. An - * {@link #initializer(SingleAddressInitializer) initializer} may override to add offloads using - * {@link SingleAddressHttpClientBuilder#executionStrategy(HttpExecutionStrategy)}. Overriding the execution - * strategy to require offloads will also allowed filters added to influence the computed execution strategy. - * If not overridden by initializer, the resulting computed {@link HttpExecutionStrategies#offloadNone()} - * execution strategy requires that filters - * and asynchronous callbacks must not ever block during - * the execution of client requests. + *
{@link SingleAddressHttpClientBuilder} instances created by the client will have a strategy of + * {@link HttpExecutionStrategies#offloadNone()}. {@link HttpExecutionStrategies#offloadNone()} execution + * strategy requires that filters and asynchronous callbacks + * must not ever block during the execution of client + * requests. An {@link #initializer(SingleAddressInitializer) initializer} may override to add offloads using + * {@link SingleAddressHttpClientBuilder#executionStrategy(HttpExecutionStrategy)}. * *
A custom execution strategy ({@link HttpExecutionStrategies#customStrategyBuilder()}) or * {@link HttpExecutionStrategies#offloadAll()} - *
{@link SingleAddressHttpClientBuilder} instances created by the client will have a default strategy of - * the provided strategy and must result in a computed execution strategy with at least the same offloads as the - * provided. + *
{@link SingleAddressHttpClientBuilder} instances created by the client will start with the provided + * strategy and may add additional offloading as required by added filters. *
* * @param strategy {@inheritDoc} diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java index 1f7cf2a615..606e7be654 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultMultiAddressUrlHttpClientBuilder.java @@ -24,6 +24,7 @@ import io.servicetalk.concurrent.api.ListenableAsyncCloseable; import io.servicetalk.concurrent.api.Single; import io.servicetalk.concurrent.api.internal.SubscribableCompletable; +import io.servicetalk.context.api.ContextMap; import io.servicetalk.http.api.DefaultHttpHeadersFactory; import io.servicetalk.http.api.DefaultStreamingHttpRequestResponseFactory; import io.servicetalk.http.api.FilterableReservedStreamingHttpConnection; @@ -259,6 +260,26 @@ public StreamingHttpClient apply(final UrlKey urlKey) { } } + private static void singleClientStrategyUpdate(ContextMap context, HttpExecutionStrategy singleStrategy) { + HttpExecutionStrategy requestStrategy = context.getOrDefault(HTTP_EXECUTION_STRATEGY_KEY, defaultStrategy()); + assert null != requestStrategy : "Request strategy unexpectedly null"; + HttpExecutionStrategy useStrategy = defaultStrategy() == requestStrategy ? + // For all apis except async streaming default conversion has already been done. + // This is the default to required strategy resolution for the async streaming client. + offloadAll() : + defaultStrategy() == singleStrategy || !singleStrategy.hasOffloads() ? + // single client is default or has no *additional* offloads + requestStrategy : + // add single client offloads to existing strategy + requestStrategy.merge(singleStrategy); + + if (useStrategy != requestStrategy) { + LOGGER.debug("Request strategy {} changes to {}. SingleAddressClient strategy: {}", + requestStrategy, useStrategy, singleStrategy); + context.put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); + } + } + /** * When request transitions from the multi-address level to the single-address level, this filter will make sure * that any missing offloading required by the selected single-address client will be applied for the request @@ -282,22 +303,8 @@ public StreamingHttpClientFilter create(final FilterableStreamingHttpClient clie protected Single request( final StreamingHttpRequester delegate, final StreamingHttpRequest request) { return defer(() -> { - HttpExecutionStrategy singleStrategy = client.executionContext().executionStrategy(); - HttpExecutionStrategy requestStrategy = - request.context().getOrDefault(HTTP_EXECUTION_STRATEGY_KEY, defaultStrategy()); - HttpExecutionStrategy useStrategy = defaultStrategy() == requestStrategy ? - offloadAll() : - defaultStrategy() == singleStrategy || !singleStrategy.hasOffloads() ? - // single client is default or has no *additional* offloads - requestStrategy : - // add single client offloads to existing strategy - requestStrategy.merge(singleStrategy); - - if (useStrategy != requestStrategy) { - LOGGER.debug("Request strategy {} changes to {}. SingleAddressClient strategy: {}", - requestStrategy, useStrategy, singleStrategy); - request.context().put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); - } + singleClientStrategyUpdate(request.context(), client.executionContext().executionStrategy()); + return delegate.request(request); }); } @@ -340,20 +347,8 @@ public Single reserveConnec return defer(() -> { try { FilterableStreamingHttpClient singleClient = selectClient(metaData); - HttpExecutionStrategy requestStrategy = metaData.context().get(HTTP_EXECUTION_STRATEGY_KEY); - HttpExecutionStrategy singleStrategy = singleClient.executionContext().executionStrategy(); - HttpExecutionStrategy useStrategy = defaultStrategy() == requestStrategy ? - offloadAll() : - defaultStrategy() == singleStrategy || !singleStrategy.hasOffloads() ? - // single client is default or has no *additional* offloads - requestStrategy : - // add single client offloads to existing strategy - requestStrategy.merge(singleStrategy); - - if (requestStrategy != useStrategy) { - // single client overrides request strategy; - metaData.context().put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); - } + singleClientStrategyUpdate(metaData.context(), singleClient.executionContext().executionStrategy()); + return singleClient.reserveConnection(metaData).shareContextOnSubscribe(); } catch (Throwable t) { return Single.failed(t).shareContextOnSubscribe();