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

com.yahoo.vespa.config.proxy.RpcConfigSourceClient Maven / Gradle / Ivy

// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.config.proxy;

import com.yahoo.concurrent.DaemonThreadFactory;
import com.yahoo.config.ConfigurationRuntimeException;
import com.yahoo.config.subscription.ConfigSourceSet;
import com.yahoo.config.subscription.impl.JRTConfigRequester;
import com.yahoo.jrt.Request;
import com.yahoo.jrt.Spec;
import com.yahoo.jrt.Supervisor;
import com.yahoo.jrt.Target;
import com.yahoo.jrt.Transport;

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import com.yahoo.vespa.config.ConfigCacheKey;
import com.yahoo.vespa.config.RawConfig;
import com.yahoo.vespa.config.TimingValues;
import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.logging.Logger;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * An Rpc client to a config source
 *
 * @author hmusum
 */
class RpcConfigSourceClient implements ConfigSourceClient, Runnable {

    private final static Logger log = Logger.getLogger(RpcConfigSourceClient.class.getName());
    private static final double timingValuesRatio = 0.8;

    private final Supervisor supervisor = new Supervisor(new Transport("config-source-client"));

    private final RpcServer rpcServer;
    private final ConfigSourceSet configSourceSet;
    private final Map activeSubscribers = new ConcurrentHashMap<>();
    private final MemoryCache memoryCache;
    private final DelayedResponses delayedResponses;
    private final static TimingValues timingValues;
    private final ScheduledExecutorService nextConfigScheduler =
            Executors.newScheduledThreadPool(1, new DaemonThreadFactory("next config"));
    private final ScheduledFuture nextConfigFuture;
    private final JRTConfigRequester requester;
    // Scheduled executor that periodically checks for requests that have timed out and response should be returned to clients
    private final ScheduledExecutorService delayedResponsesScheduler =
            Executors.newScheduledThreadPool(1, new DaemonThreadFactory("delayed responses"));
    private final ScheduledFuture delayedResponsesFuture;

    static {
        // Proxy should time out before clients upon subscription.
        TimingValues tv = new TimingValues();
        tv.setUnconfiguredDelay((long)(tv.getUnconfiguredDelay()* timingValuesRatio)).
                setConfiguredErrorDelay((long)(tv.getConfiguredErrorDelay()* timingValuesRatio)).
                setSubscribeTimeout((long)(tv.getSubscribeTimeout()* timingValuesRatio)).
                setConfiguredErrorTimeout(-1);  // Never cache errors
        timingValues = tv;
    }

    RpcConfigSourceClient(RpcServer rpcServer, ConfigSourceSet configSourceSet, MemoryCache memoryCache) {
        this.rpcServer = rpcServer;
        this.configSourceSet = configSourceSet;
        this.memoryCache = memoryCache;
        this.delayedResponses = new DelayedResponses();
        checkConfigSources();
        nextConfigFuture = nextConfigScheduler.scheduleAtFixedRate(this, 0, 10, MILLISECONDS);
        requester = JRTConfigRequester.create(configSourceSet, timingValues);
        DelayedResponseHandler command = new DelayedResponseHandler(delayedResponses, memoryCache, rpcServer);
        delayedResponsesFuture = delayedResponsesScheduler.scheduleAtFixedRate(command, 5, 1, SECONDS);
    }

    /**
     * Checks if config sources are available
     */
    private void checkConfigSources() {
        if (configSourceSet == null || configSourceSet.getSources() == null || configSourceSet.getSources().size() == 0) {
            log.log(Level.WARNING, "No config sources defined, could not check connection");
        } else {
            Request req = new Request("ping");
            for (String configSource : configSourceSet.getSources()) {
                Spec spec = new Spec(configSource);
                Target target = supervisor.connect(spec);
                target.invokeSync(req, 30.0);
                if (target.isValid()) {
                    log.log(Level.FINE, () -> "Created connection to config source at " + spec.toString());
                    return;
                } else {
                    log.log(Level.INFO, "Could not connect to config source at " + spec.toString());
                }
                target.close();
            }
            String extra = "";
            log.log(Level.INFO, "Could not connect to any config source in set " + configSourceSet.toString() +
                    ", please make sure config server(s) are running. " + extra);
        }
    }

