diff --git a/changelog/@unreleased/pr-2432.v2.yml b/changelog/@unreleased/pr-2432.v2.yml new file mode 100644 index 000000000..47023e11a --- /dev/null +++ b/changelog/@unreleased/pr-2432.v2.yml @@ -0,0 +1,9 @@ +type: improvement +improvement: + description: 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 https://github.com/palantir/dialogue/pull/2398 to also cover ApacheHttpClientBlockingChannel + requests and HttpsProxyDefaultRoutePlanner proxy parsing to leverage a shared + parsed URI cache. + links: + - https://github.com/palantir/dialogue/pull/2432 diff --git a/dialogue-apache-hc5-client/src/main/java/com/palantir/dialogue/hc5/ApacheHttpClientBlockingChannel.java b/dialogue-apache-hc5-client/src/main/java/com/palantir/dialogue/hc5/ApacheHttpClientBlockingChannel.java index 095a7ea1f..26c7df2bd 100644 --- a/dialogue-apache-hc5-client/src/main/java/com/palantir/dialogue/hc5/ApacheHttpClientBlockingChannel.java +++ b/dialogue-apache-hc5-client/src/main/java/com/palantir/dialogue/hc5/ApacheHttpClientBlockingChannel.java @@ -33,7 +33,9 @@ import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.SafeLoggable; +import com.palantir.logsafe.UnsafeArg; import com.palantir.logsafe.exceptions.SafeExceptions; +import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; import com.palantir.logsafe.exceptions.SafeRuntimeException; import com.palantir.logsafe.logger.SafeLogger; import com.palantir.logsafe.logger.SafeLoggerFactory; @@ -45,6 +47,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; +import java.net.URISyntaxException; import java.net.URL; import java.util.Arrays; import java.util.Collections; @@ -67,6 +70,7 @@ import org.apache.hc.core5.http.NoHttpResponseException; import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.hc.core5.net.URIAuthority; final class ApacheHttpClientBlockingChannel implements BlockingChannel { private static final SafeLogger log = SafeLoggerFactory.get(ApacheHttpClientBlockingChannel.class); @@ -100,8 +104,11 @@ 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); - ClassicRequestBuilder builder = - ClassicRequestBuilder.create(endpoint.httpMethod().name()).setUri(target.toString()); + ClassicRequestBuilder builder = ClassicRequestBuilder.create( + endpoint.httpMethod().name()) + .setScheme(target.getProtocol()) + .setAuthority(parseAuthority(target)) + .setPath(target.getFile()); // Fill headers request.headerParams().forEach(builder::addHeader); @@ -167,6 +174,14 @@ public Response execute(Endpoint endpoint, Request request) throws IOException { } } + private static URIAuthority parseAuthority(URL url) { + try { + return URIAuthority.create(url.getAuthority()); + } catch (URISyntaxException e) { + throw new SafeIllegalArgumentException("Invalid URI authority", e, UnsafeArg.of("url", url)); + } + } + private Arg[] failureDiagnosticArgs(Endpoint endpoint, Request request, long startTimeNanos) { long durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNanos); return new Arg[] { @@ -315,7 +330,7 @@ public int code() { public ListMultimap headers() { if (headers == null) { ListMultimap tmpHeaders = MultimapBuilder.treeKeys(String.CASE_INSENSITIVE_ORDER) - .arrayListValues() + .arrayListValues(1) .build(); Iterator
headerIterator = response.headerIterator(); while (headerIterator.hasNext()) { diff --git a/dialogue-apache-hc5-client/src/main/java/com/palantir/dialogue/hc5/HttpsProxyDefaultRoutePlanner.java b/dialogue-apache-hc5-client/src/main/java/com/palantir/dialogue/hc5/HttpsProxyDefaultRoutePlanner.java index d39d345ae..08daccd31 100644 --- a/dialogue-apache-hc5-client/src/main/java/com/palantir/dialogue/hc5/HttpsProxyDefaultRoutePlanner.java +++ b/dialogue-apache-hc5-client/src/main/java/com/palantir/dialogue/hc5/HttpsProxyDefaultRoutePlanner.java @@ -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; @@ -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(); @@ -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 proxies) { Proxy result = null; // check the list for one we can use diff --git a/dialogue-clients/src/main/java/com/palantir/dialogue/clients/DialogueDnsResolutionWorker.java b/dialogue-clients/src/main/java/com/palantir/dialogue/clients/DialogueDnsResolutionWorker.java index b599d8f31..0ec2df87e 100644 --- a/dialogue-clients/src/main/java/com/palantir/dialogue/clients/DialogueDnsResolutionWorker.java +++ b/dialogue-clients/src/main/java/com/palantir/dialogue/clients/DialogueDnsResolutionWorker.java @@ -25,8 +25,9 @@ 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; +import com.palantir.dialogue.core.Uris.MaybeUri; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.Unsafe; @@ -103,7 +104,7 @@ private synchronized void doUpdate(@Nullable INPUT updatedInputState) { long start = System.nanoTime(); ImmutableSet allHosts = spec.extractUris(inputState) .filter(Objects::nonNull) - .map(DnsSupport::tryParseUri) + .map(Uris::tryParse) .filter(MaybeUri::isSuccessful) .filter(Predicate.not(MaybeUri::isMeshMode)) .map(MaybeUri::uri) diff --git a/dialogue-clients/src/main/java/com/palantir/dialogue/clients/DnsPollingSpec.java b/dialogue-clients/src/main/java/com/palantir/dialogue/clients/DnsPollingSpec.java index f6140d7bb..a65d7ff5b 100644 --- a/dialogue-clients/src/main/java/com/palantir/dialogue/clients/DnsPollingSpec.java +++ b/dialogue-clients/src/main/java/com/palantir/dialogue/clients/DnsPollingSpec.java @@ -83,7 +83,8 @@ public String kind() { @Override public Stream extractUris(Optional input) { - return input.stream().flatMap(item -> item.uris().stream()); + return input.map(serviceConfiguration -> serviceConfiguration.uris().stream()) + .orElseGet(Stream::empty); } @Override diff --git a/dialogue-clients/src/main/java/com/palantir/dialogue/clients/DnsSupport.java b/dialogue-clients/src/main/java/com/palantir/dialogue/clients/DnsSupport.java index 271d0abe6..b72136c35 100644 --- a/dialogue-clients/src/main/java/com/palantir/dialogue/clients/DnsSupport.java +++ b/dialogue-clients/src/main/java/com/palantir/dialogue/clients/DnsSupport.java @@ -18,9 +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; import com.google.common.collect.ImmutableList; @@ -32,19 +29,17 @@ 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.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; @@ -90,24 +85,6 @@ 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 uriCache = CacheStats.of( - SharedTaggedMetricRegistries.getSingleton(), "dialogue-uri") - .register(stats -> Caffeine.newBuilder() - .maximumWeight(100_000) - .weigher((key, _value) -> key.length()) - .expireAfterAccess(Duration.ofMinutes(1)) - .softValues() - .recordStats(stats) - .build(DnsSupport::tryParseUri)); - - @VisibleForTesting - static void invalidateCaches() { - uriCache.invalidateAll(); - } - /** Identical to the overload, but using the {@link #sharedScheduler}. */ static Refreshable> pollForChanges( boolean dnsNodeDiscovery, @@ -151,16 +128,6 @@ static Refreshable> 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); - } - @SuppressWarnings("checkstyle:CyclomaticComplexity") static ImmutableList getTargetUris( @Safe String serviceNameForLogging, @@ -181,7 +148,7 @@ static ImmutableList getTargetUris( // are considered equivalent. // When a proxy is used, pre-resolved IP addresses have no impact. In many cases the // proxy handles DNS resolution. - if (resolvedHosts.isEmpty() || DnsSupport.isMeshMode(uri) || usesProxy(proxySelector, parsed)) { + if (resolvedHosts.isEmpty() || Uris.isMeshMode(uri) || usesProxy(proxySelector, parsed)) { targetUris.add(TargetUri.of(uri)); } else { String host = parsed.getHost(); @@ -220,23 +187,11 @@ 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))); - } - } - @Unsafe @Nullable private static URI tryParseUri(TaggedMetricRegistry metrics, @Safe String serviceName, @Unsafe String uri) { try { - MaybeUri maybeUri = uriCache.get(uri); - URI result = maybeUri.uriOrThrow(); + URI result = Uris.tryParse(uri).uriOrThrow(); if (result.getHost() == null) { log.error( "Failed to correctly parse URI {} for service {} due to null host component. " @@ -287,31 +242,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"); - } - } } diff --git a/dialogue-clients/src/test/java/com/palantir/dialogue/clients/DefaultDialogueDnsResolverTest.java b/dialogue-clients/src/test/java/com/palantir/dialogue/clients/DefaultDialogueDnsResolverTest.java index 4f9a9e443..25c9329ca 100644 --- a/dialogue-clients/src/test/java/com/palantir/dialogue/clients/DefaultDialogueDnsResolverTest.java +++ b/dialogue-clients/src/test/java/com/palantir/dialogue/clients/DefaultDialogueDnsResolverTest.java @@ -86,7 +86,7 @@ void unknown_host() { assertThat(result).isEmpty(); ClientDnsMetrics metrics = ClientDnsMetrics.of(registry); - assertThat(metrics.failure("EAI_NONAME").getCount()).isEqualTo(1); + assertThat(metrics.failure("EAI_NONAME").getCount()).isOne(); } @Test @@ -99,12 +99,12 @@ void unknown_host_from_cache() { ImmutableSet result = resolver.resolve(badHost); assertThat(result).isEmpty(); - assertThat(metrics.failure("EAI_NONAME").getCount()).isEqualTo(1); + assertThat(metrics.failure("EAI_NONAME").getCount()).isOne(); // should resolve from cache ImmutableSet result2 = resolver.resolve(badHost); assertThat(result2).isEmpty(); - assertThat(metrics.failure("CACHED").getCount()).isEqualTo(1); + assertThat(metrics.failure("CACHED").getCount()).isOne(); } private static ImmutableSet resolve(String hostname) { diff --git a/dialogue-clients/src/test/java/com/palantir/dialogue/clients/DnsSupportTest.java b/dialogue-clients/src/test/java/com/palantir/dialogue/clients/DnsSupportTest.java deleted file mode 100644 index a5bf89b54..000000000 --- a/dialogue-clients/src/test/java/com/palantir/dialogue/clients/DnsSupportTest.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * (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.clients; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.palantir.dialogue.core.TargetUri; -import com.palantir.tritium.metrics.registry.DefaultTaggedMetricRegistry; -import com.palantir.tritium.metrics.registry.TaggedMetricRegistry; -import java.net.URI; -import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -class DnsSupportTest { - - private final TaggedMetricRegistry taggedMetrics = new DefaultTaggedMetricRegistry(); - - @BeforeEach - void before() { - DnsSupport.invalidateCaches(); - } - - @ParameterizedTest - @CsvSource({ - "github.com,https://github.com", - "github.com,https://github.com:443", - "github.com,https://github.com:8080", - "github.com,https://github.com/palantir/dialogue/", - "github.com,https://github.com:443/palantir/dialogue/", - "github.com,https://github.com:8080/palantir/dialogue/", - "github.com,mesh-https://github.com/palantir/dialogue/", - "github.com,mesh-https://github.com:443/palantir/dialogue/", - "github.com,mesh-https://github.com:8080/palantir/dialogue/", - }) - void tryGetHost(String expectedHostname, String input) { - assertThat(DnsSupport.tryParseUri(input)).satisfies(parsed -> { - assertThat(parsed.isSuccessful()).isTrue(); - assertThat(parsed.uri()).isNotNull(); - assertThat(parsed.uri().getHost()).isEqualTo(expectedHostname); - assertThat(parsed.exception()).isNull(); - assertThat(parsed.isMeshMode()).isEqualTo(input.startsWith("mesh-")); - }); - - assertThat(DnsSupport.getTargetUris( - "test", - List.of(input), - DnsSupport.proxySelector(Optional.empty()), - Optional.empty(), - taggedMetrics)) - .hasSize(1) - .containsExactly(TargetUri.of(URI.create(input).toString())); - } - - @ParameterizedTest - @CsvSource({ - "false,https://github.com", - "false,https://github.com:443", - "false,https://github.com:8080", - "false,https://github.com/palantir/dialogue/", - "false,https://github.com:443/palantir/dialogue/", - "false,https://github.com:8080/palantir/dialogue/", - "true,mesh-https://github.com/palantir/dialogue/", - "true,mesh-https://github.com:443/palantir/dialogue/", - "true,mesh-https://github.com:8080/palantir/dialogue/", - }) - void isMeshMode(boolean expected, String input) { - assertThat(DnsSupport.isMeshMode(input)).isEqualTo(expected).isEqualTo(DnsSupport.isMeshMode(input)); - } -} diff --git a/dialogue-core/build.gradle b/dialogue-core/build.gradle index ad386a123..6a98230a9 100644 --- a/dialogue-core/build.gradle +++ b/dialogue-core/build.gradle @@ -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' diff --git a/dialogue-core/src/main/java/com/palantir/dialogue/core/Uris.java b/dialogue-core/src/main/java/com/palantir/dialogue/core/Uris.java new file mode 100644 index 000000000..cf8e3ff4b --- /dev/null +++ b/dialogue-core/src/main/java/com/palantir/dialogue/core/Uris.java @@ -0,0 +1,137 @@ +/* + * (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.Arg; +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 uriCache = CacheStats.of( + SharedTaggedMetricRegistries.getSingleton(), "dialogue-uri") + .register(stats -> Caffeine.newBuilder() + .maximumWeight(100_000) + .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 new SafeIllegalArgumentException( + "Failed to parse URI", exception, exception.getArgs().toArray(new Arg[0])); + } + 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) { + // do not cache stacktrace elements + exception.setStackTrace(new StackTraceElement[0]); + return ImmutableMaybeUri.of(null, exception); + } + } + + private Uris() {} +} diff --git a/dialogue-core/src/test/java/com/palantir/dialogue/core/UrisTest.java b/dialogue-core/src/test/java/com/palantir/dialogue/core/UrisTest.java new file mode 100644 index 000000000..59f27863b --- /dev/null +++ b/dialogue-core/src/test/java/com/palantir/dialogue/core/UrisTest.java @@ -0,0 +1,128 @@ +/* + * (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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class UrisTest { + + @BeforeEach + void before() { + Uris.clearCache(); + } + + @AfterEach + void after() { + Uris.clearCache(); + } + + @ParameterizedTest + @ValueSource( + strings = { + "http://www.palantir.com/", + "https://www.palantir.com", + "https://github.com/palantir", + "https://www.example.com/foo/bar/baz", + }) + void parsesValidUris(String input) { + assertThat(Uris.tryParse(input)).isNotNull().satisfies(parsed -> { + assertThat(parsed.isSuccessful()).isTrue(); + assertThat(parsed.isMeshMode()).isFalse(); + assertThat(parsed.exception()).isNull(); + assertThat(parsed.uri()).isNotNull(); + assertThat(parsed.uriOrThrow()).isNotNull(); + }); + } + + @ParameterizedTest + @ValueSource( + strings = { + "mesh-http://www.palantir.com/", + "mesh-https://www.palantir.com", + "mesh-https://github.com/palantir", + "mesh-https://www.example.com/foo/bar/baz", + }) + void parsesMeshUris(String input) { + assertThat(Uris.tryParse(input)).isNotNull().satisfies(parsed -> { + assertThat(parsed.isSuccessful()).isTrue(); + assertThat(parsed.isMeshMode()).isTrue(); + assertThat(parsed.exception()).isNull(); + assertThat(parsed.uri()).isNotNull(); + assertThat(parsed.uriOrThrow()).isNotNull(); + }); + } + + @ParameterizedTest + @ValueSource(strings = {" x ", " ", "foobar://"}) + void parsesInvalidUris(String input) { + assertThat(Uris.tryParse(input)).isNotNull().satisfies(parsed -> { + assertThat(parsed.isSuccessful()).isFalse(); + assertThat(parsed.isMeshMode()).isFalse(); + assertThat(parsed.exception()).isNotNull(); + assertThat(parsed.uri()).isNull(); + assertThatThrownBy(parsed::uriOrThrow) + .isInstanceOf(SafeIllegalArgumentException.class) + .hasMessageContaining("Failed to parse URI"); + }); + } + + @ParameterizedTest + @CsvSource({ + "github.com,https://github.com", + "github.com,https://github.com:443", + "github.com,https://github.com:8080", + "github.com,https://github.com/palantir/dialogue/", + "github.com,https://github.com:443/palantir/dialogue/", + "github.com,https://github.com:8080/palantir/dialogue/", + "github.com,mesh-https://github.com/palantir/dialogue/", + "github.com,mesh-https://github.com:443/palantir/dialogue/", + "github.com,mesh-https://github.com:8080/palantir/dialogue/", + }) + void tryGetHost(String expectedHostname, String input) { + assertThat(Uris.tryParse(input)).satisfies(parsed -> { + assertThat(parsed.isSuccessful()).isTrue(); + assertThat(parsed.uri()).isNotNull(); + assertThat(parsed.uri().getHost()).isEqualTo(expectedHostname); + assertThat(parsed.exception()).isNull(); + assertThat(parsed.isMeshMode()).isEqualTo(input.startsWith("mesh-")); + }); + } + + @ParameterizedTest + @CsvSource({ + "false,https://github.com", + "false,https://github.com:443", + "false,https://github.com:8080", + "false,https://github.com/palantir/dialogue/", + "false,https://github.com:443/palantir/dialogue/", + "false,https://github.com:8080/palantir/dialogue/", + "true,mesh-https://github.com/palantir/dialogue/", + "true,mesh-https://github.com:443/palantir/dialogue/", + "true,mesh-https://github.com:8080/palantir/dialogue/", + }) + void isMeshMode(boolean expected, String input) { + assertThat(Uris.isMeshMode(input)).isEqualTo(expected).isEqualTo(Uris.isMeshMode(input)); + } +}