summaryrefslogtreecommitdiffstats
path: root/config-proxy
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /config-proxy
Publish
Diffstat (limited to 'config-proxy')
-rw-r--r--config-proxy/.gitignore2
-rw-r--r--config-proxy/OWNERS2
-rw-r--r--config-proxy/pom.xml114
-rw-r--r--config-proxy/src/main/java/com/yahoo/vespa/config/proxy/CacheManager.java46
-rw-r--r--config-proxy/src/main/java/com/yahoo/vespa/config/proxy/CheckDelayedResponses.java71
-rw-r--r--config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ClientUpdater.java107
-rw-r--r--config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ConfigProxyRpcServer.java340
-rw-r--r--config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ConfigProxyStatistics.java105
-rw-r--r--config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ConfigSourceClient.java45
-rw-r--r--config-proxy/src/main/java/com/yahoo/vespa/config/proxy/DelayedResponse.java65
-rw-r--r--config-proxy/src/main/java/com/yahoo/vespa/config/proxy/DelayedResponses.java39
-rw-r--r--config-proxy/src/main/java/com/yahoo/vespa/config/proxy/MapBackedConfigSource.java65
-rw-r--r--config-proxy/src/main/java/com/yahoo/vespa/config/proxy/MemoryCache.java136
-rw-r--r--config-proxy/src/main/java/com/yahoo/vespa/config/proxy/MemoryCacheConfigClient.java62
-rw-r--r--config-proxy/src/main/java/com/yahoo/vespa/config/proxy/Mode.java80
-rw-r--r--config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ProxyServer.java285
-rw-r--r--config-proxy/src/main/java/com/yahoo/vespa/config/proxy/RpcConfigSourceClient.java197
-rw-r--r--config-proxy/src/main/java/com/yahoo/vespa/config/proxy/RpcServer.java15
-rw-r--r--config-proxy/src/main/java/com/yahoo/vespa/config/proxy/Subscriber.java13
-rw-r--r--config-proxy/src/main/java/com/yahoo/vespa/config/proxy/UpstreamConfigSubscriber.java108
-rw-r--r--config-proxy/src/main/java/com/yahoo/vespa/config/proxy/package-info.java47
-rw-r--r--config-proxy/src/main/sh/cloudconfig_loadtester68
-rw-r--r--config-proxy/src/main/sh/cloudconfig_verification68
-rwxr-xr-xconfig-proxy/src/main/sh/config-ctl162
-rw-r--r--config-proxy/src/test/java/com/yahoo/vespa/config/proxy/CacheTest.java77
-rw-r--r--config-proxy/src/test/java/com/yahoo/vespa/config/proxy/CheckDelayedResponsesTest.java45
-rw-r--r--config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ClientUpdaterTest.java113
-rw-r--r--config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ConfigProxyRpcServerTest.java253
-rw-r--r--config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ConfigProxyStatisticsTest.java46
-rw-r--r--config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ConfigTester.java49
-rw-r--r--config-proxy/src/test/java/com/yahoo/vespa/config/proxy/DelayedResponseTest.java83
-rw-r--r--config-proxy/src/test/java/com/yahoo/vespa/config/proxy/DelayedResponsesTest.java32
-rw-r--r--config-proxy/src/test/java/com/yahoo/vespa/config/proxy/Helper.java58
-rw-r--r--config-proxy/src/test/java/com/yahoo/vespa/config/proxy/MemoryCacheConfigClientTest.java29
-rw-r--r--config-proxy/src/test/java/com/yahoo/vespa/config/proxy/MemoryCacheTest.java65
-rw-r--r--config-proxy/src/test/java/com/yahoo/vespa/config/proxy/MockConnection.java86
-rw-r--r--config-proxy/src/test/java/com/yahoo/vespa/config/proxy/MockRpcServer.java23
-rw-r--r--config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ModeTest.java61
-rw-r--r--config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ProxyServerTest.java184
-rw-r--r--config-proxy/src/test/java/com/yahoo/vespa/config/proxy/UpstreamConfigSubscriberTest.java173
40 files changed, 3619 insertions, 0 deletions
diff --git a/config-proxy/.gitignore b/config-proxy/.gitignore
new file mode 100644
index 00000000000..3cc25b51fc4
--- /dev/null
+++ b/config-proxy/.gitignore
@@ -0,0 +1,2 @@
+/pom.xml.build
+/target
diff --git a/config-proxy/OWNERS b/config-proxy/OWNERS
new file mode 100644
index 00000000000..7028eebe31a
--- /dev/null
+++ b/config-proxy/OWNERS
@@ -0,0 +1,2 @@
+musum
+arnej27959
diff --git a/config-proxy/pom.xml b/config-proxy/pom.xml
new file mode 100644
index 00000000000..eba51af8882
--- /dev/null
+++ b/config-proxy/pom.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0"?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>parent</artifactId>
+ <version>6-SNAPSHOT</version>
+ <relativePath>../parent/pom.xml</relativePath>
+ </parent>
+ <artifactId>config-proxy</artifactId>
+ <packaging>jar</packaging>
+ <version>6-SNAPSHOT</version>
+ <dependencies>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>yolean</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>config-lib</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>defaults</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>config</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>jrt</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>vespajlib</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>vespalog</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <compilerArgs>
+ <arg>-Xlint:all</arg>
+ <arg>-Werror</arg>
+ </compilerArgs>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <configuration>
+ <descriptorRefs>
+ <descriptorRef>jar-with-dependencies</descriptorRef>
+ </descriptorRefs>
+ </configuration>
+ <executions>
+ <execution>
+ <id>make-assembly</id>
+ <phase>package</phase>
+ <goals>
+ <goal>single</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <redirectTestOutputToFile>${test.hide}</redirectTestOutputToFile>
+ <forkMode>once</forkMode>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-install-plugin</artifactId>
+ <configuration>
+ <updateReleaseInfo>true</updateReleaseInfo>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/CacheManager.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/CacheManager.java
new file mode 100644
index 00000000000..993edd2962f
--- /dev/null
+++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/CacheManager.java
@@ -0,0 +1,46 @@
+// Copyright 2016 Yahoo Inc. 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.vespa.config.RawConfig;
+
+/**
+ * Manages memory and disk caches.
+ *
+ * @author musum
+ */
+public class CacheManager {
+
+ private final MemoryCache memoryCache;
+
+ public CacheManager(MemoryCache memoryCache) {
+ this.memoryCache = memoryCache;
+ }
+
+ static CacheManager createTestCacheManager() {
+ return CacheManager.createTestCacheManager(new MemoryCache());
+ }
+
+ static CacheManager createTestCacheManager(MemoryCache memoryCache) {
+ return new CacheManager(memoryCache);
+ }
+
+ void putInCache(RawConfig config) {
+ putInMemoryCache(config);
+ }
+
+ private void putInMemoryCache(RawConfig config) {
+ if (!config.isError()) {
+ memoryCache.put(config);
+ }
+ }
+
+ public MemoryCache getMemoryCache() {
+ return memoryCache;
+ }
+
+ // Only for testing.
+ int getCacheSize() {
+ return getMemoryCache().size();
+ }
+
+}
diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/CheckDelayedResponses.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/CheckDelayedResponses.java
new file mode 100644
index 00000000000..e8a2f5455a1
--- /dev/null
+++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/CheckDelayedResponses.java
@@ -0,0 +1,71 @@
+// Copyright 2016 Yahoo Inc. 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.log.LogLevel;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+import com.yahoo.yolean.Exceptions;
+import com.yahoo.vespa.config.ConfigCacheKey;
+import com.yahoo.vespa.config.RawConfig;
+
+import java.util.Date;
+import java.util.logging.Logger;
+
+/**
+ * The run method of this class is executed periodically to
+ * return requests that are about to time out.
+ *
+ * @author musum
+ */
+public class CheckDelayedResponses implements Runnable {
+
+ final static Logger log = Logger.getLogger(CheckDelayedResponses.class.getName());
+
+ private final DelayedResponses delayedResponses;
+ private final MemoryCache memoryCache;
+ private final RpcServer rpcServer;
+
+ public CheckDelayedResponses(DelayedResponses delayedResponses, MemoryCache memoryCache, RpcServer rpcServer) {
+ this.delayedResponses = delayedResponses;
+ this.memoryCache = memoryCache;
+ this.rpcServer = rpcServer;
+ }
+
+ @Override
+ public void run() {
+ checkDelayedResponses();
+ }
+
+ void checkDelayedResponses() {
+ try {
+ long start = System.currentTimeMillis();
+ if (log.isLoggable(LogLevel.SPAM)) {
+ log.log(LogLevel.SPAM, "Running CheckDelayedResponses. There are " + delayedResponses.size() + " delayed responses. First one is " + delayedResponses.responses().peek());
+ }
+ DelayedResponse response;
+ int i = 0;
+
+ while ((response = delayedResponses.responses().poll()) != null) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Returning with response that has return time " + new Date(response.getReturnTime()));
+ }
+ JRTServerConfigRequest request = response.getRequest();
+ ConfigCacheKey cacheKey = new ConfigCacheKey(request.getConfigKey(), request.getConfigKey().getMd5());
+ RawConfig config = memoryCache.get(cacheKey);
+ if (config != null) {
+ rpcServer.returnOkResponse(request, config);
+ i++;
+ } else {
+ log.log(LogLevel.WARNING, "No config found for " + request.getConfigKey() + " within timeout, will retry");
+ }
+ }
+ if (log.isLoggable(LogLevel.SPAM)) {
+ log.log(LogLevel.SPAM, "Finished running CheckDelayedResponses. " + i + " delayed responses sent in " +
+ (System.currentTimeMillis() - start) + " ms");
+ }
+ } catch (Exception e) { // To avoid thread throwing exception and executor never running this again
+ log.log(LogLevel.WARNING, "Got exception in CheckDelayedResponses: " + Exceptions.toMessageString(e));
+ } catch (Throwable e) {
+ com.yahoo.protect.Process.logAndDie("Got error in CheckDelayedResponses, exiting: " + Exceptions.toMessageString(e));
+ }
+ }
+}
diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ClientUpdater.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ClientUpdater.java
new file mode 100644
index 00000000000..133ecf4c94f
--- /dev/null
+++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ClientUpdater.java
@@ -0,0 +1,107 @@
+// Copyright 2016 Yahoo Inc. 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.log.LogLevel;
+import com.yahoo.vespa.config.RawConfig;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+
+import java.util.concurrent.DelayQueue;
+import java.util.logging.Logger;
+
+/**
+ * Updates clients subscribing to config when config changes or the
+ * timeout they have specified has elapsed.
+ *
+ * @author musum
+ */
+public class ClientUpdater {
+ final static Logger log = Logger.getLogger(ClientUpdater.class.getName());
+
+ private final CacheManager cacheManager;
+ private final ConfigProxyStatistics statistics;
+ private final RpcServer rpcServer;
+ private final DelayedResponses delayedResponses;
+ private final Mode mode;
+
+ public ClientUpdater(CacheManager cacheManager,
+ RpcServer rpcServer,
+ ConfigProxyStatistics statistics,
+ DelayedResponses delayedResponses,
+ Mode mode) {
+ this.cacheManager = cacheManager;
+ this.rpcServer = rpcServer;
+ this.statistics = statistics;
+ this.delayedResponses = delayedResponses;
+ this.mode = mode;
+ }
+
+ /**
+ * 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.
+ * Updates the cache with the returned config.
+ *
+ * @param config new config
+ */
+ void updateSubscribers(RawConfig config) {
+ // ignore updates if we are in one of these modes (we will then only serve from cache).
+ if (!mode.requiresConfigSource()) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Not updating " + config.getKey() + "," + config.getGeneration() +
+ ", since we are in '" + mode + "' mode");
+ }
+ return;
+ }
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Config updated for " + config.getKey() + "," + config.getGeneration());
+ }
+ cacheManager.putInCache(config);
+ sendResponse(config);
+ }
+
+ void sendResponse(RawConfig config) {
+ if (config.isError()) { statistics.incErrorCount(); }
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Sending response for " + config.getKey() + "," + config.getGeneration());
+ }
+ DelayQueue<DelayedResponse> responseDelayQueue = delayedResponses.responses();
+ if (log.isLoggable(LogLevel.SPAM)) {
+ log.log(LogLevel.SPAM, "Delayed response queue: " + responseDelayQueue);
+ }
+ if (responseDelayQueue.size() == 0) {
+ log.log(LogLevel.DEBUG, "There exists no matching element on delayed response queue for " + config.getKey());
+ return;
+ } else {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Delayed response queue has " + responseDelayQueue.size() + " elements");
+ }
+ }
+ DelayedResponse[] responses = new DelayedResponse[1];
+ responses = responseDelayQueue.toArray(responses);
+ boolean found = false;
+ if (responses.length > 0) {
+ for (DelayedResponse response : responses) {
+ JRTServerConfigRequest request = response.getRequest();
+ if (request.getConfigKey().equals(config.getKey())) {
+ if (!delayedResponses.remove(response)) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Could not remove " + config.getKey() + " from delayed delayedResponses queue, already removed");
+ }
+ continue;
+ }
+ found = true;
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Call returnOkResponse for " + config.getKey() + "," + config.getGeneration());
+ }
+ rpcServer.returnOkResponse(request, config);
+ }
+ }
+
+ }
+ if (!found) {
+ log.log(LogLevel.DEBUG, "Found no recipient for " + config.getKey() + " in delayed response queue");
+ }
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Finished updating config for " + config.getKey() + "," + config.getGeneration());
+ }
+ }
+}
diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ConfigProxyRpcServer.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ConfigProxyRpcServer.java
new file mode 100644
index 00000000000..b1cf0c01165
--- /dev/null
+++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ConfigProxyRpcServer.java
@@ -0,0 +1,340 @@
+// Copyright 2016 Yahoo Inc. 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.jrt.*;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.*;
+import com.yahoo.vespa.config.ErrorCode;
+import com.yahoo.vespa.config.protocol.JRTConfigRequestFactory;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequestV3;
+
+import java.lang.*;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.logging.Logger;
+
+
+/**
+ * A proxy server that handles RPC config requests.
+ *
+ * @author <a href="musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1
+ */
+public class ConfigProxyRpcServer implements Runnable, TargetWatcher, RpcServer {
+
+ private final static Logger log = Logger.getLogger(ConfigProxyRpcServer.class.getName());
+ static final int TRACELEVEL = 6;
+
+ private final Spec spec;
+ private final Supervisor supervisor = new Supervisor(new Transport());
+ private final ProxyServer proxyServer;
+
+ public ConfigProxyRpcServer(ProxyServer proxyServer, Spec spec) {
+ this.proxyServer = proxyServer;
+ this.spec = spec;
+ setUp();
+ }
+
+ public void run() {
+ try {
+ Acceptor acceptor = supervisor.listen(spec);
+ log.log(LogLevel.DEBUG, "Ready for requests on " + spec);
+ supervisor.transport().join();
+ acceptor.shutdown().join();
+ } catch (ListenFailedException e) {
+ proxyServer.stop();
+ throw new RuntimeException("Could not listen on " + spec, e);
+ }
+ }
+
+ void shutdown() {
+ supervisor.transport().shutdown();
+ }
+
+ public Spec getSpec() {
+ return spec;
+ }
+
+ void setUp() {
+ // The getConfig method in this class will handle RPC calls for getting config
+ supervisor.addMethod(JRTMethods.createConfigV3GetConfigMethod(this, "getConfigV3"));
+ supervisor.addMethod(new Method("ping", "", "i",
+ this, "ping")
+ .methodDesc("ping")
+ .returnDesc(0, "ret code", "return code, 0 is OK"));
+ supervisor.addMethod(new Method("printStatistics", "", "s",
+ this, "printStatistics")
+ .methodDesc("printStatistics")
+ .returnDesc(0, "statistics", "Statistics for server"));
+ supervisor.addMethod(new Method("listCachedConfig", "", "S",
+ this, "listCachedConfig")
+ .methodDesc("list cached configs)")
+ .returnDesc(0, "data", "string array of configs"));
+ supervisor.addMethod(new Method("listCachedConfigFull", "", "S",
+ this, "listCachedConfigFull")
+ .methodDesc("list cached configs with cache content)")
+ .returnDesc(0, "data", "string array of configs"));
+ supervisor.addMethod(new Method("listSourceConnections", "", "S",
+ this, "listSourceConnections")
+ .methodDesc("list config source connections)")
+ .returnDesc(0, "data", "string array of source connections"));
+ supervisor.addMethod(new Method("invalidateCache", "", "S",
+ this, "invalidateCache")
+ .methodDesc("list config source connections)")
+ .returnDesc(0, "data", "0 if success, 1 otherwise"));
+ supervisor.addMethod(new Method("updateSources", "s", "s",
+ this, "updateSources")
+ .methodDesc("update list of config sources")
+ .returnDesc(0, "ret", "list of updated config sources"));
+ supervisor.addMethod(new Method("setMode", "s", "S",
+ this, "setMode")
+ .methodDesc("Set config proxy mode { default | memorycache }")
+ .returnDesc(0, "ret", "0 if success, 1 otherwise as first element, description as second element"));
+ supervisor.addMethod(new Method("getMode", "", "s",
+ this, "getMode")
+ .methodDesc("What serving mode the config proxy is in (default, memorycache)")
+ .returnDesc(0, "ret", "mode as a string"));
+ supervisor.addMethod(new Method("dumpCache", "s", "s",
+ this, "dumpCache")
+ .methodDesc("Dump cache to disk")
+ .paramDesc(0, "path", "path to write cache contents to")
+ .returnDesc(0, "ret", "Empty string or error message"));
+ }
+
+ /**
+ * Handles RPC method "config.v3.getConfig" requests.
+ *
+ * @param req a Request
+ */
+ @SuppressWarnings({"UnusedDeclaration"})
+ public final void getConfigV3(Request req) {
+ log.log(LogLevel.DEBUG, "getConfigV2");
+ JRTServerConfigRequest request = JRTServerConfigRequestV3.createFromRequest(req);
+ if (isProtocolVersionSupported(request)) {
+ preHandle(req);
+ getConfigImpl(request);
+ }
+ }
+
+ private boolean isProtocolVersionSupported(JRTServerConfigRequest request) {
+ Set<Long> supportedProtocolVersions = JRTConfigRequestFactory.supportedProtocolVersions();
+ if (supportedProtocolVersions.contains(request.getProtocolVersion())) {
+ return true;
+ } else {
+ String message = "Illegal protocol version " + request.getProtocolVersion() +
+ " in request " + request.getShortDescription() + ", only protocol versions " + supportedProtocolVersions + " are supported";
+ log.log(LogLevel.ERROR, message);
+ request.addErrorResponse(ErrorCode.ILLEGAL_PROTOCOL_VERSION, message);
+ }
+ return false;
+ }
+
+ private void preHandle(Request req) {
+ proxyServer.getStatistics().incRpcRequests();
+ req.detach();
+ req.target().addWatcher(this);
+ }
+
+ /**
+ * Handles all versions of "getConfig" requests.
+ *
+ * @param request a Request
+ */
+ private void getConfigImpl(JRTServerConfigRequest request) {
+ request.getRequestTrace().trace(TRACELEVEL, "Config proxy getConfig()");
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "getConfig: " + request.getShortDescription() + ",configmd5=" + request.getRequestConfigMd5());
+ }
+ if (!request.validateParameters()) {
+ // Error code is set in verifyParameters if parameters are not OK.
+ log.log(LogLevel.WARNING, "Parameters for request " + request + " did not validate: " + request.errorCode() + " : " + request.errorMessage());
+ returnErrorResponse(request, request.errorCode(), "Parameters for request " + request.getShortDescription() + " did not validate: " + request.errorMessage());
+ return;
+ }
+ try {
+ RawConfig config = proxyServer.resolveConfig(request);
+ if (ProxyServer.configOrGenerationHasChanged(config, request)) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Should send response for " + request.getShortDescription() + ",config=" + config);
+ }
+ returnOkResponse(request, config);
+ } else {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Should not send response for " + request.getShortDescription() + ", config=" + config);
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ returnErrorResponse(request, com.yahoo.vespa.config.ErrorCode.INTERNAL_ERROR, e.getMessage());
+ }
+ }
+
+ /**
+ * Returns 0 if server is alive.
+ *
+ * @param req a Request
+ */
+ public final void ping(Request req) {
+ req.returnValues().add(new Int32Value(0));
+ }
+
+ /**
+ * Returns a String with statistics data for the server.
+ *
+ * @param req a Request
+ */
+ public final void printStatistics(Request req) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("\nDelayed responses queue size: ");
+ sb.append(proxyServer.delayedResponses.size());
+ sb.append("\nContents: ");
+ for (DelayedResponse delayed : proxyServer.delayedResponses.responses()) {
+ sb.append(delayed.getRequest().toString()).append("\n");
+ }
+
+ req.returnValues().add(new StringValue(sb.toString()));
+ }
+
+ public final void listCachedConfig(Request req) {
+ listCachedConfig(req, false);
+ }
+
+ public final void listCachedConfigFull(Request req) {
+ listCachedConfig(req, true);
+ }
+
+ public final void listSourceConnections(Request req) {
+ String[] ret = new String[2];
+ ret[0] = "Current source: " + proxyServer.getActiveSourceConnection();
+ ret[1] = "All sources:\n" + printSourceConnections();
+ req.returnValues().add(new StringArray(ret));
+ }
+
+ private String printSourceConnections() {
+ StringBuilder sb = new StringBuilder();
+ for (String s : proxyServer.getSourceConnections()) {
+ sb.append(s).append("\n");
+ }
+ return sb.toString();
+ }
+
+ @SuppressWarnings({"UnusedDeclaration"})
+ public final void updateSources(Request req) {
+ String sources = req.parameters().get(0).asString();
+ String ret;
+ System.out.println(proxyServer.getMode());
+ if (proxyServer.getMode().requiresConfigSource()) {
+ proxyServer.updateSourceConnections(Arrays.asList(sources.split(",")));
+ ret = "Updated config sources to: " + sources;
+ } else {
+ ret = "Cannot update sources when in '" + proxyServer.getMode().name() + "' mode";
+ }
+ req.returnValues().add(new StringValue(ret));
+ }
+
+ public final void invalidateCache(Request req) {
+ proxyServer.getCacheManager().getMemoryCache().clear();
+ String[] s = new String[2];
+ s[0] = "0";
+ s[1] = "success";
+ req.returnValues().add(new StringArray(s));
+ }
+
+ public final void setMode(Request req) {
+ String suppliedMode = req.parameters().get(0).asString();
+ log.log(LogLevel.DEBUG, "Supplied mode=" + suppliedMode);
+ String[] s = new String[2];
+ if (Mode.validModeName(suppliedMode.toLowerCase())) {
+ proxyServer.setMode(suppliedMode);
+ s[0] = "0";
+ s[1] = "success";
+ } else {
+ s[0] = "1";
+ s[1] = "Could not set mode to '" + suppliedMode + "'. Legal modes are '" + Mode.modes() + "'";
+ }
+
+ req.returnValues().add(new StringArray(s));
+ }
+
+ public final void getMode(Request req) {
+ req.returnValues().add(new StringValue(proxyServer.getMode().name()));
+ }
+
+ @SuppressWarnings({"UnusedDeclaration"})
+ public final void dumpCache(Request req) {
+ final CacheManager cacheManager = proxyServer.getCacheManager();
+ req.returnValues().add(new StringValue(cacheManager.getMemoryCache().dumpCacheToDisk(req.parameters().get(0).asString(), cacheManager.getMemoryCache())));
+ }
+
+ final void listCachedConfig(Request req, boolean full) {
+ String[] ret;
+ MemoryCache cache = proxyServer.getCacheManager().getMemoryCache();
+ ret = new String[cache.size()];
+ int i = 0;
+ for (RawConfig config : cache.values()) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(config.getNamespace());
+ sb.append(".");
+ sb.append(config.getName());
+ sb.append(",");
+ sb.append(config.getConfigId());
+ sb.append(",");
+ sb.append(config.getGeneration());
+ sb.append(",");
+ sb.append(config.getConfigMd5());
+ if (full) {
+ sb.append(",");
+ sb.append(config.getPayload());
+ }
+ ret[i] = sb.toString();
+ i++;
+ }
+ Arrays.sort(ret);
+ req.returnValues().add(new StringArray(ret));
+ }
+
+ /**
+ * Removes invalid targets (closed client connections) from delayedResponsesQueue.
+ *
+ * @param target a Target that has become invalid (i.e, client has closed connection)
+ */
+ @Override
+ public void notifyTargetInvalid(Target target) {
+ log.log(LogLevel.DEBUG, "Target invalid " + target);
+ for (Iterator<DelayedResponse> it = proxyServer.delayedResponses.responses().iterator(); it.hasNext(); ) {
+ DelayedResponse delayed = it.next();
+ JRTServerConfigRequest request = delayed.getRequest();
+ if (request.getRequest().target().equals(target)) {
+ log.log(LogLevel.DEBUG, "Removing " + request.getShortDescription());
+ it.remove();
+ }
+ }
+ // TODO: Could we also cancel active getConfig requests upstream if the client was the only one
+ // requesting this config?
+ }
+
+ public void returnOkResponse(JRTServerConfigRequest request, RawConfig config) {
+ request.getRequestTrace().trace(TRACELEVEL, "Config proxy returnOkResponse()");
+ request.addOkResponse(config.getPayload(), config.getGeneration(), config.getConfigMd5());
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Return response: " + request.getShortDescription() + ",configMd5=" + config.getConfigMd5() +
+ ",config=" + config.getPayload());
+ }
+
+ // TODO Catch exception for now, since the request might have been returned in CheckDelayedResponse
+ // TODO Move logic so that all requests are returned in CheckDelayedResponse
+ try {
+ request.getRequest().returnRequest();
+ } catch (IllegalStateException e) {
+ log.log(LogLevel.DEBUG, "Something bad happened when sending response for '" + request.getShortDescription() + "':" + e.getMessage());
+ }
+ }
+
+ public void returnErrorResponse(JRTServerConfigRequest request, int errorCode, String message) {
+ request.getRequestTrace().trace(TRACELEVEL, "Config proxy returnErrorResponse()");
+ request.addErrorResponse(errorCode, message);
+ request.getRequest().returnRequest();
+ }
+}
diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ConfigProxyStatistics.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ConfigProxyStatistics.java
new file mode 100644
index 00000000000..8e0543d7914
--- /dev/null
+++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ConfigProxyStatistics.java
@@ -0,0 +1,105 @@
+// Copyright 2016 Yahoo Inc. 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.log.LogLevel;
+import com.yahoo.log.event.Event;
+
+/**
+ * Statistics/metrics for config proxy.
+ * //TODO Use metrics framework
+ *
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1.7
+ */
+class ConfigProxyStatistics implements Runnable {
+ static final long defaultEventInterval = 5 * 60; // in seconds
+
+ private final long eventInterval; // in seconds
+ private boolean stopped;
+ private long lastRun = System.currentTimeMillis();
+
+ /* Number of RPC getConfig requests */
+ private long rpcRequests = 0;
+ private long processedRequests = 0;
+ private long errors = 0;
+ private long delayedResponses = 0;
+
+ ConfigProxyStatistics() {
+ this(defaultEventInterval);
+ }
+
+ ConfigProxyStatistics(long eventInterval) {
+ this.eventInterval = eventInterval;
+ }
+
+ // Send events every eventInterval seconds
+ public void run() {
+ while (true) {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ ProxyServer.log.log(LogLevel.WARNING, e.getMessage());
+ }
+ if (stopped) {
+ return;
+ }
+ ProxyServer.log.log(LogLevel.SPAM, "Running ConfigProxyStatistics");
+ // Only send events every eventInterval seconds
+ if ((System.currentTimeMillis() - lastRun) > eventInterval * 1000) {
+ lastRun = System.currentTimeMillis();
+ sendEvents();
+ }
+ }
+ }
+
+ private void sendEvents() {
+ Event.count("rpc_requests", rpcRequests());
+ Event.count("processed_messages", processedRequests());
+ Event.count("errors", errors());
+ Event.value("delayed_responses", delayedResponses());
+ }
+
+ public void stop() {
+ stopped = true;
+ }
+
+ public Long getEventInterval() {
+ return eventInterval;
+ }
+
+ void incRpcRequests() {
+ rpcRequests++;
+ }
+
+ void incProcessedRequests() {
+ processedRequests++;
+ }
+
+ void incErrorCount() {
+ errors++;
+ }
+
+ long processedRequests() {
+ return processedRequests;
+ }
+
+ long rpcRequests() {
+ return rpcRequests;
+ }
+
+ long errors() {
+ return errors;
+ }
+
+ long delayedResponses() {
+ return delayedResponses;
+ }
+
+ void delayedResponses(long count) {
+ delayedResponses = count;
+ }
+
+ void decDelayedResponses() {
+ delayedResponses--;
+ }
+}
diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ConfigSourceClient.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ConfigSourceClient.java
new file mode 100644
index 00000000000..8ef63cff5c3
--- /dev/null
+++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ConfigSourceClient.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. 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.config.subscription.ConfigSource;
+import com.yahoo.config.subscription.ConfigSourceSet;
+import com.yahoo.vespa.config.RawConfig;
+import com.yahoo.vespa.config.TimingValues;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+
+import java.util.List;
+
+/**
+ * A client to a config source, which could be an RPC config server or some other backing for
+ * getting config.
+ *
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1.9
+ */
+public abstract class ConfigSourceClient {
+ static ConfigSourceClient createClient(ConfigSource source,
+ ClientUpdater clientUpdater,
+ CacheManager cacheManager,
+ TimingValues timingValues,
+ ConfigProxyStatistics statistics,
+ DelayedResponses delayedResponses) {
+ if (source instanceof ConfigSourceSet) {
+ return new RpcConfigSourceClient((ConfigSourceSet) source, clientUpdater, cacheManager, timingValues, statistics, delayedResponses);
+ } else if (source instanceof MapBackedConfigSource) {
+ return (ConfigSourceClient) source;
+ } else {
+ throw new IllegalArgumentException("config source of type " + source.getClass().getName() + " is not allowed");
+ }
+ }
+
+ abstract RawConfig getConfig(RawConfig input, JRTServerConfigRequest request);
+
+ abstract void cancel();
+
+ // TODO Should only be in rpc config source client
+ abstract void shutdownSourceConnections();
+
+ abstract String getActiveSourceConnection();
+
+ abstract List<String> getSourceConnections();
+} \ No newline at end of file
diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/DelayedResponse.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/DelayedResponse.java
new file mode 100644
index 00000000000..257b94038b6
--- /dev/null
+++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/DelayedResponse.java
@@ -0,0 +1,65 @@
+// Copyright 2016 Yahoo Inc. 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.vespa.config.protocol.JRTServerConfigRequest;
+
+import java.util.concurrent.Delayed;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Class that handles responses that are put on delayedResponses queue. Implements the <code>Delayed</code>
+ * interface to keep track of the order of responses (used when checking
+ * delayed responses and returning requests that are about to time out, in cases where no new
+ * config has been returned from upstream)
+ *
+ * @see com.yahoo.vespa.config.proxy.CheckDelayedResponses
+ */
+public class DelayedResponse implements Delayed {
+ private final JRTServerConfigRequest request;
+ private final long returnTime;
+
+ public DelayedResponse(JRTServerConfigRequest request) {
+ this(request, System.currentTimeMillis() + request.getTimeout());
+ }
+
+ DelayedResponse(JRTServerConfigRequest request, long returnTime) {
+ this.request = request;
+ this.returnTime = returnTime;
+ }
+
+ public Long getReturnTime() {
+ return returnTime;
+ }
+
+ public JRTServerConfigRequest getRequest() {
+ return request;
+ }
+
+ @Override
+ public long getDelay(TimeUnit unit) {
+ return returnTime - System.currentTimeMillis();
+ }
+
+ @Override
+ public int compareTo(Delayed delayed) {
+ if (this == delayed) {
+ return 0;
+ }
+ if (delayed instanceof com.yahoo.vespa.config.proxy.DelayedResponse) {
+ if (this.returnTime < ((com.yahoo.vespa.config.proxy.DelayedResponse) delayed).getReturnTime()) {
+ return -1;
+ } else {
+ return 1;
+ }
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(request.getShortDescription()).append(", delayLeft=").append(getDelay(TimeUnit.MILLISECONDS)).append(" ms");
+ return sb.toString();
+ }
+}
diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/DelayedResponses.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/DelayedResponses.java
new file mode 100644
index 00000000000..f3d303c840c
--- /dev/null
+++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/DelayedResponses.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.proxy;
+
+import java.util.concurrent.DelayQueue;
+
+/**
+ * Queue for requests that have no corresponding config in cache and which we are awaiting response from server for
+ *
+ * @author <a href="musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1.7
+ */
+class DelayedResponses {
+
+ private final DelayQueue<DelayedResponse> delayedResponses = new DelayQueue<>();
+ private final ConfigProxyStatistics statistics;
+
+ DelayedResponses(ConfigProxyStatistics statistics) {
+ this.statistics = statistics;
+ }
+
+ void add(DelayedResponse response) {
+ delayedResponses.add(response);
+ statistics.delayedResponses(delayedResponses.size());
+ }
+
+ boolean remove(DelayedResponse response) {
+ statistics.decDelayedResponses();
+ return delayedResponses.remove(response);
+ }
+
+ DelayQueue<DelayedResponse> responses() {
+ return delayedResponses;
+ }
+
+ int size() {
+ return responses().size();
+ }
+
+}
diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/MapBackedConfigSource.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/MapBackedConfigSource.java
new file mode 100644
index 00000000000..aa214ec0410
--- /dev/null
+++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/MapBackedConfigSource.java
@@ -0,0 +1,65 @@
+// Copyright 2016 Yahoo Inc. 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.config.subscription.ConfigSource;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.RawConfig;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * A simple class to be able to test config proxy without having an RPC config
+ * source.
+ *
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1.10
+ */
+public class MapBackedConfigSource extends ConfigSourceClient implements ConfigSource {
+ private final HashMap<ConfigKey<?>, RawConfig> backing = new HashMap<>();
+ private final ClientUpdater clientUpdater;
+
+ MapBackedConfigSource(ClientUpdater clientUpdater) {
+ this.clientUpdater = clientUpdater;
+ }
+
+ MapBackedConfigSource put(ConfigKey<?> key, RawConfig config) {
+ backing.put(key, config);
+ clientUpdater.updateSubscribers(config);
+ return this;
+ }
+
+ @Override
+ RawConfig getConfig(RawConfig input, JRTServerConfigRequest request) {
+ return getConfig(input.getKey());
+ }
+
+ RawConfig getConfig(ConfigKey<?> configKey) {
+ return backing.get(configKey);
+ }
+
+ @Override
+ void cancel() {
+ clear();
+ }
+
+ @Override
+ void shutdownSourceConnections() {
+ }
+
+ public void clear() {
+ backing.clear();
+ }
+
+ @Override
+ String getActiveSourceConnection() {
+ return "N/A";
+ }
+
+ @Override
+ List<String> getSourceConnections() {
+ return Collections.singletonList("N/A");
+ }
+}
diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/MemoryCache.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/MemoryCache.java
new file mode 100644
index 00000000000..d5531aa0ee4
--- /dev/null
+++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/MemoryCache.java
@@ -0,0 +1,136 @@
+// Copyright 2016 Yahoo Inc. 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.io.IOUtils;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.ConfigCacheKey;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.RawConfig;
+import com.yahoo.vespa.config.protocol.CompressionType;
+import com.yahoo.vespa.config.protocol.Payload;
+import com.yahoo.vespa.defaults.Defaults;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Logger;
+
+/**
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1.9
+ */
+public class MemoryCache {
+ private static final Logger log = Logger.getLogger(MemoryCache.class.getName());
+
+ // Separator in file names between different fields of config key
+ private final static String separator = ":";
+ private static final String DEFAULT_DUMP_DIR = Defaults.getDefaults().vespaHome() + "var/vespa/cache/config";
+ private final String dumpDir;
+
+ private final ConcurrentHashMap<ConfigCacheKey, RawConfig> cache = new ConcurrentHashMap<>(500, 0.75f);
+
+ public MemoryCache() {
+ dumpDir = DEFAULT_DUMP_DIR;
+ }
+
+ public RawConfig get(ConfigCacheKey key) {
+ return cache.get(key);
+ }
+
+ public RawConfig put(RawConfig config) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Putting '" + config + "' into memory cache");
+ }
+ return cache.put(new ConfigCacheKey(config.getKey(), config.getDefMd5()), config);
+ }
+
+ boolean containsKey(ConfigCacheKey key) {
+ return cache.containsKey(key);
+ }
+
+ public Collection<RawConfig> values() {
+ return cache.values();
+ }
+
+ public int size() {
+ return cache.size();
+ }
+
+ public void clear() {
+ cache.clear();
+ }
+
+ @Override
+ public String toString() {
+ return cache.toString();
+ }
+
+ String dumpCacheToDisk(String path, MemoryCache cache) {
+ if (path == null || path.isEmpty()) {
+ path = dumpDir;
+ log.log(LogLevel.DEBUG, "dumpCache. No path or empty path. Using dir '" + path + "'");
+ }
+ if (path.endsWith("/")) {
+ path = path.substring(0, path.length() - 1);
+ }
+ log.log(LogLevel.DEBUG, "Dumping cache to path '" + path + "'");
+ IOUtils.createDirectory(path);
+ File dir = new File(path);
+
+ if (!dir.isDirectory() || !dir.canWrite()) {
+ return "Not a dir or not able to write to '" + dir + "'";
+ }
+ for (RawConfig config : cache.values()) {
+ put(config, path);
+ }
+ return "success";
+ }
+
+ void put(RawConfig config, String path) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Putting '" + config.getKey() + "' into disk cache");
+ }
+ String filename = null;
+ Writer writer = null;
+ try {
+ filename = path + File.separator + createCacheFileName(config);
+ final Payload payload = config.getPayload();
+ long protocolVersion = 3;
+ log.log(LogLevel.DEBUG, "Writing config '" + config + "' to file '" + filename + "' with protocol version " + protocolVersion);
+ writer = IOUtils.createWriter(filename, "UTF-8", false);
+
+ // First three lines are meta-data about config as comment lines, fourth line is empty
+ writer.write("# defMd5:" + config.getDefMd5() + "\n");
+ writer.write("# configMd5:" + config.getConfigMd5() + "\n");
+ writer.write("# generation:" + Long.toString(config.getGeneration()) + "\n");
+ writer.write("# protocolVersion:" + Long.toString(protocolVersion) + "\n");
+ writer.write("\n");
+ writer.write(payload.withCompression(CompressionType.UNCOMPRESSED).toString());
+ writer.write("\n");
+ writer.close();
+ } catch (IOException e) {
+ log.log(LogLevel.WARNING, "Could not write to file '" + filename + "'");
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ private static String createCacheFileName(RawConfig config) {
+ return createCacheFileName(new ConfigCacheKey(config.getKey(), config.getDefMd5()));
+ }
+
+ static String createCacheFileName(ConfigCacheKey key) {
+ final ConfigKey<?> configKey = key.getKey();
+ return configKey.getNamespace() + "." + configKey.getName() + separator + configKey.getConfigId().replaceAll("/", "_") +
+ separator + key.getDefMd5();
+ }
+
+}
diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/MemoryCacheConfigClient.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/MemoryCacheConfigClient.java
new file mode 100644
index 00000000000..af3182dd919
--- /dev/null
+++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/MemoryCacheConfigClient.java
@@ -0,0 +1,62 @@
+// Copyright 2016 Yahoo Inc. 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.log.LogLevel;
+import com.yahoo.vespa.config.*;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1.10
+ */
+public class MemoryCacheConfigClient extends ConfigSourceClient {
+
+ private final static Logger log = Logger.getLogger(MemoryCacheConfigClient.class.getName());
+ private final MemoryCache cache;
+
+ MemoryCacheConfigClient(MemoryCache cache) {
+ this.cache = cache;
+ }
+
+ @Override
+ /**
+ * Retrieves the requested config from the cache. Used when in 'memorycache' mode</p>
+ *
+ * @param request The config to retrieve - can be empty (no payload), or have a valid payload.
+ * @return A Config with a payload.
+ */
+ RawConfig getConfig(RawConfig input, JRTServerConfigRequest request) {
+ log.log(LogLevel.DEBUG, "Getting config from cache");
+ ConfigKey<?> key = input.getKey();
+ RawConfig cached = cache.get(new ConfigCacheKey(key, input.getDefMd5()));
+ if (cached != null) {
+ log.log(LogLevel.DEBUG, "Found config " + key + " in cache");
+ return cached;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ void cancel() {
+ }
+
+ @Override
+ void shutdownSourceConnections() {
+ }
+
+ @Override
+ String getActiveSourceConnection() {
+ return "N/A";
+ }
+
+ @Override
+ List<String> getSourceConnections() {
+ return Collections.singletonList("N/A");
+ }
+
+}
diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/Mode.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/Mode.java
new file mode 100644
index 00000000000..ccb92c491f0
--- /dev/null
+++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/Mode.java
@@ -0,0 +1,80 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.proxy;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ *
+ * Different modes the config proxy can be running in.
+ *
+ * 'default' mode is requesting config from server, serving from cache only when known config
+ * and no new config having been sent from server. When in 'memorycache' mode, there is no connection
+ * to another config source, the proxy only serves from (memory) cache.
+ *
+ * @author musum
+ */
+class Mode {
+ private final ModeName mode;
+
+ enum ModeName {
+ DEFAULT, MEMORYCACHE
+ }
+
+ public Mode() {
+ this(ModeName.DEFAULT.name());
+ }
+
+ public Mode(String modeString) {
+ switch (modeString.toLowerCase()) {
+ case "" :
+ mode = ModeName.DEFAULT;
+ break;
+ case "default" :
+ mode = ModeName.DEFAULT;
+ break;
+ case "memorycache" :
+ mode = ModeName.MEMORYCACHE;
+ break;
+ default:
+ throw new IllegalArgumentException("Unrecognized mode'" + modeString + "' supplied");
+ }
+ }
+
+ public ModeName getMode() {
+ return mode;
+ }
+
+ public boolean isDefault() {
+ return mode.equals(ModeName.DEFAULT);
+ }
+
+ public boolean isMemoryCache() {
+ return mode.equals(ModeName.MEMORYCACHE);
+ }
+
+ public boolean requiresConfigSource() {
+ return mode.equals(ModeName.DEFAULT);
+ }
+
+ public static boolean validModeName(String modeString) {
+ return (modeString != null) && modes().contains(modeString);
+ }
+
+ static Set<String> modes() {
+ Set<String> modes = new HashSet<>();
+ for (ModeName mode : ModeName.values()) {
+ modes.add(mode.name().toLowerCase());
+ }
+ return modes;
+ }
+
+ public String name() {
+ return mode.name().toLowerCase();
+ }
+
+ @Override
+ public String toString() {
+ return mode.name().toLowerCase();
+ }
+}
diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ProxyServer.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ProxyServer.java
new file mode 100644
index 00000000000..ab053f31ac9
--- /dev/null
+++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/ProxyServer.java
@@ -0,0 +1,285 @@
+// Copyright 2016 Yahoo Inc. 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.subscription.ConfigSource;
+import com.yahoo.config.subscription.ConfigSourceSet;
+import com.yahoo.jrt.Spec;
+
+import com.yahoo.log.LogLevel;
+import com.yahoo.log.LogSetup;
+import com.yahoo.log.event.Event;
+import com.yahoo.system.CatchSigTerm;
+import com.yahoo.vespa.config.*;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Logger;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+/**
+ * A proxy server that handles RPC config requests. The proxy can run in three modes:
+ * 'default' and 'memorycache', where the last one will not get config from an upstream
+ * config source, but will serve config only from memory cache.
+ *
+ * @author <a href="musum@yahoo-inc.com">Harald Musum</a>
+ */
+public class ProxyServer implements Runnable {
+
+ private static final int DEFAULT_RPC_PORT = 19090;
+ static final String DEFAULT_PROXY_CONFIG_SOURCES = "tcp/localhost:19070";
+
+ final static Logger log = Logger.getLogger(ProxyServer.class.getName());
+ private final AtomicBoolean signalCaught = new AtomicBoolean(false);
+
+ // Scheduled executor that periodically checks for requests that have timed out and response should be returned to clients
+ private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, new DaemonThreadFactory());
+ private final ClientUpdater clientUpdater;
+ private ScheduledFuture<?> checkerHandle;
+
+ private final ConfigProxyRpcServer rpcServer;
+ final DelayedResponses delayedResponses;
+ private ConfigSource configSource;
+
+ private volatile ConfigSourceClient configSourceClient;
+
+ private final ConfigProxyStatistics statistics;
+ private final TimingValues timingValues;
+ private final CacheManager cacheManager;
+ private static final double timingvaluesRatio = 0.8;
+ private final static TimingValues defaultTimingValues;
+ private final boolean delayedResponseHandling;
+
+ private volatile Mode mode;
+
+
+ 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
+ defaultTimingValues = tv;
+ }
+
+ private ProxyServer(Spec spec, DelayedResponses delayedResponses, ConfigSource source,
+ ConfigProxyStatistics statistics, TimingValues timingValues, Mode mode, boolean delayedResponseHandling,
+ CacheManager cacheManager) {
+ this.delayedResponses = delayedResponses;
+ this.configSource = source;
+ log.log(LogLevel.DEBUG, "Using config sources: " + source);
+ this.statistics = statistics;
+ this.timingValues = timingValues;
+ this.mode = mode;
+ this.delayedResponseHandling = delayedResponseHandling;
+ this.cacheManager = cacheManager;
+ if (spec == null) {
+ rpcServer = null;
+ } else {
+ rpcServer = new ConfigProxyRpcServer(this, spec);
+ }
+ clientUpdater = new ClientUpdater(cacheManager, rpcServer, statistics, delayedResponses, mode);
+ this.configSourceClient = ConfigSourceClient.createClient(source, clientUpdater, cacheManager, timingValues, statistics, delayedResponses);
+ }
+
+ static ProxyServer create(int port, DelayedResponses delayedResponses, ConfigSource source,
+ ConfigProxyStatistics statistics, Mode mode) {
+ return new ProxyServer(new Spec(null, port), delayedResponses, source, statistics, defaultTimingValues(), mode, true, new CacheManager(new MemoryCache()));
+ }
+
+ static ProxyServer createTestServer(ConfigSource source) {
+ final Mode mode = new Mode(Mode.ModeName.DEFAULT.name());
+ return ProxyServer.createTestServer(source, false, mode, CacheManager.createTestCacheManager());
+ }
+
+ static ProxyServer createTestServer(ConfigSource source, boolean delayedResponseHandling, Mode mode, CacheManager cacheManager) {
+ final ConfigProxyStatistics statistics = new ConfigProxyStatistics();
+ return new ProxyServer(null, new DelayedResponses(statistics), source, statistics, defaultTimingValues(), mode, delayedResponseHandling, cacheManager);
+ }
+
+ public void run() {
+ if (rpcServer != null) {
+ Thread t = new Thread(rpcServer);
+ t.setName("RpcServer");
+ t.start();
+ }
+ if (delayedResponseHandling) {
+ // Wait for 5 seconds initially, then run every second
+ checkerHandle = scheduler.scheduleAtFixedRate(new CheckDelayedResponses(delayedResponses, cacheManager.getMemoryCache(), rpcServer), 5, 1, SECONDS);
+ } else {
+ log.log(LogLevel.INFO, "Running without delayed response handling");
+ }
+ }
+
+ public RawConfig resolveConfig(JRTServerConfigRequest req) {
+ statistics.incProcessedRequests();
+ // Calling getConfig() will either return with an answer immediately or
+ // create a background thread that retrieves config from the server and
+ // calls updateSubscribers when new config is returned from the config source.
+ // In the last case the method below will return null.
+ RawConfig config = configSourceClient.getConfig(RawConfig.createFromServerRequest(req), req);
+ if (configOrGenerationHasChanged(config, req)) {
+ cacheManager.putInCache(config);
+ }
+ return config;
+ }
+
+ static boolean configOrGenerationHasChanged(RawConfig config, JRTServerConfigRequest request) {
+ return (config != null && (!config.hasEqualConfig(request) || config.hasNewerGeneration(request)));
+ }
+
+ Mode getMode() {
+ return mode;
+ }
+
+ void setMode(String modeName) {
+ if (modeName.equals(this.mode.name())) return;
+
+ log.log(LogLevel.INFO, "Switching from " + this.mode + " mode to " + modeName.toLowerCase() + " mode");
+ this.mode = new Mode(modeName);
+ if (mode.isMemoryCache()) {
+ configSourceClient.shutdownSourceConnections();
+ configSourceClient = new MemoryCacheConfigClient(cacheManager.getMemoryCache());
+ } else if (mode.isDefault()) {
+ flush();
+ configSourceClient = createRpcClient();
+ } else {
+ throw new IllegalArgumentException("Not able to handle mode '" + modeName + "'");
+ }
+ }
+
+ private RpcConfigSourceClient createRpcClient() {
+ return new RpcConfigSourceClient((ConfigSourceSet) configSource, clientUpdater, cacheManager, timingValues, statistics, delayedResponses);
+ }
+
+ private void setupSigTermHandler() {
+ CatchSigTerm.setup(signalCaught); // catch termination signal
+ }
+
+ private void waitForShutdown() {
+ synchronized (signalCaught) {
+ while (!signalCaught.get()) {
+ try {
+ signalCaught.wait();
+ } catch (InterruptedException e) {
+ // empty
+ }
+ }
+ }
+ stop();
+ System.exit(0);
+ }
+
+ public static void main(String[] args) {
+ /* Initialize the log handler */
+ LogSetup.clearHandlers();
+ LogSetup.initVespaLogging("configproxy");
+
+ Properties properties = getSystemProperties();
+
+ int port = DEFAULT_RPC_PORT;
+ if (args.length > 0) {
+ port = Integer.parseInt(args[0]);
+ }
+ Event.started("configproxy");
+ ConfigProxyStatistics statistics = new ConfigProxyStatistics(properties.eventInterval);
+ Thread t = new Thread(statistics);
+ t.setName("Metrics generator");
+ t.setDaemon(true);
+ t.start();
+
+ Mode startupMode = new Mode(properties.mode);
+ if (!startupMode.isDefault()) log.log(LogLevel.INFO, "Starting config proxy in '" + startupMode + "' mode");
+
+ if (startupMode.isMemoryCache()) {
+ log.log(LogLevel.ERROR, "Starting config proxy in '" + startupMode + "' mode is not allowed");
+ System.exit(1);
+ }
+
+ ConfigSourceSet configSources = new ConfigSourceSet();
+ if (startupMode.requiresConfigSource()) {
+ configSources = new ConfigSourceSet(properties.configSources);
+ }
+ DelayedResponses delayedResponses = new DelayedResponses(statistics);
+ ProxyServer proxyServer = ProxyServer.create(port, delayedResponses, configSources, statistics, startupMode);
+ // catch termination signal
+ proxyServer.setupSigTermHandler();
+ Thread proxyserverThread = new Thread(proxyServer);
+ proxyserverThread.setName("configproxy");
+ proxyserverThread.start();
+ proxyServer.waitForShutdown();
+ }
+
+ static Properties getSystemProperties() {
+ // Read system properties
+ long eventInterval = Long.getLong("eventinterval", ConfigProxyStatistics.defaultEventInterval);
+ String mode = System.getProperty("mode", Mode.ModeName.DEFAULT.name());
+ final String[] inputConfigSources = System.getProperty("proxyconfigsources", DEFAULT_PROXY_CONFIG_SOURCES).split(",");
+ return new Properties(eventInterval, mode, inputConfigSources);
+ }
+
+ static class Properties {
+ final long eventInterval;
+ final String mode;
+ final String[] configSources;
+
+ Properties(long eventInterval, String mode, String[] configSources) {
+ this.eventInterval = eventInterval;
+ this.mode = mode;
+ this.configSources = configSources;
+ }
+ }
+
+ static TimingValues defaultTimingValues() {
+ return defaultTimingValues;
+ }
+
+ TimingValues getTimingValues() {
+ return timingValues;
+ }
+
+ ConfigProxyStatistics getStatistics() {
+ return statistics;
+ }
+
+ // Cancels all config instances and flushes the cache. When this method returns,
+ // the cache will not be updated again before someone calls getConfig().
+ synchronized void flush() {
+ cacheManager.getMemoryCache().clear();
+ configSourceClient.cancel();
+ }
+
+ public void stop() {
+ Event.stopping("configproxy", "shutdown");
+ if (rpcServer != null) rpcServer.shutdown();
+ if (checkerHandle != null) checkerHandle.cancel(true);
+ flush();
+ if (statistics != null) {
+ statistics.stop();
+ }
+ }
+
+ public CacheManager getCacheManager() {
+ return cacheManager;
+ }
+
+ String getActiveSourceConnection() {
+ return configSourceClient.getActiveSourceConnection();
+ }
+
+ List<String> getSourceConnections() {
+ return configSourceClient.getSourceConnections();
+ }
+
+ public void updateSourceConnections(List<String> sources) {
+ configSource = new ConfigSourceSet(sources);
+ flush();
+ configSourceClient = createRpcClient();
+ }
+}
diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/RpcConfigSourceClient.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/RpcConfigSourceClient.java
new file mode 100644
index 00000000000..100de46901e
--- /dev/null
+++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/RpcConfigSourceClient.java
@@ -0,0 +1,197 @@
+// Copyright 2016 Yahoo Inc. 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.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 com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.*;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+
+import java.util.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.logging.Logger;
+
+/**
+ * An Rpc client to a config source
+ *
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1.9
+ */
+public class RpcConfigSourceClient extends ConfigSourceClient {
+
+ private final static Logger log = Logger.getLogger(RpcConfigSourceClient.class.getName());
+ private final Supervisor supervisor = new Supervisor(new Transport());
+
+ private final ConfigSourceSet configSourceSet;
+ private final HashMap<ConfigCacheKey, Subscriber> activeSubscribers = new HashMap<>();
+ private final Object activeSubscribersLock = new Object();
+ private final MemoryCache memoryCache;
+ private final ClientUpdater clientUpdater;
+ private final DelayedResponses delayedResponses;
+ private final TimingValues timingValues;
+
+ private ExecutorService exec;
+ private Map<ConfigSourceSet, JRTConfigRequester> requesterPool;
+
+
+ RpcConfigSourceClient(ConfigSourceSet configSourceSet, ClientUpdater clientUpdater,
+ CacheManager cacheManager,
+ TimingValues timingValues,
+ ConfigProxyStatistics statistics,
+ DelayedResponses delayedResponses) {
+ this.configSourceSet = configSourceSet;
+ this.clientUpdater = clientUpdater;
+ this.memoryCache = cacheManager.getMemoryCache();
+ this.delayedResponses = delayedResponses;
+ this.timingValues = timingValues;
+ checkConfigSources();
+ exec = Executors.newCachedThreadPool(new DaemonThreadFactory());
+ requesterPool = createRequesterPool(configSourceSet, timingValues);
+ }
+
+ /**
+ * Creates a requester (connection) pool of one entry, to be used each time this {@link RpcConfigSourceClient} is used
+ * @param ccs a {@link ConfigSourceSet}
+ * @param timingValues a {@link TimingValues}
+ * @return requester map
+ */
+ private Map<ConfigSourceSet, JRTConfigRequester> createRequesterPool(ConfigSourceSet ccs, TimingValues timingValues) {
+ Map<ConfigSourceSet, JRTConfigRequester> ret = new HashMap<>();
+ if (ccs.getSources().isEmpty()) return ret; // unit test, just skip creating any requester
+ ret.put(ccs, JRTConfigRequester.get(new JRTConnectionPool(ccs), timingValues));
+ return ret;
+ }
+
+ /**
+ * Checks if config sources are available
+ */
+ private boolean checkConfigSources() {
+ if (configSourceSet == null || configSourceSet.getSources() == null || configSourceSet.getSources().size() == 0) {
+ log.log(LogLevel.WARNING, "No config sources defined, could not check connection");
+ return false;
+ } else {
+ Request req = new Request("ping");
+ for (String configSource : configSourceSet.getSources()) {
+ Spec spec = new Spec(configSource);
+ Target target = supervisor.connect(spec);
+ target.invokeSync(req, 10.0);
+ if (target.isValid()) {
+ log.log(LogLevel.DEBUG, "Created connection to config source at " + spec.toString());
+ return true;
+ } else {
+ log.log(LogLevel.WARNING, "Could not connect to config source at " + spec.toString());
+ }
+ target.close();
+ }
+ String extra = "";
+ log.log(LogLevel.ERROR, "Could not connect to any config source in set " + configSourceSet.toString() +
+ ", please make sure config server(s) are running. " + extra);
+ }
+ return false;
+ }
+
+ /**
+ * Retrieves the requested config from the cache or the remote server.
+ * <p>
+ * 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.
+ * <p>
+ * If the config was not in cache, this method starts a <em>Subscriber</em> 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
+ RawConfig getConfig(RawConfig input, JRTServerConfigRequest request) {
+ long start = System.currentTimeMillis();
+ RawConfig ret = null;
+ final ConfigCacheKey configCacheKey = new ConfigCacheKey(input.getKey(), input.getDefMd5());
+ RawConfig cachedConfig = memoryCache.get(configCacheKey);
+ boolean needToGetConfig = true;
+
+ if (cachedConfig != null) {
+ log.log(LogLevel.DEBUG, "Found config " + configCacheKey + " in cache, generation=" + cachedConfig.getGeneration() +
+ ",configmd5=" + cachedConfig.getConfigMd5());
+ if (log.isLoggable(LogLevel.SPAM)) {
+ log.log(LogLevel.SPAM, "input config=" + input + ",cached config=" + cachedConfig);
+ }
+ // equals() also takes generation into account
+ if (!cachedConfig.equals(input)) {
+ log.log(LogLevel.SPAM, "Cached config is not equal to requested, will return it");
+ ret = cachedConfig;
+ }
+ if (!cachedConfig.isError()) {
+ needToGetConfig = false;
+ }
+ }
+ if (!ProxyServer.configOrGenerationHasChanged(ret, request)) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Delaying response " + request.getShortDescription() + " (" +
+ (System.currentTimeMillis() - start) + " ms)");
+ }
+ delayedResponses.add(new DelayedResponse(request));
+ }
+ if (needToGetConfig) {
+ synchronized (activeSubscribersLock) {
+ if (activeSubscribers.containsKey(configCacheKey)) {
+ log.log(LogLevel.DEBUG, "Already a subscriber running for: " + configCacheKey);
+ } else {
+ log.log(LogLevel.DEBUG, "Could not find good config in cache, creating subscriber for: " + configCacheKey);
+ Subscriber subscriber = new UpstreamConfigSubscriber(input, clientUpdater, configSourceSet, timingValues, requesterPool, activeSubscribers);
+ activeSubscribers.put(configCacheKey, subscriber);
+ exec.execute(subscriber);
+ }
+ }
+ }
+ return ret;
+ }
+
+ @Override
+ void cancel() {
+ shutdownSourceConnections();
+ }
+
+ /**
+ * Takes down connection(s) to config sources and running tasks
+ */
+ @Override
+ void shutdownSourceConnections() {
+ synchronized (activeSubscribersLock) {
+ for (Subscriber subscriber : activeSubscribers.values()) {
+ subscriber.cancel();
+ }
+ activeSubscribers.clear();
+ }
+ exec.shutdown();
+ for (JRTConfigRequester requester : requesterPool.values()) {
+ requester.close();
+ }
+ }
+
+ @Override
+ String getActiveSourceConnection() {
+ if (requesterPool.get(configSourceSet) != null) {
+ return requesterPool.get(configSourceSet).getConnectionPool().getCurrent().getAddress();
+ } else {
+ return "";
+ }
+ }
+
+ @Override
+ List<String> getSourceConnections() {
+ ArrayList<String> ret = new ArrayList<>();
+ final JRTConfigRequester jrtConfigRequester = requesterPool.get(configSourceSet);
+ if (jrtConfigRequester != null) {
+ ret.addAll(configSourceSet.getSources());
+ }
+ return ret;
+ }
+}
diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/RpcServer.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/RpcServer.java
new file mode 100644
index 00000000000..c4c31b315dc
--- /dev/null
+++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/RpcServer.java
@@ -0,0 +1,15 @@
+// Copyright 2016 Yahoo Inc. 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.vespa.config.RawConfig;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+
+/**
+ * @author musum
+ */
+interface RpcServer {
+
+ void returnOkResponse(JRTServerConfigRequest request, RawConfig config);
+
+ void returnErrorResponse(JRTServerConfigRequest request, int errorCode, String message);
+}
diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/Subscriber.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/Subscriber.java
new file mode 100644
index 00000000000..49e2bb86a15
--- /dev/null
+++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/Subscriber.java
@@ -0,0 +1,13 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.proxy;
+
+/**
+ * Interface for subscribing to config from upstream config sources.
+ *
+ * @author musum
+ * @since 5.5
+ */
+public interface Subscriber extends Runnable {
+
+ void cancel();
+}
diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/UpstreamConfigSubscriber.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/UpstreamConfigSubscriber.java
new file mode 100644
index 00000000000..849eb3f7910
--- /dev/null
+++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/UpstreamConfigSubscriber.java
@@ -0,0 +1,108 @@
+// Copyright 2016 Yahoo Inc. 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.config.ConfigurationRuntimeException;
+import com.yahoo.config.subscription.ConfigSource;
+import com.yahoo.config.subscription.ConfigSourceSet;
+import com.yahoo.config.subscription.impl.GenericConfigHandle;
+import com.yahoo.config.subscription.impl.GenericConfigSubscriber;
+import com.yahoo.config.subscription.impl.JRTConfigRequester;
+import com.yahoo.log.LogLevel;
+import com.yahoo.yolean.Exceptions;
+import com.yahoo.vespa.config.ConfigCacheKey;
+import com.yahoo.vespa.config.RawConfig;
+import com.yahoo.vespa.config.TimingValues;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Logger;
+
+/**
+ * @author musum
+ * @since 5.5
+ */
+public class UpstreamConfigSubscriber implements Subscriber {
+ private final static Logger log = Logger.getLogger(UpstreamConfigSubscriber.class.getName());
+
+ private final RawConfig config;
+ private final ClientUpdater clientUpdater;
+ private final ConfigSource configSourceSet;
+ private final TimingValues timingValues;
+ private Map<ConfigSourceSet, JRTConfigRequester> requesterPool;
+ private final Map<ConfigCacheKey, Subscriber> activeSubscribers;
+ private GenericConfigSubscriber subscriber;
+
+ public UpstreamConfigSubscriber(RawConfig config,
+ ClientUpdater clientUpdater,
+ ConfigSource configSourceSet,
+ TimingValues timingValues,
+ Map<ConfigSourceSet, JRTConfigRequester> requesterPool,
+ Map<ConfigCacheKey, Subscriber> activeSubscribers) {
+ this.config = config;
+ this.clientUpdater = clientUpdater;
+ this.configSourceSet = configSourceSet;
+ this.timingValues = timingValues;
+ this.requesterPool = requesterPool;
+ this.activeSubscribers = activeSubscribers;
+ }
+
+ UpstreamConfigSubscriber(RawConfig config,
+ ClientUpdater clientUpdater,
+ ConfigSource configSourceSet,
+ TimingValues timingValues,
+ Map<ConfigSourceSet, JRTConfigRequester> requesterPool) {
+ this(config, clientUpdater, configSourceSet, timingValues, requesterPool, new HashMap<>());
+ }
+
+ @Override
+ public void run() {
+ GenericConfigHandle handle;
+ subscriber = new GenericConfigSubscriber(requesterPool);
+ try {
+ handle = subscriber.subscribe(config.getKey(), config.getDefContent(), configSourceSet, timingValues);
+ } catch (ConfigurationRuntimeException e) {
+ log.log(LogLevel.INFO, "Subscribe for '" + config + "' failed, closing subscriber");
+ final ConfigCacheKey key = new ConfigCacheKey(config.getKey(), config.getDefMd5());
+ synchronized (activeSubscribers) {
+ final Subscriber activeSubscriber = activeSubscribers.get(key);
+ if (activeSubscriber != null) {
+ activeSubscriber.cancel();
+ activeSubscribers.remove(key);
+ }
+ }
+ return;
+ }
+
+ do {
+ try {
+ if (subscriber.nextGeneration()) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "nextGeneration returned for " + config.getKey() + ", subscriber generation=" + subscriber.getGeneration());
+ }
+ updateWithNewConfig(handle);
+ }
+ } catch (Exception e) { // To avoid thread throwing exception and loop never running this again
+ log.log(LogLevel.WARNING, "Got exception: " + Exceptions.toMessageString(e));
+ } catch (Throwable e) {
+ com.yahoo.protect.Process.logAndDie("Got error, exiting: " + Exceptions.toMessageString(e));
+ }
+ } while (!subscriber.isClosed());
+ }
+
+ private void updateWithNewConfig(GenericConfigHandle handle) {
+ final RawConfig newConfig = handle.getRawConfig();
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "config to be returned for '" + newConfig.getKey() +
+ "', generation=" + newConfig.getGeneration() +
+ ", payload=" + newConfig.getPayload());
+ }
+ clientUpdater.updateSubscribers(newConfig);
+ }
+
+ @Override
+ public void cancel() {
+ if (subscriber != null) {
+ subscriber.close();
+ }
+ }
+}
diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/package-info.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/package-info.java
new file mode 100644
index 00000000000..9ccbf4f7749
--- /dev/null
+++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/package-info.java
@@ -0,0 +1,47 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ Provides the classes for the Vespa config proxy.
+
+ <p>The {@link com.yahoo.vespa.config.proxy.ProxyServer ProxyServer}
+ contains the main method that instantiates a new ProxyServer that
+ listens for incoming config requests from clients at a given
+ port. The ProxyServer handles communication with the remote server,
+ and keeps a cache of {@link com.yahoo.vespa.config.RawConfig RawConfig}
+ objects.
+ </p>
+
+ <p>This package is very slim, due to extensive reuse of low-level code from
+ the config client library.
+ </p>
+
+ <h3>Why Vespa needs a config proxy</h3>
+
+ <p>It is possible for a client to subscribe
+ to config from the config server directly. However, if all Vespa
+ services applied this philosophy, it would cause a tremendous load
+ on the server that would need to handle a very large number of
+ requests from individual clients. Even more importantly, it would
+ inflict a huge load on the network if all config requests were
+ sent to the remote server.
+ </p>
+
+ <p>
+ Typically, one config proxy is running on each host in a Vespa
+ installation, so each subscriber on that host sends requests to
+ the proxy and never to the remote server. The proxy is responsible
+ for keeping that config up-to-date on behalf of all clients that
+ subscribe to it. This means that multiple subscribers to the same
+ config (and using the same config id) maps to only one request
+ from the proxy to the external server.
+ </p>
+
+ <p>Another advantage with a local config proxy on each node is
+ that subscribers become less vulnerable to network
+ failures. Should the network or the remote server go down for a
+ short period, only the proxy will notice, hence removing overhead from
+ the subscribers.
+ </p>
+*/
+
+@com.yahoo.api.annotations.PackageMarker
+package com.yahoo.vespa.config.proxy;
diff --git a/config-proxy/src/main/sh/cloudconfig_loadtester b/config-proxy/src/main/sh/cloudconfig_loadtester
new file mode 100644
index 00000000000..882f3af037b
--- /dev/null
+++ b/config-proxy/src/main/sh/cloudconfig_loadtester
@@ -0,0 +1,68 @@
+#!/bin/sh
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+# BEGIN environment bootstrap section
+# Do not edit between here and END as this section should stay identical in all scripts
+
+findpath () {
+ myname=${0}
+ mypath=${myname%/*}
+ myname=${myname##*/}
+ if [ "$mypath" ] && [ -d "$mypath" ]; then
+ return
+ fi
+ mypath=$(pwd)
+ if [ -f "${mypath}/${myname}" ]; then
+ return
+ fi
+ echo "FATAL: Could not figure out the path where $myname lives from $0"
+ exit 1
+}
+
+COMMON_ENV=libexec/vespa/common-env.sh
+
+source_common_env () {
+ if [ "$VESPA_HOME" ] && [ -d "$VESPA_HOME" ]; then
+ # ensure it ends with "/" :
+ VESPA_HOME=${VESPA_HOME%/}/
+ export VESPA_HOME
+ common_env=$VESPA_HOME/$COMMON_ENV
+ if [ -f "$common_env" ]; then
+ . $common_env
+ return
+ fi
+ fi
+ return 1
+}
+
+findroot () {
+ source_common_env && return
+ if [ "$VESPA_HOME" ]; then
+ echo "FATAL: bad VESPA_HOME value '$VESPA_HOME'"
+ exit 1
+ fi
+ if [ "$ROOT" ] && [ -d "$ROOT" ]; then
+ VESPA_HOME="$ROOT"
+ source_common_env && return
+ fi
+ findpath
+ while [ "$mypath" ]; do
+ VESPA_HOME=${mypath}
+ source_common_env && return
+ mypath=${mypath%/*}
+ done
+ echo "FATAL: missing VESPA_HOME environment variable"
+ echo "Could not locate $COMMON_ENV anywhere"
+ exit 1
+}
+
+findroot
+
+# END environment bootstrap section
+
+ROOT=$VESPA_HOME
+export ROOT
+
+echo "# Using CLASSPATH=$CLASSPATH, args=$@"
+
+java -cp $CLASSPATH:$ROOT/lib/jars/config-proxy-jar-with-dependencies.jar com.yahoo.vespa.config.benchmark.LoadTester "$@"
diff --git a/config-proxy/src/main/sh/cloudconfig_verification b/config-proxy/src/main/sh/cloudconfig_verification
new file mode 100644
index 00000000000..ba5504e95d9
--- /dev/null
+++ b/config-proxy/src/main/sh/cloudconfig_verification
@@ -0,0 +1,68 @@
+#!/bin/sh
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+# BEGIN environment bootstrap section
+# Do not edit between here and END as this section should stay identical in all scripts
+
+findpath () {
+ myname=${0}
+ mypath=${myname%/*}
+ myname=${myname##*/}
+ if [ "$mypath" ] && [ -d "$mypath" ]; then
+ return
+ fi
+ mypath=$(pwd)
+ if [ -f "${mypath}/${myname}" ]; then
+ return
+ fi
+ echo "FATAL: Could not figure out the path where $myname lives from $0"
+ exit 1
+}
+
+COMMON_ENV=libexec/vespa/common-env.sh
+
+source_common_env () {
+ if [ "$VESPA_HOME" ] && [ -d "$VESPA_HOME" ]; then
+ # ensure it ends with "/" :
+ VESPA_HOME=${VESPA_HOME%/}/
+ export VESPA_HOME
+ common_env=$VESPA_HOME/$COMMON_ENV
+ if [ -f "$common_env" ]; then
+ . $common_env
+ return
+ fi
+ fi
+ return 1
+}
+
+findroot () {
+ source_common_env && return
+ if [ "$VESPA_HOME" ]; then
+ echo "FATAL: bad VESPA_HOME value '$VESPA_HOME'"
+ exit 1
+ fi
+ if [ "$ROOT" ] && [ -d "$ROOT" ]; then
+ VESPA_HOME="$ROOT"
+ source_common_env && return
+ fi
+ findpath
+ while [ "$mypath" ]; do
+ VESPA_HOME=${mypath}
+ source_common_env && return
+ mypath=${mypath%/*}
+ done
+ echo "FATAL: missing VESPA_HOME environment variable"
+ echo "Could not locate $COMMON_ENV anywhere"
+ exit 1
+}
+
+findroot
+
+# END environment bootstrap section
+
+ROOT=$VESPA_HOME
+export ROOT
+
+echo "# Using CLASSPATH=$CLASSPATH, args=$@"
+
+java -cp $CLASSPATH:$ROOT/lib/jars/config-proxy-jar-with-dependencies.jar com.yahoo.vespa.config.ConfigVerification "$@"
diff --git a/config-proxy/src/main/sh/config-ctl b/config-proxy/src/main/sh/config-ctl
new file mode 100755
index 00000000000..36155560d8d
--- /dev/null
+++ b/config-proxy/src/main/sh/config-ctl
@@ -0,0 +1,162 @@
+#!/bin/sh
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+# BEGIN environment bootstrap section
+# Do not edit between here and END as this section should stay identical in all scripts
+
+findpath () {
+ myname=${0}
+ mypath=${myname%/*}
+ myname=${myname##*/}
+ if [ "$mypath" ] && [ -d "$mypath" ]; then
+ return
+ fi
+ mypath=$(pwd)
+ if [ -f "${mypath}/${myname}" ]; then
+ return
+ fi
+ echo "FATAL: Could not figure out the path where $myname lives from $0"
+ exit 1
+}
+
+COMMON_ENV=libexec/vespa/common-env.sh
+
+source_common_env () {
+ if [ "$VESPA_HOME" ] && [ -d "$VESPA_HOME" ]; then
+ # ensure it ends with "/" :
+ VESPA_HOME=${VESPA_HOME%/}/
+ export VESPA_HOME
+ common_env=$VESPA_HOME/$COMMON_ENV
+ if [ -f "$common_env" ]; then
+ . $common_env
+ return
+ fi
+ fi
+ return 1
+}
+
+findroot () {
+ source_common_env && return
+ if [ "$VESPA_HOME" ]; then
+ echo "FATAL: bad VESPA_HOME value '$VESPA_HOME'"
+ exit 1
+ fi
+ if [ "$ROOT" ] && [ -d "$ROOT" ]; then
+ VESPA_HOME="$ROOT"
+ source_common_env && return
+ fi
+ findpath
+ while [ "$mypath" ]; do
+ VESPA_HOME=${mypath}
+ source_common_env && return
+ mypath=${mypath%/*}
+ done
+ echo "FATAL: missing VESPA_HOME environment variable"
+ echo "Could not locate $COMMON_ENV anywhere"
+ exit 1
+}
+
+findroot
+
+# END environment bootstrap section
+
+ROOT=$VESPA_HOME
+export ROOT
+cd $ROOT || { echo "Cannot cd to $ROOT" 1>&2; exit 1; }
+
+# get common PATH:
+. $ROOT/libexec/vespa/common-env.sh
+
+P_SENTINEL=var/run/sentinel.pid
+P_CONFIG_PROXY=var/run/configproxy.pid
+
+export P_SENTINEL P_CONFIG_PROXY
+
+LOGDIR="$ROOT/logs/vespa"
+LOGFILE="$LOGDIR/vespa.log"
+VESPA_LOG_TARGET="file:$LOGFILE"
+VESPA_LOG_CONTROL_DIR="$ROOT/var/db/vespa/logcontrol"
+cp="libexec/vespa/patches/configproxy:lib/jars/config-proxy-jar-with-dependencies.jar"
+
+VESPA_LOG_LEVEL="all -debug -spam"
+
+export VESPA_LOG_TARGET VESPA_LOG_LEVEL VESPA_LOG_CONTROL_DIR
+export VESPA_SENTINEL_PORT
+
+mkdir -p "$LOGDIR"
+mkdir -p "$VESPA_LOG_CONTROL_DIR"
+
+# sanity check hostname
+hname=$(hostname)
+canon=$(perl -e 'use Socket;
+ my $hostname = `hostname`; chomp($hostname);
+ my ($err, @results) = Socket::getaddrinfo($hostname, 0, {"flags" => Socket::AI_CANONNAME});
+ print @results[0]->{"canonname"} . "\n";')
+if [ "$hname" != "$canon" ]; then
+ echo "The hostname ($hname) must match with gethostbyname (was $canon)"
+ exit 1
+fi
+
+CONFIG_ID="hosts/$hname"
+export CONFIG_ID
+export MALLOC_ARENA_MAX=1 #Does not need fast allocation
+export LD_LIBRARY_PATH="$VESPA_HOME/lib64"
+
+
+case $1 in
+ start)
+ configsources=`libexec/vespa/vespa-config.pl -configsources`
+ userargs=$vespa_base__jvmargs_configproxy
+ if [ "$userargs" == "" ]; then
+ userargs=$services__jvmargs_configproxy
+ fi
+ proxymode=$services__configproxy_mode
+ jvmopts="-Xms32M -Xmx256M -XX:ThreadStackSize=256 -XX:MaxJavaStackTraceDepth=-1"
+
+ VESPA_SERVICE_NAME=configproxy
+ export VESPA_SERVICE_NAME
+ if [ "$proxymode" ]; then
+ echo "Will start config proxy in mode $proxymode"
+ fi
+ echo "Starting config proxy using $configsources as config source(s)"
+ vespa-runserver -r 10 -s configproxy -p $P_CONFIG_PROXY -- \
+ java ${jvmopts} \
+ -XX:OnOutOfMemoryError="kill -9 %p" $(getJavaOptionsIPV46) \
+ -Dproxyconfigsources="${configsources}" -Dmode="${proxymode}" ${userargs} \
+ -cp $cp com.yahoo.vespa.config.proxy.ProxyServer 19090
+
+ echo "Waiting for config proxy to start"
+ fail=true
+ for ((sleepcount=0;$sleepcount<600;sleepcount=$sleepcount+1)) ; do
+ usleep 100000
+ if [ -f $P_CONFIG_PROXY ] && kill -0 `cat $P_CONFIG_PROXY` && pingproxy -s `hostname` 2>/dev/null
+ then
+ echo "config proxy started (runserver pid `cat $P_CONFIG_PROXY`)"
+ fail=false
+ break
+ fi
+ done
+ if $fail ; then
+ echo "Failed to start config proxy!" 1>&2
+ echo "look for reason in vespa.log, last part follows..."
+ tail -n 15 $LOGFILE | logfmt -
+ exit 1
+ fi
+
+ VESPA_SERVICE_NAME=config-sentinel
+ export VESPA_SERVICE_NAME
+
+ vespa-runserver -s config-sentinel -r 10 -p $P_SENTINEL -- \
+ sbin/config-sentinel -c "$CONFIG_ID"
+ ;;
+
+ stop)
+ vespa-runserver -s config-sentinel -p $P_SENTINEL -S
+ vespa-runserver -s configproxy -p $P_CONFIG_PROXY -S
+ ;;
+
+ *)
+ echo "Unknown option $@" 1>&2
+ exit 1
+ ;;
+esac
diff --git a/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/CacheTest.java b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/CacheTest.java
new file mode 100644
index 00000000000..e4ddee9da97
--- /dev/null
+++ b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/CacheTest.java
@@ -0,0 +1,77 @@
+// Copyright 2016 Yahoo Inc. 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.slime.Slime;
+import com.yahoo.vespa.config.ConfigCacheKey;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.RawConfig;
+import com.yahoo.vespa.config.protocol.Payload;
+import org.junit.Before;
+
+import java.util.ArrayList;
+import java.util.Optional;
+
+/**
+ * Helper class for memory and disk cache unit tests
+ *
+ * @author musum
+ * @since 5.1.10
+ */
+public class CacheTest {
+ String defName = "foo";
+ String configId = "id";
+ String namespace = "bar";
+ String defMd5 = "a";
+
+ long generation = 1L;
+ String defName2 = "baz-quux";
+ String namespace2 = "search.config";
+ // Test with a config id with / in it
+ String configId2 = "clients/gateways/gateway/component/com.yahoo.feedhandler.VespaFeedHandlerRemoveLocation";
+ String defMd52 = "a2";
+ String differentDefMd5 = "09ef";
+ String configMd5 = "b";
+ ConfigKey<?> configKey = new ConfigKey<>(defName, configId, namespace);
+ ConfigKey<?> configKey2 = new ConfigKey<>(defName2, configId2, namespace2);
+ ConfigCacheKey cacheKey;
+ ConfigCacheKey cacheKeyDifferentMd5;
+ ConfigCacheKey cacheKey2;
+ RawConfig config;
+ RawConfig config2;
+ RawConfig configDifferentMd5;
+ RawConfig unknown;
+ Payload payload;
+ Payload payload2;
+ Payload payloadDifferentMd5;
+
+ public CacheTest() {
+ }
+
+ @Before
+ public void setup() {
+ ArrayList<String> defContent = new ArrayList<>();
+ defContent.add("bar string");
+
+ Slime slime = new Slime();
+ slime.setString("bar \"value\"");
+ payload = Payload.from(new ConfigPayload(slime));
+
+ slime = new Slime();
+ slime.setString("bar \"baz\"");
+ payload2 = Payload.from(new ConfigPayload(slime));
+
+ slime = new Slime();
+ slime.setString("bar \"value2\"");
+ payloadDifferentMd5 = Payload.from(new ConfigPayload(slime));
+
+ config = new RawConfig(configKey, defMd5, payload, configMd5, generation, defContent, Optional.empty());
+ config2 = new RawConfig(configKey2, defMd52, payload2, configMd5, generation, defContent, Optional.empty());
+ unknown = new RawConfig(new ConfigKey<>("unknown", configId, namespace), defMd5, payload, configMd5, generation, defContent, Optional.empty());
+ configDifferentMd5 = new RawConfig(configKey, differentDefMd5, payloadDifferentMd5, configMd5, generation, defContent, Optional.empty());
+
+ cacheKey = new ConfigCacheKey(configKey, config.getDefMd5());
+ cacheKey2 = new ConfigCacheKey(configKey2, config2.getDefMd5());
+ cacheKeyDifferentMd5 = new ConfigCacheKey(configKey, differentDefMd5);
+ }
+} \ No newline at end of file
diff --git a/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/CheckDelayedResponsesTest.java b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/CheckDelayedResponsesTest.java
new file mode 100644
index 00000000000..f96f584f257
--- /dev/null
+++ b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/CheckDelayedResponsesTest.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.proxy;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author musum
+ */
+public class CheckDelayedResponsesTest {
+
+ private final MapBackedConfigSource source = new MapBackedConfigSource(UpstreamConfigSubscriberTest.MockClientUpdater.create());
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Before
+ public void setup() {
+ source.clear();
+ source.put(ProxyServerTest.fooConfig.getKey(), ProxyServerTest.createConfigWithNextConfigGeneration(ProxyServerTest.fooConfig, 0));
+ }
+
+ @Test
+ public void basic() {
+ ConfigTester tester = new ConfigTester();
+ ConfigProxyStatistics statistics = new ConfigProxyStatistics();
+ DelayedResponses delayedResponses = new DelayedResponses(statistics);
+ final MockRpcServer mockRpcServer = new MockRpcServer();
+ final MemoryCache memoryCache = new MemoryCache();
+ memoryCache.put(ProxyServerTest.fooConfig);
+ final CheckDelayedResponses checkDelayedResponses = new CheckDelayedResponses(delayedResponses, memoryCache, mockRpcServer);
+ delayedResponses.add(new DelayedResponse(tester.createRequest(ProxyServerTest.fooConfig, 0)));
+ delayedResponses.add(new DelayedResponse(tester.createRequest(ProxyServerTest.fooConfig, 1200000))); // should not be returned yet
+ delayedResponses.add(new DelayedResponse(tester.createRequest(ProxyServerTest.errorConfig, 0))); // will not give a config when resolving
+ checkDelayedResponses.checkDelayedResponses();
+
+ assertThat(mockRpcServer.responses, is(1L));
+ }
+
+}
diff --git a/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ClientUpdaterTest.java b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ClientUpdaterTest.java
new file mode 100644
index 00000000000..7aca5990a12
--- /dev/null
+++ b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ClientUpdaterTest.java
@@ -0,0 +1,113 @@
+// Copyright 2016 Yahoo Inc. 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.vespa.config.ConfigCacheKey;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.RawConfig;
+import com.yahoo.vespa.config.protocol.JRTConfigRequestFactory;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequestV3;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static junit.framework.TestCase.assertNull;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author musum
+ */
+public class ClientUpdaterTest {
+ private MockRpcServer rpcServer;
+ private ConfigProxyStatistics statistics;
+ private DelayedResponses delayedResponses;
+ private Mode mode;
+ private MemoryCache memoryCache;
+ private ClientUpdater clientUpdater;
+
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+
+
+ @Before
+ public void setup() {
+ rpcServer = new MockRpcServer();
+ statistics = new ConfigProxyStatistics();
+ delayedResponses = new DelayedResponses(statistics);
+ mode = new Mode();
+ memoryCache = new MemoryCache();
+ clientUpdater = new ClientUpdater(CacheManager.createTestCacheManager(memoryCache),
+ rpcServer,
+ statistics,
+ delayedResponses,
+ mode);
+ }
+
+ @Test
+ public void basic() {
+ assertThat(rpcServer.responses, is(0L));
+
+ final RawConfig fooConfig = ProxyServerTest.fooConfig;
+ clientUpdater.updateSubscribers(fooConfig);
+
+ // No delayed response, so not returned
+ assertResponseAndCache(rpcServer, memoryCache, fooConfig, 0, 1);
+
+ delayedResponses.add(new DelayedResponse(JRTServerConfigRequestV3.createFromRequest(JRTConfigRequestFactory.createFromRaw(fooConfig, -10L).getRequest())));
+ clientUpdater.updateSubscribers(fooConfig);
+ assertResponseAndCache(rpcServer, memoryCache, fooConfig, 1, 1);
+
+ // Will not find bar config in delayed responses
+ RawConfig barConfig = new RawConfig(new ConfigKey<>("bar", "id", "namespace"), fooConfig.getDefMd5());
+ clientUpdater.updateSubscribers(barConfig);
+ assertResponseAndCache(rpcServer, memoryCache, barConfig, 1, 2);
+
+
+ mode = new Mode(Mode.ModeName.MEMORYCACHE.name());
+ // Nothing should be returned, so still 1 response
+ assertResponseAndCache(rpcServer, memoryCache, fooConfig, 1, 2);
+ assertThat(statistics.errors(), is(0L));
+ }
+
+ @Test
+ public void memoryCacheMode() {
+ final RawConfig fooConfig = ProxyServerTest.fooConfig;
+ mode = new Mode(Mode.ModeName.MEMORYCACHE.name());
+ clientUpdater = new ClientUpdater(CacheManager.createTestCacheManager(memoryCache),
+ rpcServer,
+ statistics,
+ delayedResponses,
+ mode);
+ memoryCache.clear();
+ assertThat(rpcServer.responses, is(0L));
+
+ clientUpdater.updateSubscribers(fooConfig);
+ assertNull(memoryCache.get(new ConfigCacheKey(fooConfig.getKey(), fooConfig.getDefMd5())));
+ assertThat(memoryCache.size(), is(0));
+ assertThat(rpcServer.responses, is(0L));
+ }
+
+ @Test
+ public void errorResponse() {
+ assertThat(rpcServer.responses, is(0L));
+
+ final RawConfig errorConfig = ProxyServerTest.errorConfig;
+
+ clientUpdater.updateSubscribers(errorConfig);
+ // Error response, so not put into cache
+ assertNull(memoryCache.get(new ConfigCacheKey(errorConfig.getKey(), errorConfig.getDefMd5())));
+ assertThat(rpcServer.responses, is(0L));
+ assertThat(statistics.errors(), is(1L));
+ }
+
+ private static void assertResponseAndCache(MockRpcServer rpcServer,
+ MemoryCache memoryCache,
+ RawConfig expectedConfig,
+ long expectedResponses,
+ int cacheSize) {
+ assertThat(rpcServer.responses, is(expectedResponses));
+ assertThat(memoryCache.size(), is(cacheSize));
+ assertThat(memoryCache.get(new ConfigCacheKey(expectedConfig.getKey(), expectedConfig.getDefMd5())), is(expectedConfig));
+ }
+}
diff --git a/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ConfigProxyRpcServerTest.java b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ConfigProxyRpcServerTest.java
new file mode 100644
index 00000000000..fbef9cf1d17
--- /dev/null
+++ b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ConfigProxyRpcServerTest.java
@@ -0,0 +1,253 @@
+// Copyright 2016 Yahoo Inc. 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.config.subscription.ConfigSourceSet;
+import com.yahoo.jrt.Request;
+import com.yahoo.jrt.Spec;
+import com.yahoo.jrt.StringValue;
+import com.yahoo.vespa.config.RawConfig;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1.9
+ */
+public class ConfigProxyRpcServerTest {
+ private static final String hostname = "localhost";
+ private static final int port = 12345;
+ private static final String address = "tcp/" + hostname + ":" + port;
+ ProxyServer proxyServer;
+ private ConfigProxyRpcServer rpcServer;
+
+ @Before
+ public void setup() {
+ proxyServer = ProxyServer.createTestServer(new ConfigSourceSet(address));
+ rpcServer = new ConfigProxyRpcServer(proxyServer, null);
+ }
+
+ @After
+ public void teardown() {
+ rpcServer.shutdown();
+ }
+
+ @Test
+ public void basic() {
+ ConfigSourceSet configSources = new ConfigSourceSet();
+ ProxyServer proxy = ProxyServer.createTestServer(configSources);
+ Spec spec = new Spec("localhost", 12345);
+ ConfigProxyRpcServer server = new ConfigProxyRpcServer(proxy, spec);
+ assertThat(server.getSpec(), is(spec));
+ }
+
+ /**
+ * Tests ping RPC command
+ */
+ @Test
+ public void testRpcMethodPing() {
+ Request req = new Request("ping");
+ rpcServer.ping(req);
+
+ assertFalse(req.errorMessage(), req.isError());
+ assertThat(req.returnValues().size(), is(1));
+ assertThat(req.returnValues().get(0).asInt32(), is(0));
+ }
+
+ /**
+ * Tests listCachedConfig RPC command
+ */
+ @Test
+ public void testRpcMethodListCachedConfig() {
+ Request req = new Request("listCachedConfig");
+ rpcServer.listCachedConfig(req);
+
+ assertFalse(req.errorMessage(), req.isError());
+ String[] ret = req.returnValues().get(0).asStringArray();
+ assertThat(req.returnValues().size(), is(1));
+ assertThat(ret.length, is(0));
+
+ final RawConfig config = ProxyServerTest.fooConfig;
+ proxyServer.getCacheManager().putInCache(config);
+ req = new Request("listCachedConfig");
+ rpcServer.listCachedConfig(req);
+ assertFalse(req.errorMessage(), req.isError());
+ assertThat(req.returnValues().size(), is(1));
+ ret = req.returnValues().get(0).asStringArray();
+ assertThat(ret.length, is(1));
+ assertThat(ret[0], is(config.getNamespace() + "." + config.getName() + "," +
+ config.getConfigId() + "," +
+ config.getGeneration() + "," +
+ config.getConfigMd5()));
+ }
+
+ /**
+ * Tests listCachedConfig RPC command
+ */
+ @Test
+ public void testRpcMethodListCachedConfigFull() {
+ Request req = new Request("listCachedConfigFull");
+ rpcServer.listCachedConfigFull(req);
+
+ assertFalse(req.errorMessage(), req.isError());
+ assertThat(req.returnValues().size(), is(1));
+ String[] ret = req.returnValues().get(0).asStringArray();
+ assertThat(ret.length, is(0));
+
+ final RawConfig config = ProxyServerTest.fooConfig;
+ proxyServer.getCacheManager().putInCache(config);
+ req = new Request("listCachedConfigFull");
+ rpcServer.listCachedConfigFull(req);
+ assertFalse(req.errorMessage(), req.isError());
+ ret = req.returnValues().get(0).asStringArray();
+ assertThat(ret.length, is(1));
+ assertThat(ret[0], is(config.getNamespace() + "." + config.getName() + "," +
+ config.getConfigId() + "," +
+ config.getGeneration() + "," +
+ config.getConfigMd5() + "," +
+ config.getPayload().getData()));
+ }
+
+ /**
+ * Tests printStatistics RPC command
+ */
+ @Test
+ public void testRpcMethodListSourceConnections() {
+ Request req = new Request("listSourceConnections");
+ rpcServer.listSourceConnections(req);
+
+ assertFalse(req.errorMessage(), req.isError());
+ assertThat(req.returnValues().size(), is(1));
+ final String[] ret = req.returnValues().get(0).asStringArray();
+ assertThat(ret.length, is(2));
+ assertThat(ret[0], is("Current source: " + address));
+ assertThat(ret[1], is("All sources:\n" + address + "\n"));
+ }
+
+ /**
+ * Tests printStatistics RPC command
+ */
+ @Test
+ public void testRpcMethodPrintStatistics() {
+ Request req = new Request("printStatistics");
+ rpcServer.printStatistics(req);
+ assertFalse(req.errorMessage(), req.isError());
+ assertThat(req.returnValues().size(), is(1));
+ assertThat(req.returnValues().get(0).asString(), is("\n" +
+ "Delayed responses queue size: 0\n" +
+ "Contents: "));
+ }
+
+ /**
+ * Tests invalidateCache RPC command
+ */
+ @Test
+ public void testRpcMethodInvalidateCache() {
+ Request req = new Request("invalidateCache");
+ rpcServer.invalidateCache(req);
+
+ assertFalse(req.errorMessage(), req.isError());
+ assertThat(req.returnValues().size(), is(1));
+ final String[] ret = req.returnValues().get(0).asStringArray();
+ assertThat(ret.length, is(2));
+ assertThat(ret[0], is("0"));
+ assertThat(ret[1], is("success"));
+ }
+
+ /**
+ * Tests getMode and setMode RPC commands
+ */
+ @Test
+ public void testRpcMethodGetModeAndSetMode() {
+ Request req = new Request("getMode");
+ rpcServer.getMode(req);
+ assertFalse(req.errorMessage(), req.isError());
+ assertThat(req.returnValues().size(), is(1));
+ assertThat(req.returnValues().get(0).asString(), is("default"));
+
+ req = new Request("setMode");
+ String mode = "memorycache";
+ req.parameters().add(new StringValue(mode));
+ rpcServer.setMode(req);
+ assertFalse(req.errorMessage(), req.isError());
+ assertThat(req.returnValues().size(), is(1));
+ String[] ret = req.returnValues().get(0).asStringArray();
+ assertThat(ret.length, is(2));
+ assertThat(ret[0], is("0"));
+ assertThat(ret[1], is("success"));
+ assertThat(proxyServer.getMode().name(), is(mode));
+
+ req = new Request("getMode");
+ rpcServer.getMode(req);
+ assertFalse(req.errorMessage(), req.isError());
+ assertThat(req.returnValues().size(), is(1));
+ assertThat(req.returnValues().get(0).asString(), is(mode));
+
+ req = new Request("setMode");
+ String oldMode = mode;
+ mode = "invalid";
+ req.parameters().add(new StringValue(mode));
+ rpcServer.setMode(req);
+
+ assertFalse(req.errorMessage(), req.isError());
+ ret = req.returnValues().get(0).asStringArray();
+ assertThat(ret.length, is(2));
+ assertThat(ret[0], is("1"));
+ assertThat(ret[1], is("Could not set mode to '" + mode + "'. Legal modes are '" + Mode.modes() + "'"));
+ assertThat(proxyServer.getMode().name(), is(oldMode));
+ }
+
+ /**
+ * Tests updateSources RPC command
+ */
+ @Test
+ public void testRpcMethodUpdateSources() {
+ Request req = new Request("updateSources");
+ String spec1 = "tcp/a:19070";
+ String spec2 = "tcp/b:19070";
+ req.parameters().add(new StringValue(spec1 + "," + spec2));
+ rpcServer.updateSources(req);
+ assertFalse(req.errorMessage(), req.isError());
+ assertThat(req.returnValues().size(), is(1));
+ assertThat(req.returnValues().get(0).asString(), is("Updated config sources to: " + spec1 + "," + spec2));
+
+
+ proxyServer.setMode(Mode.ModeName.MEMORYCACHE.name());
+
+ req = new Request("updateSources");
+ req.parameters().add(new StringValue(spec1 + "," + spec2));
+ rpcServer.updateSources(req);
+ assertFalse(req.errorMessage(), req.isError());
+ assertThat(req.returnValues().size(), is(1));
+ assertThat(req.returnValues().get(0).asString(), is("Cannot update sources when in '" + Mode.ModeName.MEMORYCACHE.name().toLowerCase() + "' mode"));
+
+ // TODO source connections needs to have deterministic order to work
+ /*req = new Request("listSourceConnections");
+ rpcServer.listSourceConnections(req);
+ assertFalse(req.errorMessage(), req.isError());
+ final String[] ret = req.returnValues().get(0).asStringArray();
+ assertThat(ret.length, is(2));
+ assertThat(ret[0], is("Current source: " + spec1));
+ assertThat(ret[1], is("All sources:\n" + spec2 + "\n" + spec1 + "\n"));
+ */
+ }
+
+ /**
+ * Tests dumpCache RPC command
+ */
+ @Test
+ public void testRpcMethodDumpCache() {
+ Request req = new Request("dumpCache");
+ String path = "/tmp";
+ req.parameters().add(new StringValue(path));
+ rpcServer.dumpCache(req);
+ assertFalse(req.errorMessage(), req.isError());
+ assertThat(req.returnValues().size(), is(1));
+ assertThat(req.returnValues().get(0).asString(), is("success"));
+ }
+
+}
diff --git a/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ConfigProxyStatisticsTest.java b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ConfigProxyStatisticsTest.java
new file mode 100644
index 00000000000..8f1529142af
--- /dev/null
+++ b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ConfigProxyStatisticsTest.java
@@ -0,0 +1,46 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.proxy;
+
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1.9
+ */
+public class ConfigProxyStatisticsTest {
+
+ @Test
+ public void basic() {
+ ConfigProxyStatistics statistics = new ConfigProxyStatistics();
+ assertThat(statistics.getEventInterval(), is(ConfigProxyStatistics.defaultEventInterval));
+ assertThat(statistics.processedRequests(), is(0L));
+ assertThat(statistics.errors(), is(0L));
+ assertThat(statistics.delayedResponses(), is(0L));
+
+ statistics.delayedResponses(1);
+ statistics.incProcessedRequests();
+ statistics.incRpcRequests();
+ statistics.incErrorCount();
+
+ assertThat(statistics.processedRequests(), is(1L));
+ assertThat(statistics.rpcRequests(), is(1L));
+ assertThat(statistics.errors(), is(1L));
+ assertThat(statistics.delayedResponses(), is(1L));
+
+ statistics.decDelayedResponses();
+ assertThat(statistics.delayedResponses(), is(0L));
+
+
+ Long eventInterval = 1L;
+ statistics = new ConfigProxyStatistics(eventInterval);
+ assertThat(statistics.getEventInterval(), is(eventInterval));
+
+ Thread t = new Thread(statistics);
+ t.start();
+
+ statistics.stop();
+ }
+}
diff --git a/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ConfigTester.java b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ConfigTester.java
new file mode 100644
index 00000000000..187a9f0726f
--- /dev/null
+++ b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ConfigTester.java
@@ -0,0 +1,49 @@
+// Copyright 2016 Yahoo Inc. 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.jrt.Request;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.RawConfig;
+import com.yahoo.vespa.config.protocol.CompressionType;
+import com.yahoo.vespa.config.protocol.DefContent;
+import com.yahoo.vespa.config.protocol.JRTClientConfigRequestV3;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequestV3;
+import com.yahoo.vespa.config.protocol.Trace;
+
+import java.util.Collections;
+import java.util.Optional;
+
+/**
+ * @author bratseth
+ */
+public class ConfigTester {
+
+ private final long defaultTimeout = 10000;
+
+ public JRTServerConfigRequest createRequest(RawConfig config) {
+ return createRequest(config, defaultTimeout);
+ }
+
+ public JRTServerConfigRequest createRequest(RawConfig config, long timeout) {
+ return createRequest(config.getName(), config.getConfigId(), config.getNamespace(),
+ config.getConfigMd5(), config.getDefMd5(), config.getGeneration(), timeout);
+ }
+
+ public JRTServerConfigRequest createRequest(String configName, String configId, String namespace) {
+ return createRequest(configName, configId, namespace, defaultTimeout);
+ }
+
+ public JRTServerConfigRequest createRequest(String configName, String configId, String namespace, long timeout) {
+ return createRequest(configName, configId, namespace, "", null, 0, timeout);
+ }
+
+ public JRTServerConfigRequest createRequest(String configName, String configId, String namespace, String md5, String defMd5, long generation, long timeout) {
+ Request request = JRTClientConfigRequestV3.
+ createWithParams(new ConfigKey<>(configName, configId, namespace, defMd5, null), DefContent.fromList(Collections.emptyList()),
+ "fromHost", md5, generation, timeout, Trace.createDummy(), CompressionType.UNCOMPRESSED,
+ Optional.empty()).getRequest();
+ return JRTServerConfigRequestV3.createFromRequest(request);
+ }
+
+}
diff --git a/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/DelayedResponseTest.java b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/DelayedResponseTest.java
new file mode 100644
index 00000000000..c81846d78d8
--- /dev/null
+++ b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/DelayedResponseTest.java
@@ -0,0 +1,83 @@
+// Copyright 2016 Yahoo Inc. 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.vespa.config.protocol.JRTServerConfigRequest;
+import org.junit.Test;
+
+import java.util.concurrent.Delayed;
+import java.util.concurrent.TimeUnit;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1.11
+ */
+public class DelayedResponseTest {
+
+ private static final String configId = "id";
+ private static final String namespace = "bar";
+
+ @Test
+ public void basic() {
+ ConfigTester tester = new ConfigTester();
+ final long returnTime = System.currentTimeMillis();
+ final long timeout = 1;
+ final String configName = "foo";
+ final JRTServerConfigRequest request = tester.createRequest(configName, configId, namespace, timeout);
+ DelayedResponse delayedResponse = new DelayedResponse(request, returnTime);
+ assertThat(delayedResponse.getRequest(), is(request));
+ assertThat(delayedResponse.getReturnTime(), is(returnTime));
+ assertTrue(delayedResponse.getDelay(TimeUnit.SECONDS) < returnTime);
+
+ DelayedResponse before = new DelayedResponse(request, returnTime - 1000L);
+ DelayedResponse after = new DelayedResponse(request, returnTime + 1000L);
+
+ assertThat(delayedResponse.compareTo(delayedResponse), is(0));
+ assertThat(delayedResponse.compareTo(before), is(1));
+ assertThat(delayedResponse.compareTo(after), is(-1));
+ assertThat(delayedResponse.compareTo(new Delayed() {
+ @Override
+ public long getDelay(TimeUnit unit) {
+ return 0;
+ }
+
+ @Override
+ public int compareTo(Delayed o) {
+ return 0;
+ }
+ }), is(0));
+ }
+
+ @Test
+ public void testDelayedResponse() {
+ ConfigTester tester = new ConfigTester();
+ final long timeout = 20000;
+ JRTServerConfigRequest request1 = tester.createRequest("baz", configId, namespace, timeout);
+ DelayedResponse delayed1 = new DelayedResponse(request1);
+ assertTrue(delayed1.getReturnTime() > System.currentTimeMillis());
+ assertTrue(delayed1.getDelay(TimeUnit.MILLISECONDS) > 0);
+ assertTrue(delayed1.getDelay(TimeUnit.MILLISECONDS) <= timeout);
+
+ // Just to make sure we do not get requests within the same millisecond
+ try {
+ Thread.sleep(1);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ // New request, should have larger delay than the first
+ JRTServerConfigRequest request2 = tester.createRequest("baz", configId, namespace, timeout);
+ DelayedResponse delayed2 = new DelayedResponse(request2);
+ assertTrue("delayed1=" + delayed1.getReturnTime() + ", delayed2=" +
+ delayed2.getReturnTime() + ": delayed2 should be greater than delayed1",
+ delayed2.getReturnTime() > delayed1.getReturnTime());
+
+ // Test compareTo() method
+ assertThat(delayed1.compareTo(delayed1), is(0));
+ assertThat(delayed1.compareTo(delayed2), is(-1));
+ assertThat(delayed2.compareTo(delayed1), is(1));
+ }
+
+}
diff --git a/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/DelayedResponsesTest.java b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/DelayedResponsesTest.java
new file mode 100644
index 00000000000..701ca959ee8
--- /dev/null
+++ b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/DelayedResponsesTest.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.proxy;
+
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1.9
+ */
+public class DelayedResponsesTest {
+
+ @Test
+ public void basic() throws InterruptedException {
+ ConfigTester tester = new ConfigTester();
+ DelayedResponses responses = new DelayedResponses(new ConfigProxyStatistics());
+ DelayedResponse delayedResponse = new DelayedResponse(tester.createRequest("foo", "id", "bar"));
+ responses.add(delayedResponse);
+
+ assertThat(responses.size(), is(1));
+ assertThat(responses.responses().take(), is(delayedResponse));
+ assertThat(responses.size(), is(0));
+
+ responses.add(delayedResponse);
+ assertThat(responses.size(), is(1));
+ responses.remove(delayedResponse);
+ assertThat(responses.size(), is(0));
+ }
+
+}
diff --git a/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/Helper.java b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/Helper.java
new file mode 100644
index 00000000000..dc2b667f823
--- /dev/null
+++ b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/Helper.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. 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.slime.Slime;
+import com.yahoo.vespa.config.*;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+import com.yahoo.vespa.config.protocol.Payload;
+import com.yahoo.vespa.config.util.ConfigUtils;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1.9
+ */
+public class Helper {
+
+ static final long serverTimeout = 100000;
+ static RawConfig fooConfig;
+ static RawConfig fooConfigV2;
+ static RawConfig barConfig;
+ static Payload fooConfigPayload;
+
+ static JRTServerConfigRequest fooConfigRequest;
+ static JRTServerConfigRequest barConfigRequest;
+
+ static ConfigCacheKey fooConfigConfigCacheKey;
+ static ConfigCacheKey barConfigConfigCacheKey;
+
+ static {
+ ConfigTester tester = new ConfigTester();
+ String defName = "foo";
+ String configId = "id";
+ String namespace = "bar";
+ ConfigKey<?> configKey = new ConfigKey<>(defName, configId, namespace);
+ Payload payloadV1 = Payload.from("bar \"value\"");
+ Slime slime = new Slime();
+ slime.setString("bar \"value\"");
+ fooConfigPayload = Payload.from(new ConfigPayload(slime));
+
+ List<String> defContent = Collections.singletonList("bar string");
+ long generation = 1;
+ String defMd5 = ConfigUtils.getDefMd5(defContent);
+ String configMd5 = "5752ad0f757d7e711e32037f29940b73";
+ fooConfig = new RawConfig(configKey, defMd5, payloadV1, configMd5, generation, defContent, Optional.empty());
+ fooConfigV2 = new RawConfig(configKey, defMd5, fooConfigPayload, configMd5, generation, defContent, Optional.empty());
+ fooConfigRequest = tester.createRequest(defName, configId, namespace, serverTimeout);
+ fooConfigConfigCacheKey = new ConfigCacheKey(fooConfig.getKey(), fooConfig.getDefMd5());
+
+ String defName2 = "bar";
+ barConfig = new RawConfig(new ConfigKey<>(defName2, configId, namespace), defMd5, payloadV1, configMd5, generation, defContent, Optional.empty());
+ barConfigRequest = tester.createRequest(defName2, configId, namespace, serverTimeout);
+ barConfigConfigCacheKey = new ConfigCacheKey(barConfig.getKey(), barConfig.getDefMd5());
+ }
+
+}
diff --git a/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/MemoryCacheConfigClientTest.java b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/MemoryCacheConfigClientTest.java
new file mode 100644
index 00000000000..b59d8710f36
--- /dev/null
+++ b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/MemoryCacheConfigClientTest.java
@@ -0,0 +1,29 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.proxy;
+
+import org.junit.Test;
+
+import java.util.Collections;
+
+import static junit.framework.TestCase.assertNull;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1.9
+ */
+public class MemoryCacheConfigClientTest {
+
+ @Test
+ public void basic() {
+ MemoryCache cache = new MemoryCache();
+ cache.put(Helper.fooConfig);
+ MemoryCacheConfigClient client = new MemoryCacheConfigClient(cache);
+ assertThat(client.getConfig(Helper.fooConfig, null), is(Helper.fooConfig));
+ assertNull(client.getConfig(Helper.barConfig, null));
+
+ assertThat(client.getActiveSourceConnection(), is("N/A"));
+ assertThat(client.getSourceConnections(), is(Collections.singletonList("N/A")));
+ }
+}
diff --git a/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/MemoryCacheTest.java b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/MemoryCacheTest.java
new file mode 100644
index 00000000000..3707217b9c6
--- /dev/null
+++ b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/MemoryCacheTest.java
@@ -0,0 +1,65 @@
+// Copyright 2016 Yahoo Inc. 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.vespa.config.RawConfig;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.*;
+
+/**
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1.9
+ */
+public class MemoryCacheTest extends CacheTest {
+
+ @Test
+ public void basic() {
+ MemoryCache cache = new MemoryCache();
+
+ cache.put(config);
+ cache.put(config2);
+ assertThat(cache.size(), is(2));
+ assertTrue(cache.containsKey(cacheKey));
+ assertTrue(cache.containsKey(cacheKey2));
+
+ RawConfig response = cache.get(cacheKey);
+ assertNotNull(response);
+ assertThat(response.getName(), is(defName));
+ assertThat(response.getPayload().toString(), is(payload.toString()));
+ assertThat(response.getGeneration(), is(generation));
+
+ response = cache.get(cacheKey2);
+ assertNotNull(response);
+ assertThat(response.getName(), is(defName2));
+ assertThat(response.getPayload().toString(), is(payload2.toString()));
+ assertThat(response.getGeneration(), is(generation));
+
+ cache.clear();
+ }
+
+ @Test
+ public void testSameConfigNameDifferentMd5() {
+ MemoryCache cache = new MemoryCache();
+
+ cache.put(config);
+ cache.put(configDifferentMd5); // same name, different defMd5
+ assertThat(cache.size(), is(2));
+ assertTrue(cache.containsKey(cacheKey));
+
+ RawConfig response = cache.get(cacheKey);
+ assertNotNull(response);
+ assertThat(response.getName(), is(defName));
+ assertThat(response.getPayload().getData(), is(payload.getData()));
+ assertThat(response.getGeneration(), is(generation));
+
+ response = cache.get(cacheKeyDifferentMd5);
+ assertNotNull(response);
+ assertThat(response.getName(), is(defName));
+ assertThat(response.getPayload().getData(), is(payloadDifferentMd5.getData()));
+ assertThat(response.getGeneration(), is(generation));
+
+ cache.clear();
+ assertThat(cache.size(), is(0));
+ }
+}
diff --git a/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/MockConnection.java b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/MockConnection.java
new file mode 100644
index 00000000000..15f00cea18a
--- /dev/null
+++ b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/MockConnection.java
@@ -0,0 +1,86 @@
+// Copyright 2016 Yahoo Inc. 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.jrt.Request;
+import com.yahoo.jrt.RequestWaiter;
+import com.yahoo.vespa.config.RawConfig;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequestV3;
+import com.yahoo.vespa.config.protocol.Payload;
+import com.yahoo.vespa.config.util.ConfigUtils;
+
+/**
+ * For unit testing
+ *
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1.11
+ */
+public class MockConnection extends com.yahoo.config.subscription.impl.MockConnection {
+
+ public MockConnection(MapBackedConfigSource configSource) {
+ this(new ProxyResponseHandler(configSource));
+ }
+
+ public MockConnection(ResponseHandler responseHandler) {
+ super(responseHandler);
+ }
+
+ static class ProxyResponseHandler implements ResponseHandler {
+ private RequestWaiter requestWaiter;
+ private Request request;
+ private final MapBackedConfigSource configSource;
+
+ protected ProxyResponseHandler(MapBackedConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ @Override
+ public RequestWaiter requestWaiter() {
+ return requestWaiter;
+ }
+
+ @Override
+ public Request request() {
+ return request;
+ }
+
+ @Override
+ public ResponseHandler requestWaiter(RequestWaiter requestWaiter) {
+ this.requestWaiter = requestWaiter;
+ return this;
+ }
+
+ @Override
+ public ResponseHandler request(Request request) {
+ this.request = request;
+ return this;
+ }
+
+ @Override
+ public void run() {
+ if (request.isError()) {
+ System.out.println("Returning error response");
+ createErrorResponse();
+ } else {
+ System.out.println("Returning OK response");
+ createOkResponse();
+ }
+ requestWaiter.handleRequestDone(request);
+ }
+
+ protected void createOkResponse() {
+ JRTServerConfigRequestV3 jrtReq = JRTServerConfigRequestV3.createFromRequest(request);
+ long generation = 1;
+ RawConfig config = configSource.getConfig(jrtReq.getConfigKey());
+ if (config == null || config.getPayload() == null) {
+ throw new RuntimeException("No config for " + jrtReq.getConfigKey() + " found");
+ }
+ Payload payload = config.getPayload();
+ jrtReq.addOkResponse(payload, generation, ConfigUtils.getMd5(payload.getData()));
+ }
+
+ protected void createErrorResponse() {
+ JRTServerConfigRequestV3 jrtReq = JRTServerConfigRequestV3.createFromRequest(request);
+ jrtReq.addErrorResponse(request.errorCode(), request.errorMessage());
+ }
+ }
+}
diff --git a/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/MockRpcServer.java b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/MockRpcServer.java
new file mode 100644
index 00000000000..ed41fbf0e26
--- /dev/null
+++ b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/MockRpcServer.java
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. 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.vespa.config.RawConfig;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+
+/**
+ * @author musum
+ */
+public class MockRpcServer implements RpcServer {
+
+ volatile long responses = 0;
+ volatile long errorResponses = 0;
+
+ public void returnOkResponse(JRTServerConfigRequest request, RawConfig config) {
+ responses++;
+ }
+
+ public void returnErrorResponse(JRTServerConfigRequest request, int errorCode, String message) {
+ responses++;
+ errorResponses++;
+ }
+}
diff --git a/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ModeTest.java b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ModeTest.java
new file mode 100644
index 00000000000..c068a531a80
--- /dev/null
+++ b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ModeTest.java
@@ -0,0 +1,61 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.proxy;
+
+import org.junit.Test;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author musum
+ */
+public class ModeTest {
+
+ @Test
+ public void basic() {
+ Mode mode = new Mode();
+ assertModeName(Mode.ModeName.DEFAULT, mode);
+ assertTrue(mode.isDefault());
+
+ mode = new Mode("");
+ assertModeName(Mode.ModeName.DEFAULT, mode);
+ assertTrue(mode.isDefault());
+
+ mode = new Mode(Mode.ModeName.DEFAULT.name());
+ assertModeName(Mode.ModeName.DEFAULT, mode);
+ assertTrue(mode.isDefault());
+
+ mode = new Mode(Mode.ModeName.MEMORYCACHE.name());
+ assertModeName(Mode.ModeName.MEMORYCACHE, mode);
+ assertTrue(mode.isMemoryCache());
+
+ assertTrue(new Mode(Mode.ModeName.DEFAULT.name()).requiresConfigSource());
+
+ assertFalse(new Mode(Mode.ModeName.MEMORYCACHE.name()).requiresConfigSource());
+
+ Set<String> modes = new HashSet<>();
+ for (Mode.ModeName modeName : Mode.ModeName.values()) {
+ modes.add(modeName.name().toLowerCase());
+ }
+
+ assertThat(Mode.modes(), is(modes));
+
+ assertFalse(Mode.validModeName("foo"));
+
+ assertThat(mode.toString(), is(Mode.ModeName.MEMORYCACHE.name().toLowerCase()));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void failWhenInvalidMode() {
+ new Mode("invalid_mode");
+ }
+
+ private void assertModeName(Mode.ModeName expected, Mode actual) {
+ assertThat(actual.name(), is(expected.name().toLowerCase()));
+ }
+}
diff --git a/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ProxyServerTest.java b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ProxyServerTest.java
new file mode 100644
index 00000000000..3ed876d547f
--- /dev/null
+++ b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/ProxyServerTest.java
@@ -0,0 +1,184 @@
+// Copyright 2016 Yahoo Inc. 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.config.subscription.ConfigSourceSet;
+import com.yahoo.config.subscription.RawSource;
+import com.yahoo.vespa.config.*;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.util.Optional;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.*;
+
+/**
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1.9
+ */
+public class ProxyServerTest {
+
+ private final MapBackedConfigSource source = new MapBackedConfigSource(UpstreamConfigSubscriberTest.MockClientUpdater.create());
+ private ProxyServer proxy = ProxyServer.createTestServer(source);
+
+ static final RawConfig fooConfig = Helper.fooConfigV2;
+
+ // errorConfig based on fooConfig
+ static final ConfigKey<?> errorConfigKey = new ConfigKey<>("error", fooConfig.getConfigId(), fooConfig.getNamespace());
+ static final RawConfig errorConfig = new RawConfig(errorConfigKey, fooConfig.getDefMd5(),
+ fooConfig.getPayload(), fooConfig.getConfigMd5(),
+ fooConfig.getGeneration(), ErrorCode.UNKNOWN_DEFINITION, fooConfig.getDefContent(), Optional.empty());
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Before
+ public void setup() {
+ source.clear();
+ source.put(fooConfig.getKey(), createConfigWithNextConfigGeneration(fooConfig, 0));
+ source.put(errorConfigKey, createConfigWithNextConfigGeneration(fooConfig, ErrorCode.UNKNOWN_DEFINITION));
+ proxy = ProxyServer.createTestServer(source);
+ }
+
+ @After
+ public void shutdown() {
+ proxy.stop();
+ }
+
+ @Test
+ public void basic() {
+ assertTrue(proxy.getMode().isDefault());
+ assertThat(proxy.getCacheManager().getMemoryCache().size(), is(0));
+ assertThat(proxy.getTimingValues(), is(ProxyServer.defaultTimingValues()));
+ }
+
+ /**
+ * Tests that the proxy server RPC commands for setting and getting mode works..
+ */
+ @Test
+ public void testModeSwitch() {
+ ConfigSourceSet source = new ConfigSourceSet(); // Need to use a ConfigSourceSet to test modes
+ ProxyServer proxy = ProxyServer.createTestServer(source);
+ assertTrue(proxy.getMode().isDefault());
+
+ for (String mode : Mode.modes()) {
+ proxy.setMode(mode);
+ assertThat(proxy.getMode().name(), is(mode));
+ }
+
+ // Also switch to DEFAULT mode, as that is not covered above
+ proxy.setMode("default");
+ assertTrue(proxy.getMode().isDefault());
+
+ // Set mode to the same as the current mode
+ proxy.setMode(proxy.getMode().name());
+ assertTrue(proxy.getMode().isDefault());
+
+ proxy.stop();
+ }
+
+ /**
+ * Tests that the proxy server can be tested with a MapBackedConfigSource,
+ * which is a simple hash map with configs
+ */
+ @Test
+ public void testRawConfigSetBasics() {
+ ConfigTester tester = new ConfigTester();
+ JRTServerConfigRequest errorConfigRequest = tester.createRequest(errorConfig);
+
+ assertTrue(proxy.getMode().isDefault());
+ RawConfig config = proxy.resolveConfig(Helper.fooConfigRequest);
+ assertThat(config, is(createConfigWithNextConfigGeneration(Helper.fooConfig, 0)));
+
+ config = proxy.resolveConfig(Helper.barConfigRequest);
+ assertNull(config);
+
+ config = proxy.resolveConfig(errorConfigRequest);
+ assertThat(config.errorCode(), is(ErrorCode.UNKNOWN_DEFINITION));
+ }
+
+ /**
+ * Verifies that config is retrieved from the real server when it is not found in the cache,
+ * that the cache is populated with the config and that the entry in the cache is used
+ * when it is found there.
+ */
+ @Test
+ public void testGetConfigAndCaching() {
+ ConfigTester tester = new ConfigTester();
+ final CacheManager cacheManager = proxy.getCacheManager();
+ assertEquals(0, cacheManager.getCacheSize());
+ RawConfig res = proxy.resolveConfig(tester.createRequest(fooConfig));
+ assertNotNull(res);
+ assertThat(res.getPayload().toString(), is(Helper.fooConfigPayload.toString()));
+ assertEquals(1, cacheManager.getCacheSize());
+ assertThat(cacheManager.getMemoryCache().get(new ConfigCacheKey(fooConfig.getKey(), fooConfig.getDefMd5())), is(res));
+
+ // Trying same config again
+ JRTServerConfigRequest newRequestBasedOnResponse = tester.createRequest(res);
+ RawConfig res2 = proxy.resolveConfig(newRequestBasedOnResponse);
+ assertFalse(ProxyServer.configOrGenerationHasChanged(res2, newRequestBasedOnResponse));
+ assertEquals(1, cacheManager.getCacheSize());
+ }
+
+ /**
+ * Verifies that error responses are not cached. When the config has been successfully retrieved,
+ * it must be put in the cache.
+ */
+ @Test
+ public void testNoCachingOfErrorRequests() {
+ ConfigTester tester = new ConfigTester();
+ // Simulate an error response
+ source.put(fooConfig.getKey(), createConfigWithNextConfigGeneration(fooConfig, ErrorCode.INTERNAL_ERROR));
+
+ final CacheManager cacheManager = proxy.getCacheManager();
+ assertEquals(0, cacheManager.getCacheSize());
+
+ RawConfig res = proxy.resolveConfig(tester.createRequest(fooConfig));
+ assertNotNull(res);
+ assertNotNull(res.getPayload());
+ assertTrue(res.isError());
+ assertEquals(0, cacheManager.getCacheSize());
+
+ // Put a version of the same config into backend without error and see that it now works (i.e. we are
+ // not getting a cached response (of the error in the previous request)
+ source.put(fooConfig.getKey(), createConfigWithNextConfigGeneration(fooConfig, 0));
+
+ // Verify that we get the config now and that it is cached
+ res = proxy.resolveConfig(tester.createRequest(fooConfig));
+ assertNotNull(res);
+ assertNotNull(res.getPayload().getData());
+ assertThat(res.getPayload().toString(), is(Helper.fooConfigPayload.toString()));
+ assertEquals(1, cacheManager.getCacheSize());
+
+ JRTServerConfigRequest newRequestBasedOnResponse = tester.createRequest(res);
+ RawConfig res2 = proxy.resolveConfig(newRequestBasedOnResponse);
+ assertFalse(ProxyServer.configOrGenerationHasChanged(res2, newRequestBasedOnResponse));
+ assertEquals(1, cacheManager.getCacheSize());
+ }
+
+ @Test
+ public void testReadingSystemProperties() {
+ ProxyServer.Properties properties = ProxyServer.getSystemProperties();
+ assertThat(properties.eventInterval, is(ConfigProxyStatistics.defaultEventInterval));
+ assertThat(properties.mode, is(Mode.ModeName.DEFAULT.name()));
+ assertThat(properties.configSources.length, is(1));
+ assertThat(properties.configSources[0], is(ProxyServer.DEFAULT_PROXY_CONFIG_SOURCES));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testIllegalConfigSource() {
+ RawSource source = new RawSource("bar 1");
+ proxy = ProxyServer.createTestServer(source);
+ }
+
+ static RawConfig createConfigWithNextConfigGeneration(RawConfig config, int errorCode) {
+ return new RawConfig(config.getKey(), config.getDefMd5(),
+ config.getPayload(), config.getConfigMd5(),
+ config.getGeneration() + 1, errorCode, config.getDefContent(), Optional.empty());
+ }
+
+}
diff --git a/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/UpstreamConfigSubscriberTest.java b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/UpstreamConfigSubscriberTest.java
new file mode 100644
index 00000000000..695c4068155
--- /dev/null
+++ b/config-proxy/src/test/java/com/yahoo/vespa/config/proxy/UpstreamConfigSubscriberTest.java
@@ -0,0 +1,173 @@
+// Copyright 2016 Yahoo Inc. 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.config.subscription.ConfigSourceSet;
+import com.yahoo.config.subscription.impl.JRTConfigRequester;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.*;
+import com.yahoo.vespa.config.protocol.*;
+import com.yahoo.vespa.config.util.ConfigUtils;
+import org.junit.*;
+import org.junit.rules.TemporaryFolder;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import static junit.framework.TestCase.assertNotNull;
+import static junit.framework.TestCase.fail;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author musum
+ */
+public class UpstreamConfigSubscriberTest {
+ private final ConfigSourceSet sourceSet = new ConfigSourceSet("tcp/foo:78");
+ private final TimingValues timingValues = ProxyServer.defaultTimingValues();
+
+ private MapBackedConfigSource sourceResponses;
+ private MockClientUpdater clientUpdater;
+ private MockConnection mockConnection;
+ static RawConfig fooConfig;
+ static RawConfig errorConfig;
+ static ConfigKey<?> errorConfigKey;
+ static Payload fooPayload;
+ static Payload errorPayload;
+ long generation = 1;
+
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Before
+ public void setup() {
+ clientUpdater = MockClientUpdater.create();
+ sourceResponses = new MapBackedConfigSource(clientUpdater);
+
+ ConfigPayload payload = getConfigPayload("bar", "value");
+ fooPayload = Payload.from(payload);
+ fooConfig = new RawConfig(Helper.fooConfig.getKey(), Helper.fooConfig.getDefMd5(), fooPayload, ConfigUtils.getMd5(payload), generation, 0, Helper.fooConfig.getDefContent(), Optional.empty());
+
+ payload = new ConfigPayload(new Slime());
+ errorPayload = Payload.from(payload);
+ errorConfigKey = new ConfigKey<>("error", fooConfig.getConfigId(), fooConfig.getNamespace());
+ errorConfig = new RawConfig(errorConfigKey, fooConfig.getDefMd5(), errorPayload, ConfigUtils.getMd5(payload), generation, ErrorCode.UNKNOWN_DEFINITION, fooConfig.getDefContent(), Optional.empty());
+
+ sourceResponses.clear();
+ sourceResponses.put(fooConfig.getKey(), fooConfig);
+
+ mockConnection = new MockConnection(sourceResponses);
+ }
+
+ private ConfigPayload getConfigPayload(String key, String value) {
+ Slime slime = new Slime();
+ slime.setObject().setString(key, value);
+ return new ConfigPayload(slime);
+ }
+
+ @Test
+ public void basic() {
+ final UpstreamConfigSubscriber subscriber = createUpstreamConfigSubscriber(fooConfig);
+ new Thread(subscriber).start();
+ waitForConfigGeneration(clientUpdater, generation);
+ assertThat(clientUpdater.getLastConfig(), is(fooConfig));
+ subscriber.cancel();
+ }
+
+ @Test
+ public void require_that_reconfiguration_works() {
+ final UpstreamConfigSubscriber subscriber = createUpstreamConfigSubscriber(fooConfig);
+
+ new Thread(subscriber).start();
+ waitForConfigGeneration(clientUpdater, generation);
+ assertThat(clientUpdater.getLastConfig(), is(fooConfig));
+
+ // Add updated config
+ generation++;
+ final ConfigPayload payload = getConfigPayload("bar", "value2");
+ fooPayload = Payload.from(payload);
+ RawConfig fooConfig2 = new RawConfig(fooConfig.getKey(), fooConfig.getDefMd5(), fooPayload, ConfigUtils.getMd5(payload), generation, fooConfig.getDefContent(), Optional.empty());
+ sourceResponses.put(fooConfig2.getKey(), fooConfig2);
+
+ waitForConfigGeneration(clientUpdater, generation);
+ assertThat(clientUpdater.getLastConfig(), is(not(fooConfig)));
+ subscriber.cancel();
+ }
+
+ @Test
+ public void require_that_error_response_is_handled() {
+ sourceResponses.put(errorConfigKey, errorConfig);
+ final UpstreamConfigSubscriber subscriber = createUpstreamConfigSubscriber(fooConfig);
+
+ new Thread(subscriber).start();
+ waitForConfigGeneration(clientUpdater, generation);
+ RawConfig lastConfig = clientUpdater.getLastConfig();
+ assertThat(lastConfig, is(errorConfig));
+ assertThat(lastConfig.errorCode(), is(ErrorCode.UNKNOWN_DEFINITION));
+ subscriber.cancel();
+ }
+
+ private UpstreamConfigSubscriber createUpstreamConfigSubscriber(RawConfig config) {
+ return new UpstreamConfigSubscriber(config, clientUpdater, sourceSet, timingValues, createRequesterPool());
+ }
+
+ private Map<ConfigSourceSet, JRTConfigRequester> createRequesterPool() {
+ JRTConfigRequester request = JRTConfigRequester.get(mockConnection, timingValues);
+
+ Map<ConfigSourceSet, JRTConfigRequester> requesterPool = new LinkedHashMap<>();
+ requesterPool.put(sourceSet, request);
+ return requesterPool;
+ }
+
+ private void waitForConfigGeneration(MockClientUpdater clientUpdater, long expectedGeneration) {
+ int i = 0;
+ RawConfig lastConfig;
+ do {
+ lastConfig = clientUpdater.getLastConfig();
+ if (lastConfig != null) {
+ System.out.println("i=" + i + ", config=" + lastConfig + ",generation=" + lastConfig.getGeneration());
+ }
+ if (lastConfig != null && lastConfig.getGeneration() == expectedGeneration) {
+ break;
+ } else {
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ }
+ i++;
+ } while (i < 1000);
+ assertNotNull(lastConfig);
+ assertThat(lastConfig.getGeneration(), is(expectedGeneration));
+ }
+
+ static class MockClientUpdater extends ClientUpdater {
+ private RawConfig lastConfig;
+
+ private MockClientUpdater(ConfigProxyStatistics statistics, Mode mode) {
+ super(CacheManager.createTestCacheManager(),
+ new MockRpcServer(),
+ statistics,
+ new DelayedResponses(statistics),
+ mode);
+ }
+
+ public static MockClientUpdater create() {
+ Mode mode = new Mode();
+ ConfigProxyStatistics statistics = new ConfigProxyStatistics();
+ return new MockClientUpdater(statistics, mode);
+ }
+
+ @Override
+ public void updateSubscribers(RawConfig newConfig) {
+ lastConfig = newConfig;
+ }
+
+ RawConfig getLastConfig() {
+ return lastConfig;
+ }
+ }
+}