Skip to content

Reuse the per-request partition key instead of recomputing it#2231

Open
pavel-ptashyts wants to merge 1 commit into
AsyncHttpClient:mainfrom
maygemdev:perf/reuse-partition-key
Open

Reuse the per-request partition key instead of recomputing it#2231
pavel-ptashyts wants to merge 1 commit into
AsyncHttpClient:mainfrom
maygemdev:perf/reuse-partition-key

Conversation

@pavel-ptashyts

Copy link
Copy Markdown
Contributor

The base (host/scheme/port) partition key was recomputed — and a fresh key object allocated — at every site that needs it within one request: the connection-semaphore acquire, each pool poll, the completion offer, and the HTTP/2 registration. In pollPooledChannel it was computed twice on a single H2-miss (once for the HTTP/2 registry poll, once for the HTTP/1.1 pool poll). The key is immutable for a given target, so all but the first computation were pure young-gen churn.

NettyResponseFuture.basePartitionKey() now memoizes the key. It depends only on targetRequest (host/scheme/port + virtualHost) and the final proxyServer, so it is invalidated in exactly one place — setTargetRequest, the sole post-construction writer of targetRequest (e.g. a redirect/retry to a different host). Behaviour is unchanged: the memoized key equals a fresh computation and is refreshed whenever the target changes, so a redirect can never reuse a connection keyed to the previous host. pollPooledChannel computes the base key once and passes it to the pollHttp2Connection(key)/poll(key) overloads instead of recomputing it inside each channelManager call.

Adds a NettyResponseFuture unit test that the base key is memoized (same instance on repeat calls), equals a fresh computation, and is invalidated to the new host's key on setTargetRequest. Existing connection-pool, redirect-connection-usage and round-robin integration tests pass unchanged. No public API change.

The base (host/scheme/port) partition key was recomputed — and a fresh key object allocated — at every site that needs it within one request: the connection-semaphore acquire, each pool poll, the completion offer, and the HTTP/2 registration. In pollPooledChannel it was computed twice on a single H2-miss (once for the HTTP/2 registry poll, once for the HTTP/1.1 pool poll). The key is immutable for a given target, so all but the first computation were pure young-gen churn.

NettyResponseFuture.basePartitionKey() now memoizes the key. It depends only on targetRequest (host/scheme/port + virtualHost) and the final proxyServer, so it is invalidated in exactly one place — setTargetRequest, the sole post-construction writer of targetRequest (e.g. a redirect/retry to a different host). Behaviour is unchanged: the memoized key equals a fresh computation and is refreshed whenever the target changes, so a redirect can never reuse a connection keyed to the previous host. pollPooledChannel computes the base key once and passes it to the pollHttp2Connection(key)/poll(key) overloads instead of recomputing it inside each channelManager call.

Adds a NettyResponseFuture unit test that the base key is memoized (same instance on repeat calls), equals a fresh computation, and is invalidated to the new host's key on setTargetRequest. Existing connection-pool, redirect-connection-usage and round-robin integration tests pass unchanged. No public API change.

Fixes finding AsyncHttpClient#7.
private volatile List<InetSocketAddress> roundRobinAddresses;
private volatile Uri roundRobinBaseUri;
private volatile ScramContext scramContext;
// Memoized base (host/scheme/port) partition key; see basePartitionKey(). proxyServer is final and

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment lists "pool poll/offer" as sites that use this memoized key, but pollPooledChannel never calls basePartitionKey(). It computes its own key from the request parameter, and that is intentional. On the filter replay path (replayRequestsendNextRequest), a filter can replace the request without setTargetRequest() ever being called, so the future's memoized key may still correspond to the original target.

If someone later "fixes" pollPooledChannel to use basePartitionKey() based on this comment, a replayed request could poll a pooled connection using the wrong host's key. Could you reword the comment to mention only the actual consumers (semaphore acquisition, pool offer, and HTTP/2 registration)? It would also be worth adding a short note in pollPooledChannel explaining that it intentionally derives the key from the current request.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java:451

After this change, poll(Uri, String, ProxyServer, ChannelPoolPartitioning) has no remaining internal callers. Since ChannelManager is a public class, we shouldn't remove it, but it would be good to mark it @Deprecated and add Javadoc pointing callers to poll(Object) instead. That helps prevent it from being reintroduced internally and sets it up for removal in the next major release.

The same could be considered for pollHttp2(Uri, ...) at line 446 once its remaining internal caller (NettyRequestSender around line 1162, in the waitForHttp2Connection path) has been migrated to pollHttp2Connection(Object).

if (addresses.size() > 1) {
ordered = rrSelector.rotate(host, addresses);
InetAddress chosen = ordered.get(0).getAddress();
Object baseKey = request.getChannelPoolPartitioning().getPartitionKey(uri, request.getVirtualHost(), proxyServer);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small follow-up in the same spirit as this PR: the round-robin path still recomputes the base key inline here, even though newFuture is already in scope. newFuture.basePartitionKey() returns an equivalent key (targetRequest, partitioning, and proxyServer are all fixed at construction) and would also warm the memo for the later per-host semaphore acquisition.

This is a minor optimization since it only applies with LoadBalance.ROUND_ROBIN and multiple resolved IPs, but it removes the same redundant allocation that this PR eliminates elsewhere.

// poll/offer, HTTP/2 registration). It depends only on targetRequest (host/scheme/port + virtualHost)
// and the final proxyServer, so it is recomputed only when setTargetRequest changes the target.
Object key = basePartitionKeyCache;
if (key == null) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a bug today, since all reads and writes to this cache are serialized by the one-at-a-time request lifecycle. However, the lazy check-then-store pattern only remains safe as long as that invariant holds.

Computing the key eagerly in the constructor and in setTargetRequest(), and making basePartitionKey() a simple read, would provide the same allocation savings without introducing a reader-side write. It also removes any concurrency considerations if the threading model changes in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants