All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.palantir.dialogue.clients.ChannelCache Maven / Gradle / Ivy

/*
 * (c) Copyright 2020 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 com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.annotations.VisibleForTesting;
import com.palantir.conjure.java.api.config.service.ServiceConfiguration;
import com.palantir.conjure.java.client.config.ClientConfiguration;
import com.palantir.dialogue.core.DialogueChannel;
import com.palantir.dialogue.core.DialogueDnsResolver;
import com.palantir.dialogue.core.TargetUri;
import com.palantir.dialogue.hc5.ApacheHttpClientChannels;
import com.palantir.logsafe.DoNotLog;
import com.palantir.logsafe.Preconditions;
import com.palantir.logsafe.Safe;
import com.palantir.logsafe.SafeArg;
import com.palantir.logsafe.Unsafe;
import com.palantir.logsafe.exceptions.SafeRuntimeException;
import com.palantir.logsafe.logger.SafeLogger;
import com.palantir.logsafe.logger.SafeLoggerFactory;
import com.palantir.refreshable.Refreshable;
import java.io.IOException;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import javax.annotation.concurrent.ThreadSafe;
import org.immutables.value.Value;

@ThreadSafe
final class ChannelCache {
    private static final SafeLogger log = SafeLoggerFactory.get(ChannelCache.class);

    /** Arbitrary bound to avoid runaway OOM. Creating more than this is still allowed, will just cause cache misses. */
    private static final int MAX_CACHED_CHANNELS = 1000;

    /** Ideally there should only be one ChannelCache per JVM, this AtomicInteger & WeakSet helps us spot extras. */
    private static final AtomicInteger INSTANCE_NUMBER = new AtomicInteger(0);

    private static final Set LIVE_INSTANCES =
            Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));

    /**
     * apacheCache is indexed by channel name so we effectively have a per-service cache of size 1. Slightly
     * dangerous because we could flip back and forth if params are different, but allows us to close evicted clients
     * nicely.
     */
    private final Map apacheCache = new ConcurrentHashMap<>();

    private final LoadingCache channelCache = Caffeine.newBuilder()
            .maximumSize(MAX_CACHED_CHANNELS)
            // Avoid holding onto old targets, which is now more common as we bind to resolved IP addresses
            .weakValues()
            .build(this::createNonLiveReloadingChannel);
    private final int instanceNumber;

    private ChannelCache() {
        this.instanceNumber = INSTANCE_NUMBER.incrementAndGet(); // 1 indexed for human readability
    }

    static ChannelCache createEmptyCache() {
        ChannelCache newCache = new ChannelCache();
        LIVE_INSTANCES.add(newCache);

        int numLiveInstances = LIVE_INSTANCES.size();
        if ((numLiveInstances > 5 && log.isInfoEnabled()) || log.isDebugEnabled()) {
            if (numLiveInstances >= 10) {
                log.info(
                        "Created ChannelCache instance #{} ({} alive): {}",
                        SafeArg.of("instanceNumber", newCache.instanceNumber),
                        SafeArg.of("totalAliveNow", numLiveInstances),
                        SafeArg.of("newCache", newCache),
                        new SafeRuntimeException("ChannelCache constructed here"));
            } else {
                log.info(
                        "Created ChannelCache instance #{} ({} alive): {}",
                        SafeArg.of("instanceNumber", newCache.instanceNumber),
                        SafeArg.of("totalAliveNow", numLiveInstances),
                        SafeArg.of("newCache", newCache));
            }
        }

        return newCache;
    }

    DialogueChannel getNonReloadingChannel(
            ReloadingClientFactory.ReloadingParams reloadingParams,
            ServiceConfiguration serviceConf,
            @Safe String channelName) {
        return getNonReloadingChannel(reloadingParams, serviceConf, channelName, Optional.empty());
    }

    DialogueChannel getNonReloadingChannel(
            ReloadingClientFactory.ReloadingParams reloadingParams,
            ServiceConfiguration serviceConf,
            @Safe String channelName,
            Optional overrideHostIndex) {
        if (log.isWarnEnabled()) {
            long estimatedSize = channelCache.estimatedSize();
            if (estimatedSize >= MAX_CACHED_CHANNELS * 0.75) {
                log.warn(
                        "channelCache nearing capacity - possible bug? {} {} {}",
                        SafeArg.of("estimatedSize", estimatedSize),
                        SafeArg.of("maxSize", MAX_CACHED_CHANNELS),
                        SafeArg.of("cache", this));
            }
        }

        return channelCache.get(ImmutableChannelCacheKey.builder()
                .from(reloadingParams)
                .blockingExecutor(reloadingParams.blockingExecutor())
                .serviceConf(serviceConf)
                .channelName(channelName)
                .overrideHostIndex(overrideHostIndex)
                .dnsResolver(reloadingParams.dnsResolver())
                .dnsRefreshInterval(reloadingParams.dnsRefreshInterval())
                .dnsNodeDiscovery(overrideHostIndex.isEmpty() && reloadingParams.dnsNodeDiscovery())
                .build());
    }

    private DialogueChannel createNonLiveReloadingChannel(ChannelCacheKey channelCacheRequest) {
        ImmutableApacheClientRequest request = ImmutableApacheClientRequest.builder()
                .from(channelCacheRequest)
                .channelName(channelCacheRequest.channelName())
                .serviceConf(stripUris(channelCacheRequest.serviceConf())) // we strip out uris to maximise cache hits
                .blockingExecutor(channelCacheRequest.blockingExecutor())
                .dnsResolver(channelCacheRequest.dnsResolver())
                .build();

        ApacheCacheEntry apacheClient = getApacheClient(request);

        Refreshable> targets;
        if (channelCacheRequest.overrideHostIndex().isPresent()) {
            targets = Refreshable.only(
                    List.of(channelCacheRequest.overrideHostIndex().get().target()));
        } else {
            DnsPollingSpec spec = DnsPollingSpec.serviceConfig(channelCacheRequest.channelName());
            targets = DnsSupport.pollForChanges(
                            channelCacheRequest.dnsNodeDiscovery(),
                            spec,
                            channelCacheRequest.dnsResolver(),
                            channelCacheRequest.dnsRefreshInterval(),
                            channelCacheRequest.taggedMetrics(),
                            Refreshable.only(channelCacheRequest.serviceConf()))
                    .map(dnsResolutionResults -> DnsSupport.getTargetUris(
                            channelCacheRequest.channelName(),
                            channelCacheRequest.serviceConf().uris(),
                            DnsSupport.proxySelector(
                                    channelCacheRequest.serviceConf().proxy()),
                            dnsResolutionResults.resolvedHosts(),
                            channelCacheRequest.taggedMetrics()));
        }
        return DialogueChannel.builder()
                .channelName(channelCacheRequest.channelName())
                .clientConfiguration(ClientConfiguration.builder()
                        .from(apacheClient.conf())
                        .uris(channelCacheRequest.serviceConf().uris()) // restore uris
                        .build())
                .uris(targets)
                .factory(args -> ApacheHttpClientChannels.createSingleUri(args, apacheClient.client()))
                .overrideHostIndex(channelCacheRequest.overrideHostIndex().stream()
                        .mapToInt(OverrideHostIndex::index)
                        .findAny())
                .build();
    }

    @VisibleForTesting
    ApacheCacheEntry getApacheClient(ImmutableApacheClientRequest request) {
        Optional cacheEntry = Optional.ofNullable(apacheCache.get(request.channelName()));
        if (log.isDebugEnabled()) {
            log.debug(
                    "Lookup in apacheCache for {} (size {}) hit {}. apacheCacheKeys: {}",
                    SafeArg.of("channelName", request.channelName()),
                    SafeArg.of("size", apacheCache.size()),
                    SafeArg.of("hit", cacheEntry.isPresent()),
                    SafeArg.of("apacheCacheKeys", apacheCache.keySet()));
        }

        // real equality not reference equality!
        if (cacheEntry.isPresent() && request.equals(cacheEntry.get().originalRequest())) {
            return cacheEntry.get();
        } else {
            Preconditions.checkState(
                    ImmutableApacheClientRequest.copyOf(request).equals(request),
                    "A sane equals() method is required - this is a likely bug in Dialogue");
        }

        ClientConfiguration clientConf = AugmentClientConfig.getClientConf(request.serviceConf(), request);

        ApacheHttpClientChannels.ClientBuilder clientBuilder = ApacheHttpClientChannels.clientBuilder()
                .clientConfiguration(clientConf)
                .clientName(request.channelName())
                .dnsResolver(request.dnsResolver());
        request.blockingExecutor().ifPresent(clientBuilder::executor);
        ApacheHttpClientChannels.CloseableClient client = clientBuilder.build();

        ImmutableApacheCacheEntry newEntry = ImmutableApacheCacheEntry.builder()
                .originalRequest(request)
                .client(client)
                .conf(clientConf)
                .build();
        ApacheCacheEntry prev = apacheCache.put(request.channelName(), newEntry);

        try {
            if (prev != null) {
                prev.client().close(); // maybe this is unnecessary?
            }
        } catch (IOException e) {
            log.warn("Failed to close old apache client", e);
        }

        return newEntry;
    }

    private static ServiceConfiguration stripUris(ServiceConfiguration serviceConf) {
        return ServiceConfiguration.builder()
                .from(serviceConf)
                .uris(Collections.emptyList())
                .build();
    }

    @Safe
    @Override
    public String toString() {
        return "ChannelCache{"
                + "instanceNumber=" + instanceNumber
                + ", apacheCache.size=" + apacheCache.size()
                // Channel names are safe-loggable
                + ", apacheCache=" + apacheCache.keySet()
                + ", channelCache.size=" + channelCache.estimatedSize() + "/" + MAX_CACHED_CHANNELS
                + ", channelCache="
                // Channel names are safe-loggable
                + channelCache.asMap().keySet().stream()
                        .map(ChannelCacheKey::channelName)
                        .collect(Collectors.joining(", ", "[", "]"))
                + '}';
    }

    @DoNotLog
    @Value.Immutable
    interface ChannelCacheKey extends AugmentClientConfig {
        ServiceConfiguration serviceConf();

        Optional blockingExecutor();

        String channelName();

        Optional overrideHostIndex();

        DialogueDnsResolver dnsResolver();

        Duration dnsRefreshInterval();

        boolean dnsNodeDiscovery();
    }

    @Unsafe
    @Value.Immutable
    interface OverrideHostIndex {
        @Value.Parameter(order = 0)
        int index();

        @Value.Parameter(order = 1)
        TargetUri target();

        static OverrideHostIndex of(int index, TargetUri target) {
            return ImmutableOverrideHostIndex.of(index, target);
        }
    }

    @DoNotLog
    @Value.Immutable
    interface ApacheClientRequest extends AugmentClientConfig {
        ServiceConfiguration serviceConf();

        String channelName();

        Optional blockingExecutor();

        DialogueDnsResolver dnsResolver();

        @Value.Check
        default void check() {
            Preconditions.checkState(serviceConf().uris().isEmpty(), "Uris must be empty");
        }
    }

    @DoNotLog
    @Value.Immutable
    interface ApacheCacheEntry {
        ApacheClientRequest originalRequest();

        ApacheHttpClientChannels.CloseableClient client();

        ClientConfiguration conf();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy