Skip to content

Commit

Permalink
Cache parsed URIs throughout Dialogue
Browse files Browse the repository at this point in the history
Parsing String to URI can be expensive in terms of CPU and allocations
for high throughput services. This generalizes the caching previously
added to DnsSupport in #2398 to
also cover ApacheHttpClientBlockingChannel requests and
HttpsProxyDefaultRoutePlanner proxy parsing to leverage a shared parsed
URI cache.
  • Loading branch information
schlosna committed Nov 22, 2024
1 parent 540a4ba commit 8a9e95e
Show file tree
Hide file tree
Showing 8 changed files with 297 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.palantir.dialogue.ResponseAttachments;
import com.palantir.dialogue.blocking.BlockingChannel;
import com.palantir.dialogue.core.BaseUrl;
import com.palantir.dialogue.core.Uris;
import com.palantir.logsafe.Arg;
import com.palantir.logsafe.Preconditions;
import com.palantir.logsafe.SafeArg;
Expand All @@ -45,6 +46,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.URI;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -100,8 +102,9 @@ final class ApacheHttpClientBlockingChannel implements BlockingChannel {
public Response execute(Endpoint endpoint, Request request) throws IOException {
// Create base request given the URL
URL target = baseUrl.render(endpoint, request);
URI targetUri = Uris.tryParse(target.toString()).uriOrThrow();
ClassicRequestBuilder builder =
ClassicRequestBuilder.create(endpoint.httpMethod().name()).setUri(target.toString());
ClassicRequestBuilder.create(endpoint.httpMethod().name()).setUri(targetUri);

// Fill headers
request.headerParams().forEach(builder::addHeader);
Expand Down Expand Up @@ -315,7 +318,7 @@ public int code() {
public ListMultimap<String, String> headers() {
if (headers == null) {
ListMultimap<String, String> tmpHeaders = MultimapBuilder.treeKeys(String.CASE_INSENSITIVE_ORDER)
.arrayListValues()
.arrayListValues(1)
.build();
Iterator<Header> headerIterator = response.headerIterator();
while (headerIterator.hasNext()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@
package com.palantir.dialogue.hc5;

import com.palantir.conjure.java.client.config.HttpsProxies;
import com.palantir.dialogue.core.Uris;
import com.palantir.dialogue.core.Uris.MaybeUri;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import javax.annotation.CheckForNull;
import org.apache.hc.client5.http.impl.routing.DefaultRoutePlanner;
Expand Down Expand Up @@ -49,12 +50,8 @@ final class HttpsProxyDefaultRoutePlanner extends DefaultRoutePlanner {
@Override
@CheckForNull
public HttpHost determineProxy(final HttpHost target, final HttpContext _context) throws HttpException {
final URI targetUri;
try {
targetUri = new URI(target.toURI());
} catch (final URISyntaxException ex) {
throw new HttpException("Cannot convert host to URI: " + target, ex);
}
final URI targetUri = parseTargetUri(target);

ProxySelector proxySelectorInstance = this.proxySelector;
if (proxySelectorInstance == null) {
proxySelectorInstance = ProxySelector.getDefault();
Expand All @@ -79,6 +76,15 @@ public HttpHost determineProxy(final HttpHost target, final HttpContext _context
return result;
}

private static URI parseTargetUri(HttpHost target) throws HttpException {
MaybeUri maybeUri = Uris.tryParse(target.toString());
if (maybeUri.isSuccessful()) {
return maybeUri.uriOrThrow();
} else {
throw new HttpException("Cannot convert host to URI: " + target, maybeUri.exception());
}
}

private Proxy chooseProxy(final List<Proxy> proxies) {
Proxy result = null;
// check the list for one we can use
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import com.palantir.dialogue.clients.DnsSupport.MaybeUri;
import com.palantir.dialogue.core.DialogueDnsResolver;
import com.palantir.dialogue.core.Uris.MaybeUri;
import com.palantir.logsafe.Safe;
import com.palantir.logsafe.SafeArg;
import com.palantir.logsafe.Unsafe;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ public String kind() {

@Override
public Stream<String> extractUris(Optional<ServiceConfiguration> input) {
return input.stream().flatMap(item -> item.uris().stream());
return input.map(serviceConfiguration -> serviceConfiguration.uris().stream())
.orElseGet(Stream::empty);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@

import com.codahale.metrics.Counter;
import com.codahale.metrics.Timer;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Suppliers;
import com.google.common.collect.Collections2;
Expand All @@ -32,19 +30,18 @@
import com.palantir.dialogue.core.DialogueDnsResolver;
import com.palantir.dialogue.core.DialogueExecutors;
import com.palantir.dialogue.core.TargetUri;
import com.palantir.logsafe.Preconditions;
import com.palantir.dialogue.core.Uris;
import com.palantir.dialogue.core.Uris.MaybeUri;
import com.palantir.logsafe.Safe;
import com.palantir.logsafe.SafeArg;
import com.palantir.logsafe.Unsafe;
import com.palantir.logsafe.UnsafeArg;
import com.palantir.logsafe.exceptions.SafeIllegalArgumentException;
import com.palantir.logsafe.logger.SafeLogger;
import com.palantir.logsafe.logger.SafeLoggerFactory;
import com.palantir.refreshable.Disposable;
import com.palantir.refreshable.Refreshable;
import com.palantir.refreshable.SettableRefreshable;
import com.palantir.tritium.metrics.MetricRegistries;
import com.palantir.tritium.metrics.caffeine.CacheStats;
import com.palantir.tritium.metrics.registry.SharedTaggedMetricRegistries;
import com.palantir.tritium.metrics.registry.TaggedMetricRegistry;
import java.lang.ref.Cleaner;
Expand Down Expand Up @@ -90,22 +87,9 @@ final class DnsSupport {
.build(),
SCHEDULER_NAME)));

/**
* Shared cache of string to parsed URI. This avoids excessive allocation overhead when parsing repeated targets.
*/
private static final LoadingCache<String, MaybeUri> uriCache = CacheStats.of(
SharedTaggedMetricRegistries.getSingleton(), "dialogue-uri")
.register(stats -> Caffeine.newBuilder()
.maximumWeight(100_000)
.<String, MaybeUri>weigher((key, _value) -> key.length())
.expireAfterAccess(Duration.ofMinutes(1))
.softValues()
.recordStats(stats)
.build(DnsSupport::tryParseUri));

@VisibleForTesting
static void invalidateCaches() {
uriCache.invalidateAll();
Uris.clearCache();
}

/** Identical to the overload, but using the {@link #sharedScheduler}. */
Expand Down Expand Up @@ -151,14 +135,8 @@ static <I> Refreshable<DnsResolutionResults<I>> pollForChanges(
return dnsResolutionResult;
}

/**
* This prefix may reconfigure several aspects of the client to work better in a world where requests are routed
* through a service mesh like istio/envoy.
*/
private static final String MESH_PREFIX = "mesh-";

static boolean isMeshMode(String uri) {
return uri.startsWith(MESH_PREFIX);
return Uris.isMeshMode(uri);
}

@SuppressWarnings("checkstyle:CyclomaticComplexity")
Expand Down Expand Up @@ -222,20 +200,14 @@ private static boolean usesProxy(ProxySelector proxySelector, URI uri) {

@Unsafe
static MaybeUri tryParseUri(@Unsafe String uriString) {
try {
return MaybeUri.success(new URI(uriString));
} catch (Exception e) {
log.debug("Failed to parse URI", e);
return MaybeUri.failure(
new SafeIllegalArgumentException("Failed to parse URI", e, UnsafeArg.of("uri", uriString)));
}
return Uris.tryParse(uriString);
}

@Unsafe
@Nullable
private static URI tryParseUri(TaggedMetricRegistry metrics, @Safe String serviceName, @Unsafe String uri) {
try {
MaybeUri maybeUri = uriCache.get(uri);
MaybeUri maybeUri = Uris.tryParse(uri);
URI result = maybeUri.uriOrThrow();
if (result.getHost() == null) {
log.error(
Expand Down Expand Up @@ -287,31 +259,4 @@ public void run() {
}
}
}

@Unsafe
record MaybeUri(@Nullable URI uri, @Nullable SafeIllegalArgumentException exception) {
static @Unsafe DnsSupport.MaybeUri success(URI uri) {
return new MaybeUri(uri, null);
}

static @Unsafe DnsSupport.MaybeUri failure(SafeIllegalArgumentException exception) {
return new MaybeUri(null, exception);
}

boolean isSuccessful() {
return uri() != null;
}

boolean isMeshMode() {
return uri() != null && DnsSupport.isMeshMode(uri().getScheme());
}

@Unsafe
URI uriOrThrow() {
if (exception() != null) {
throw exception();
}
return Preconditions.checkNotNull(uri(), "uri");
}
}
}
17 changes: 9 additions & 8 deletions dialogue-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@ dependencies {
api 'com.palantir.conjure.java.runtime:client-config'
implementation project(':dialogue-futures')
implementation 'com.github.ben-manes.caffeine:caffeine'
implementation 'com.google.code.findbugs:jsr305'
implementation 'com.google.errorprone:error_prone_annotations'
implementation 'com.google.guava:guava'
implementation 'com.palantir.conjure.java.api:errors'
implementation 'com.palantir.conjure.java.api:service-config'
implementation 'com.palantir.refreshable:refreshable'
implementation 'com.palantir.safe-logging:logger'
implementation 'com.palantir.safe-logging:preconditions'
implementation 'com.palantir.safe-logging:safe-logging'
implementation 'com.palantir.tracing:tracing'
implementation 'io.dropwizard.metrics:metrics-core'
implementation 'com.palantir.safethreadlocalrandom:safe-thread-local-random'
implementation 'com.palantir.tritium:tritium-metrics'
implementation 'com.google.code.findbugs:jsr305'
implementation 'com.google.errorprone:error_prone_annotations'
implementation 'com.palantir.conjure.java.api:errors'
implementation 'com.palantir.conjure.java.api:service-config'
implementation 'com.palantir.tracing:tracing'
implementation 'com.palantir.tracing:tracing-api'
implementation 'com.palantir.refreshable:refreshable'
implementation 'com.palantir.tritium:tritium-caffeine'
implementation 'com.palantir.tritium:tritium-metrics'
implementation 'io.dropwizard.metrics:metrics-core'

testImplementation 'com.palantir.tracing:tracing-test-utils'
testImplementation 'com.palantir.safe-logging:preconditions-assertj'
Expand Down
133 changes: 133 additions & 0 deletions dialogue-core/src/main/java/com/palantir/dialogue/core/Uris.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* (c) Copyright 2024 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.dialogue.core;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.palantir.dialogue.DialogueImmutablesStyle;
import com.palantir.logsafe.Preconditions;
import com.palantir.logsafe.Unsafe;
import com.palantir.logsafe.UnsafeArg;
import com.palantir.logsafe.exceptions.SafeIllegalArgumentException;
import com.palantir.logsafe.logger.SafeLogger;
import com.palantir.logsafe.logger.SafeLoggerFactory;
import com.palantir.tritium.metrics.caffeine.CacheStats;
import com.palantir.tritium.metrics.registry.SharedTaggedMetricRegistries;
import java.net.URI;
import java.time.Duration;
import javax.annotation.Nullable;
import org.immutables.value.Value;

public final class Uris {
private static final SafeLogger log = SafeLoggerFactory.get(Uris.class);

/**
* This prefix may reconfigure several aspects of the client to work better in a world where requests are routed
* through a service mesh like istio/envoy.
*/
private static final String MESH_PREFIX = "mesh-";

/**
* Shared cache of string to parsed URI. This avoids excessive allocation overhead when parsing repeated targets.
*/
private static final LoadingCache<String, MaybeUri> uriCache = CacheStats.of(
SharedTaggedMetricRegistries.getSingleton(), "dialogue-uri")
.register(stats -> Caffeine.newBuilder()
.maximumWeight(100_000)
.<String, MaybeUri>weigher((key, _value) -> key.length())
.expireAfterAccess(Duration.ofMinutes(1))
.softValues()
.recordStats(stats)
.build(Uris::parse));

@Unsafe
public static MaybeUri tryParse(@Unsafe String uriString) {
return uriCache.get(uriString);
}

@Unsafe
private static MaybeUri parse(String uriString) {
try {
return MaybeUri.success(new URI(uriString));
} catch (Exception e) {
log.debug("Failed to parse URI", e);
return MaybeUri.failure(
new SafeIllegalArgumentException("Failed to parse URI", e, UnsafeArg.of("uri", uriString)));
}
}

public static void clearCache() {
uriCache.invalidateAll();
}

/**
* Returns true if the specified URI string is a mesh-mode formatted URI, configured to route through a
* service mesh like istio/envoy.
*/
public static boolean isMeshMode(String uri) {
return uri.startsWith(MESH_PREFIX);
}

@Unsafe
@Value.Immutable(builder = false)
@DialogueImmutablesStyle
public interface MaybeUri {
@Value.Parameter
@Nullable
URI uri();

@Value.Parameter
@Nullable
SafeIllegalArgumentException exception();

@Value.Derived
default boolean isSuccessful() {
return uri() != null;
}

@Value.Derived
default boolean isMeshMode() {
URI uri = uri();
return uri != null && Uris.isMeshMode(uri.getScheme());
}

@Unsafe
@Value.Auxiliary
default URI uriOrThrow() {
SafeIllegalArgumentException exception = exception();
if (exception != null) {
throw exception;
}
return Preconditions.checkNotNull(uri(), "uri");
}

@Value.Check
default void check() {
Preconditions.checkState(uri() != null ^ exception() != null, "Only one of uri or exception can be null");
}

static @Unsafe MaybeUri success(URI uri) {
return ImmutableMaybeUri.of(uri, null);
}

static @Unsafe MaybeUri failure(SafeIllegalArgumentException exception) {
return ImmutableMaybeUri.of(null, exception);
}
}

private Uris() {}
}
Loading

0 comments on commit 8a9e95e

Please sign in to comment.