    /**
     * Retrieves the requested config from the cache or the remote server.
     * 

* If the requested config is different from the one in cache, the cached request is returned immediately. * If they are equal, this method returns null. *

* If the config was not in cache, this method starts a Subscriber in a separate thread * that gets the config and calls updateSubscribers(). * * @param input The config to retrieve - can be empty (no payload), or have a valid payload. * @return A Config with a payload. */ @Override public RawConfig getConfig(RawConfig input, JRTServerConfigRequest request) { // Always add to delayed responses (we remove instead if we find config in cache) // This is to avoid a race where we might end up not adding to delayed responses // nor subscribing to config if another request for the same config // happens at the same time DelayedResponse delayedResponse = new DelayedResponse(request); delayedResponses.add(delayedResponse); final ConfigCacheKey configCacheKey = new ConfigCacheKey(input.getKey(), input.getDefMd5()); RawConfig cachedConfig = memoryCache.get(configCacheKey); boolean needToGetConfig = true; RawConfig ret = null; if (cachedConfig != null) { log.log(Level.FINE, () -> "Found config " + configCacheKey + " in cache, generation=" + cachedConfig.getGeneration() + ",configmd5=" + cachedConfig.getConfigMd5()); log.log(Level.FINEST, () -> "input config=" + input + ",cached config=" + cachedConfig); if (ProxyServer.configOrGenerationHasChanged(cachedConfig, request)) { log.log(Level.FINEST, () -> "Cached config is not equal to requested, will return it"); if (delayedResponses.remove(delayedResponse)) { // unless another thread already did it ret = cachedConfig; } } if (!cachedConfig.isError() && cachedConfig.getGeneration() > 0) { needToGetConfig = false; } } if (needToGetConfig) { subscribeToConfig(input, configCacheKey); } return ret; } private void subscribeToConfig(RawConfig input, ConfigCacheKey configCacheKey) { if (activeSubscribers.containsKey(configCacheKey)) return; log.log(Level.FINE, () -> "Could not find good config in cache, creating subscriber for: " + configCacheKey); var subscriber = new Subscriber(input, configSourceSet, timingValues, requester); try { subscriber.subscribe(); activeSubscribers.put(configCacheKey, subscriber); } catch (ConfigurationRuntimeException e) { log.log(Level.INFO, "Subscribe for '" + configCacheKey + "' failed, closing subscriber"); subscriber.cancel(); } } @Override public void run() { activeSubscribers.values().forEach(subscriber -> { if (!subscriber.isClosed()) { Optional config = subscriber.nextGeneration(); config.ifPresent(this::updateWithNewConfig); } }); } @Override public void cancel() { shutdownSourceConnections(); delayedResponsesFuture.cancel(true); delayedResponsesScheduler.shutdownNow(); nextConfigFuture.cancel(true); nextConfigScheduler.shutdownNow(); requester.close(); supervisor.transport().shutdown().join(); } /** * Takes down connection(s) to config sources and running tasks */ @Override public void shutdownSourceConnections() { activeSubscribers.values().forEach(Subscriber::cancel); activeSubscribers.clear(); nextConfigScheduler.shutdownNow(); requester.close(); } @Override public String getActiveSourceConnection() { return requester.getConnectionPool().getCurrent().getAddress(); } @Override public List getSourceConnections() { ArrayList ret = new ArrayList<>(); if (configSourceSet != null) { ret.addAll(configSourceSet.getSources()); } return ret; } /** * This method will be called when a response with changed config is received from upstream * (content or generation has changed) or the server timeout has elapsed. * * @param config new config */ public void updateSubscribers(RawConfig config) { log.log(Level.FINE, () -> "Config updated for " + config.getKey() + "," + config.getGeneration()); DelayQueue responseDelayQueue = delayedResponses.responses(); log.log(Level.FINEST, () -> "Delayed response queue: " + responseDelayQueue); if (responseDelayQueue.size() == 0) { log.log(Level.FINE, () -> "There exists no matching element on delayed response queue for " + config.getKey()); return; } else { log.log(Level.FINE, () -> "Delayed response queue has " + responseDelayQueue.size() + " elements"); } boolean found = false; for (DelayedResponse response : responseDelayQueue.toArray(new DelayedResponse[0])) { JRTServerConfigRequest request = response.getRequest(); if (request.getConfigKey().equals(config.getKey()) // Generation 0 is special, used when returning empty sentinel config && (config.getGeneration() >= request.getRequestGeneration() || config.getGeneration() == 0)) { if (delayedResponses.remove(response)) { found = true; log.log(Level.FINE, () -> "Call returnOkResponse for " + config.getKey() + "," + config.getGeneration()); rpcServer.returnOkResponse(request, config); } else { log.log(Level.INFO, "Could not remove " + config.getKey() + " from delayedResponses queue, already removed"); } } } if (!found) { log.log(Level.FINE, () -> "Found no recipient for " + config.getKey() + " in delayed response queue"); } log.log(Level.FINE, () -> "Finished updating config for " + config.getKey() + "," + config.getGeneration()); } @Override public DelayedResponses delayedResponses() { return delayedResponses; } private void updateWithNewConfig(RawConfig newConfig) { log.log(Level.FINE, () -> "config to be returned for '" + newConfig.getKey() + "', generation=" + newConfig.getGeneration() + ", payload=" + newConfig.getPayload()); memoryCache.update(newConfig); updateSubscribers(newConfig); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy