aboutsummaryrefslogtreecommitdiffstats
path: root/configserver/src/main/java/com
diff options
context:
space:
mode:
Diffstat (limited to 'configserver/src/main/java/com')
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ActivateLock.java39
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationMapper.java77
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationSet.java81
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/CompressedApplicationInputStream.java128
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactory.java23
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactoryFactory.java24
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerBootstrap.java57
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerDB.java93
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerSpec.java73
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/DelayedConfigResponses.java224
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/DeployHandlerLogger.java48
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigContext.java57
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigProcessor.java176
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/GlobalComponentRegistry.java41
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistries.java32
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistry.java95
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/HostValidator.java17
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistry.java109
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/LZ4ConfigResponseFactory.java32
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/PathProvider.java46
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ReloadHandler.java26
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ReloadListener.java51
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/RequestHandler.java81
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/RotationsCache.java69
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/RpcServer.java407
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ServerCache.java83
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/StaticConfigDefinitionRepo.java43
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelController.java161
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelGenerationCounter.java40
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelRequestHandler.java83
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/Tenant.java182
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/TenantBuilder.java201
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/TenantDebugger.java38
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/TenantHandlerProvider.java15
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/TenantListener.java32
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/TenantRequestHandler.java213
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/Tenants.java358
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/TimeoutBudget.java59
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/UncompressedConfigResponseFactory.java27
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/UnknownConfigDefinitionException.java14
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/VersionDoesNotExistException.java11
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/Application.java243
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationConvergenceChecker.java262
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationRepo.java53
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/ConfigNotConvergedException.java11
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/LogServerLogGrabber.java120
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/PermanentApplicationPackage.java45
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/ZKApplicationRepo.java172
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActions.java39
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsSlimeConverter.java70
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActions.java91
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActionsFormatter.java33
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActions.java95
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActionsFormatter.java30
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/counter/package-info.java8
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployer.java80
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployment.java202
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java160
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/deploy/TenantFileSystemDirs.java47
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClient.java378
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperDeployer.java45
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBHandler.java47
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBRegistry.java54
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionLock.java93
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionProvider.java55
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDBHandler.java39
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDistributionProvider.java19
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/BadRequestException.java15
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentHandler.java108
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentRequest.java95
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigRequest.java197
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigResponse.java42
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpErrorResponse.java81
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpGetConfigHandler.java49
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpHandler.java131
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListConfigsHandler.java128
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListNamedConfigsHandler.java60
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/InternalServerException.java21
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/InvalidApplicationException.java16
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/JSONResponse.java35
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/NotFoundException.java16
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionActiveHandlerBase.java55
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentListResponse.java34
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentReadResponse.java39
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusListResponse.java41
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusResponse.java54
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreate.java115
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreateResponse.java15
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionHandler.java112
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionResponse.java46
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/UnknownVespaVersionException.java17
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/Utils.java72
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentRequest.java51
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java256
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HostHandler.java78
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpConfigRequests.java54
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpGetConfigHandler.java49
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsHandler.java180
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsRequest.java156
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListNamedConfigsHandler.java49
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsHandler.java67
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsResponse.java39
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsHandler.java36
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsResponse.java40
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandler.java59
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveResponse.java27
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandler.java70
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentRequestV2.java43
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java93
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateResponseV2.java37
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandler.java114
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareResponse.java29
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantCreateResponse.java26
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantDeleteResponse.java25
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantGetResponse.java25
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantHandler.java96
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantRequest.java15
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/model/ElkProducer.java51
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/model/LbServicesProducer.java117
-rwxr-xr-xconfigserver/src/main/java/com/yahoo/vespa/config/server/model/RoutingProducer.java36
-rwxr-xr-xconfigserver/src/main/java/com/yahoo/vespa/config/server/model/SuperModel.java93
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java126
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelFactoryRegistry.java58
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelResult.java13
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelsBuilder.java132
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/PreparedModelsBuilder.java224
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdater.java222
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdaterFactory.java14
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/Metrics.java140
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/provision/HostProvisionerProvider.java62
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/provision/ProvisionerAdapter.java39
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/provision/StaticProvisioner.java44
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/StatusResource.java57
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/package-info.java5
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/StatusInformation.java82
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/package-info.java5
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/FileDistributionFactory.java40
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSession.java170
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionLoader.java14
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionRepo.java121
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java165
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSession.java97
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionFactory.java44
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionRepo.java226
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/ServerCacheLoader.java82
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java65
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionContext.java60
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactory.java38
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactoryImpl.java170
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java283
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepo.java78
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionStateWatcher.java82
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java213
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SilentDeployLogger.java25
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/version/VersionState.java61
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ConfigCurator.java429
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/InitializedCounter.java88
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/SessionCounter.java27
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationFile.java175
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationPackage.java281
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKLiveApp.java208
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/package-info.java8
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/serviceview/Cluster.java85
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/serviceview/ConfigServerLocation.java25
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/serviceview/ProxyErrorMapper.java24
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/serviceview/Service.java172
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/serviceview/ServiceModel.java236
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/serviceview/StateResource.java282
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/serviceview/package-info.java13
169 files changed, 15072 insertions, 0 deletions
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ActivateLock.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ActivateLock.java
new file mode 100644
index 00000000000..bcc920614ec
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ActivateLock.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.server;
+
+import com.yahoo.path.Path;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.curator.recipes.CuratorLock;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A lock to protect session activation.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ActivateLock {
+ private static final String ACTIVATE_LOCK_NAME = "activateLock";
+ private final CuratorLock curatorLock;
+
+ public ActivateLock(Curator curator, Path rootPath) {
+ this.curatorLock = new CuratorLock(curator, rootPath.append(ACTIVATE_LOCK_NAME).getAbsolute());
+ }
+
+ public synchronized void acquire(TimeoutBudget timeoutBudget, boolean ignoreLockError) {
+ try {
+ curatorLock.tryLock(timeoutBudget.timeLeft().toMillis(), TimeUnit.MILLISECONDS);
+ } catch (Exception e) {
+ if (!ignoreLockError) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ public synchronized void release() {
+ if (curatorLock.hasLock()) {
+ curatorLock.unlock();
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationMapper.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationMapper.java
new file mode 100644
index 00000000000..6ff0fa49593
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationMapper.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.server;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Version;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.vespa.config.server.http.NotFoundException;
+
+/**
+ * Used during config request handling to route to the right config model
+ * based on application id and version.
+ * @author Vegard Sjonfjell
+ */
+public final class ApplicationMapper {
+
+ private final Map<ApplicationId, ApplicationSet> requestHandlers = new ConcurrentHashMap<>();
+
+ private ApplicationSet getApplicationSet(ApplicationId applicationId) {
+ ApplicationSet list = requestHandlers.get(applicationId);
+ if (list != null) {
+ return list;
+ }
+
+ throw new NotFoundException("No such application id: " + applicationId);
+ }
+
+ /**
+ * Register a Application to an application id and specific vespa version
+ */
+ public void register(ApplicationId applicationId, ApplicationSet applicationSet) {
+ requestHandlers.put(applicationId, applicationSet);
+ }
+
+ /**
+ * Remove all applications associated with this application id
+ */
+ public void remove(ApplicationId applicationId) {
+ requestHandlers.remove(applicationId);
+ }
+
+ /**
+ * Retrieve the Application corresponding to this application id and specific vespa version.
+ *
+ * @return the matching application, or null if none matches
+ */
+ public Application getForVersion(ApplicationId applicationId, Optional<Version> vespaVersion) throws VersionDoesNotExistException {
+ return getApplicationSet(applicationId).getForVersionOrLatest(vespaVersion);
+ }
+
+ /** Returns whether this registry has an application for the given application id */
+ public boolean hasApplication(ApplicationId applicationId) {
+ return hasApplicationForVersion(applicationId, Optional.<Version>empty());
+ }
+
+ /** Returns whether this registry has an application for the given application id and vespa version */
+ public boolean hasApplicationForVersion(ApplicationId applicationId, Optional<Version> vespaVersion) {
+ try {
+ return getForVersion(applicationId, vespaVersion) != null;
+ }
+ catch (VersionDoesNotExistException | NotFoundException ex) {
+ return false;
+ }
+ }
+
+ /**
+ * Get the number of applications registered
+ */
+ public int numApplications() {
+ return requestHandlers.size();
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationSet.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationSet.java
new file mode 100644
index 00000000000..dd41ef2d489
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationSet.java
@@ -0,0 +1,81 @@
+// 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.server;
+import com.yahoo.config.model.api.HostInfo;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Version;
+import com.yahoo.vespa.config.server.application.Application;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * Immutable set of {@link Application}s with the same {@link ApplicationId}. With methods for getting defaults.
+ *
+ * @author vegard
+ */
+public final class ApplicationSet {
+
+ private final Version latestVersion;
+ // TODO: Should not need these as part of application?
+ private final ApplicationId applicationId;
+ private final long generation;
+ private final HashMap<Version, Application> applications = new HashMap<>();
+
+ private ApplicationSet(List<Application> applicationList) {
+ applicationId = applicationList.get(0).getId();
+ generation = applicationList.get(0).getApplicationGeneration();
+ for (Application application : applicationList) {
+ applications.put(application.getVespaVersion(), application);
+ if (!application.getId().equals(applicationId)) {
+ throw new IllegalArgumentException("Trying to create set with different application ids");
+ }
+ }
+ latestVersion = applications.keySet().stream().max((a, b) -> a.compareTo(b)).get();
+ }
+
+ public Application getForVersionOrLatest(Optional<Version> optionalVersion) {
+ return resolveForVersion(optionalVersion.orElse(latestVersion));
+ }
+
+ private Application resolveForVersion(Version vespaVersion) {
+ Application application = applications.get(vespaVersion);
+ if (application != null)
+ return application;
+
+ // Does the latest version specify we can use it regardless?
+ Application latest = applications.get(latestVersion);
+ if (latest.getModel().allowModelVersionMismatch())
+ return latest;
+
+ throw new VersionDoesNotExistException(String.format("No application with vespa version %s exists", vespaVersion.toString()));
+ }
+
+ public ApplicationId getId() {
+ return applicationId;
+ }
+
+ public static ApplicationSet fromList(List<Application> applications) {
+ return new ApplicationSet(applications);
+ }
+
+ public static ApplicationSet fromSingle(Application application) {
+ return fromList(Arrays.asList(application));
+ }
+
+ public Collection<String> getAllHosts() {
+ return applications.values().stream()
+ .flatMap(app -> app.getModel().getHosts().stream()
+ .map(HostInfo::getHostname))
+ .collect(Collectors.toList());
+ }
+
+ public void updateHostMetrics() {
+ for (Application application : applications.values()) {
+ application.updateHostMetrics(application.getModel().getHosts().size());
+ }
+ }
+
+ public long getApplicationGeneration() {
+ return generation;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/CompressedApplicationInputStream.java b/configserver/src/main/java/com/yahoo/vespa/config/server/CompressedApplicationInputStream.java
new file mode 100644
index 00000000000..b8da2448c5b
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/CompressedApplicationInputStream.java
@@ -0,0 +1,128 @@
+// 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.server;
+
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Files;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.server.http.BadRequestException;
+import com.yahoo.vespa.config.server.http.InternalServerException;
+import com.yahoo.vespa.config.server.http.SessionCreate;
+import org.apache.commons.compress.archivers.ArchiveEntry;
+import org.apache.commons.compress.archivers.ArchiveInputStream;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
+
+import java.io.*;
+import java.util.logging.Logger;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * A compressed application points to an application package that can be decompressed.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class CompressedApplicationInputStream implements AutoCloseable {
+ private static final Logger log = Logger.getLogger(CompressedApplicationInputStream.class.getPackage().getName());
+ private final ArchiveInputStream ais;
+
+ /**
+ * Create an instance of a compressed application from an input stream.
+ *
+ * @param is the input stream containing the compressed files.
+ * @param contentType the content type for determining what kind of compressed stream should be used.
+ * @return An instance of an unpacked application.
+ */
+ public static CompressedApplicationInputStream createFromCompressedStream(InputStream is, String contentType) {
+ try {
+ ArchiveInputStream ais = getArchiveInputStream(is, contentType);
+ return createFromCompressedStream(ais);
+ } catch (IOException e) {
+ throw new InternalServerException("Unable to create compressed application stream", e);
+ }
+ }
+
+ public static CompressedApplicationInputStream createFromCompressedStream(ArchiveInputStream ais) {
+ return new CompressedApplicationInputStream(ais);
+ }
+
+ private static ArchiveInputStream getArchiveInputStream(InputStream is, String contentTypeHeader) throws IOException {
+ ArchiveInputStream ais;
+ switch (contentTypeHeader) {
+ case SessionCreate.APPLICATION_X_GZIP:
+ ais = new TarArchiveInputStream(new GZIPInputStream(is));
+ break;
+ case SessionCreate.APPLICATION_ZIP:
+ ais = new ZipArchiveInputStream(is);
+ break;
+ default:
+ throw new BadRequestException("Unable to decompress");
+ }
+ return ais;
+ }
+
+ private CompressedApplicationInputStream(ArchiveInputStream ais) {
+ this.ais = ais;
+ }
+
+ /**
+ * Close this stream.
+ * @throws IOException if the stream could not be closed
+ */
+ public void close() throws IOException {
+ ais.close();
+ }
+
+ File decompress() throws IOException {
+ return decompress(Files.createTempDir());
+ }
+
+ public File decompress(File dir) throws IOException {
+ decompressInto(dir);
+ dir = findActualApplicationDir(dir);
+ return dir;
+ }
+
+ private void decompressInto(File application) throws IOException {
+ log.log(LogLevel.DEBUG, "Application is in " + application.getAbsolutePath());
+ int entries = 0;
+ ArchiveEntry entry;
+ while ((entry = ais.getNextEntry()) != null) {
+ log.log(LogLevel.DEBUG, "Unpacking " + entry.getName());
+ File outFile = new File(application, entry.getName());
+ // FIXME/TODO: write more tests that break this logic. I have a feeling it is not very robust.
+ if (entry.isDirectory()) {
+ if (!(outFile.exists() && outFile.isDirectory())) {
+ log.log(LogLevel.DEBUG, "Creating dir: " + outFile.getAbsolutePath());
+ boolean res = outFile.mkdirs();
+ if (!res) {
+ log.log(LogLevel.WARNING, "Could not create dir " + entry.getName());
+ }
+ }
+ } else {
+ log.log(LogLevel.DEBUG, "Creating output file: " + outFile.getAbsolutePath());
+
+ // Create parent dir if necessary
+ String parent = outFile.getParent();
+ new File(parent).mkdirs();
+
+ FileOutputStream fos = new FileOutputStream(outFile);
+ ByteStreams.copy(ais, fos);
+ fos.close();
+ }
+ entries++;
+ }
+ if (entries == 0) {
+ log.log(LogLevel.WARNING, "Not able to read any entries from " + application.getName());
+ }
+ }
+
+ private File findActualApplicationDir(File application) {
+ // If application is in e.g. application/, use that as root for UnpackedApplication
+ File[] files = application.listFiles();
+ if (files != null && files.length == 1 && files[0].isDirectory()) {
+ application = files[0];
+ }
+ return application;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactory.java
new file mode 100644
index 00000000000..3fc3c0ff8aa
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactory.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.server;
+
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+
+/**
+ * Represents a component that creates config responses from a payload. Different implementations
+ * can do transformations of the payload such as compression.
+ *
+ * @author lulf
+ * @since 5.19
+ */
+public interface ConfigResponseFactory {
+ /**
+ * Create a {@link ConfigResponse} for a given payload and generation.
+ * @param payload The {@link com.yahoo.vespa.config.ConfigPayload} to put in the response.
+ * @param defFile The {@link com.yahoo.config.codegen.InnerCNode} def file for this config.
+ * @param generation The payload generation. @return A {@link ConfigResponse} that can be sent to the client.
+ */
+ ConfigResponse createResponse(ConfigPayload payload, InnerCNode defFile, long generation);
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactoryFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactoryFactory.java
new file mode 100644
index 00000000000..309f9052a71
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactoryFactory.java
@@ -0,0 +1,24 @@
+// 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.server;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+
+/**
+ * Logic to select the appropriate response factory based on config.
+ * TODO: Move this to {@link ConfigResponseFactory} when we have java 8.
+ *
+ * @author lulf
+ * @since 5.20
+ */
+public class ConfigResponseFactoryFactory {
+ public static ConfigResponseFactory createFactory(ConfigserverConfig configserverConfig) {
+ switch (configserverConfig.payloadCompressionType()) {
+ case LZ4:
+ return new LZ4ConfigResponseFactory();
+ case UNCOMPRESSED:
+ return new UncompressedConfigResponseFactory();
+ default:
+ throw new IllegalArgumentException("Unknown payload compression type " + configserverConfig.payloadCompressionType());
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerBootstrap.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerBootstrap.java
new file mode 100644
index 00000000000..b4a0f6135c3
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerBootstrap.java
@@ -0,0 +1,57 @@
+// 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.server;
+
+import com.google.inject.Inject;
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.config.provision.Deployer;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.server.version.VersionState;
+
+/**
+ * Main component that bootstraps and starts config server threads.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ConfigServerBootstrap extends AbstractComponent implements Runnable {
+
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(ConfigServerBootstrap.class.getName());
+ private final RpcServer server;
+ private final Thread serverThread;
+
+ // The tenants object is injected so that all initial requests handlers are
+ // added to the rpcserver before it starts answering rpc requests.
+ @SuppressWarnings("UnusedParameters")
+ @Inject
+ public ConfigServerBootstrap(Tenants tenants, RpcServer server, Deployer deployer, VersionState versionState) {
+ this.server = server;
+ if (versionState.isUpgraded()) {
+ log.log(LogLevel.INFO, "Configserver upgraded from " + versionState.storedVersion() + " to " + versionState.currentVersion() + ". Redeploying all applications");
+ tenants.redeployApplications(deployer);
+ log.log(LogLevel.INFO, "All applications redeployed");
+ }
+ versionState.saveNewVersion();
+ this.serverThread = new Thread(this, "configserver main");
+ serverThread.start();
+ }
+
+ @Override
+ public void deconstruct() {
+ log.log(LogLevel.INFO, "Stopping config server");
+ server.stop();
+ try {
+ serverThread.join();
+ } catch (InterruptedException e) {
+ log.log(LogLevel.WARNING, "Error joining server thread on shutdown: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public void run() {
+ log.log(LogLevel.DEBUG, "Starting RPC server");
+ server.run();
+ log.log(LogLevel.DEBUG, "RPC server stopped");
+ }
+
+}
+
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerDB.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerDB.java
new file mode 100644
index 00000000000..b589e96e2e9
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerDB.java
@@ -0,0 +1,93 @@
+// 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.server;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.model.application.provider.Bundle;
+import com.yahoo.config.application.ConfigDefinitionDir;
+import com.yahoo.io.IOUtils;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.defaults.Defaults;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Config server db is the maintainer of the serverdb directory containing def files and the file system sessions.
+ * See also {@link com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs} which maintains directories per tenant.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ConfigServerDB {
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(ConfigServerDB.class.getName());
+ private final File serverDB;
+ private final ConfigserverConfig configserverConfig;
+
+ public ConfigServerDB(ConfigserverConfig configserverConfig) {
+ this.configserverConfig = configserverConfig;
+ this.serverDB = new File(Defaults.getDefaults().underVespaHome(configserverConfig.configServerDBDir()));
+ create();
+ try {
+ initialize(configserverConfig.configModelPluginDir());
+ } catch (IllegalArgumentException e) {
+ log.log(LogLevel.ERROR, "Error initializing serverdb: " + e.getMessage());
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to initialize server db", e);
+ }
+ }
+
+ public static ConfigServerDB createTestConfigServerDb(String dir) {
+ return new ConfigServerDB(new ConfigserverConfig(new ConfigserverConfig.Builder().configServerDBDir(dir)));
+ }
+
+ public File dest() { return new File(serverDB, "configs"); }
+ public File classes() { return new File(serverDB, "classes"); }
+ public File vespaapps() { return new File(serverDB, "vespaapps"); }
+ public File serverdefs() { return new File(serverDB, "serverdefs"); }
+
+
+ /**
+ * Creates all the config server db's dirs that are global.
+ */
+ public void create() {
+ cr(dest());
+ cr(classes());
+ cr(vespaapps());
+ cr(serverdefs());
+ }
+
+ public static void cr(File d) {
+ if (d.exists()) {
+ if (!d.isDirectory()) {
+ throw new IllegalArgumentException(d.getAbsolutePath() + " exists, but isn't a directory.");
+ }
+ } else {
+ if (!d.mkdirs()) {
+ throw new IllegalArgumentException("Couldn't create " + d.getAbsolutePath());
+ }
+ }
+ }
+
+ private void initialize(List<String> pluginDirectories) throws IOException {
+ IOUtils.recursiveDeleteDir(serverdefs());
+ IOUtils.copyDirectory(classes(), serverdefs());
+ ConfigDefinitionDir configDefinitionDir = new ConfigDefinitionDir(serverdefs());
+ ArrayList<Bundle> bundles = new ArrayList<>();
+ for (String pluginDirectory : pluginDirectories) {
+ bundles.addAll(Bundle.getBundles(new File(pluginDirectory)));
+ }
+ log.log(LogLevel.DEBUG, "Found " + bundles.size() + " bundles");
+ List<Bundle> addedBundles = new ArrayList<>();
+ for (Bundle bundle : bundles) {
+ log.log(LogLevel.DEBUG, "Bundle in " + bundle.getFile().getAbsolutePath() + " appears to contain " + bundle.getDefEntries().size() + " entries");
+ configDefinitionDir.addConfigDefinitionsFromBundle(bundle, addedBundles);
+ addedBundles.add(bundle);
+ }
+ }
+
+ public ConfigserverConfig getConfigserverConfig() {
+ return configserverConfig;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerSpec.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerSpec.java
new file mode 100644
index 00000000000..a84985d738c
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerSpec.java
@@ -0,0 +1,73 @@
+// 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.server;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author tonytv
+ */
+public class ConfigServerSpec implements com.yahoo.config.model.api.ConfigServerSpec {
+ private final String hostName;
+ private final int configServerPort;
+ private final int httpPort;
+ private final int zooKeeperPort;
+
+ public String getHostName() {
+ return hostName;
+ }
+
+ public int getConfigServerPort() {
+ return configServerPort;
+ }
+
+ public int getHttpPort() {
+ return httpPort;
+ }
+
+ public int getZooKeeperPort() {
+ return zooKeeperPort;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof ConfigServerSpec) {
+ ConfigServerSpec other = (ConfigServerSpec)o;
+
+ return hostName.equals(other.hostName) &&
+ configServerPort == other.configServerPort &&
+ httpPort == other.httpPort &&
+ zooKeeperPort == other.zooKeeperPort;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return hostName.hashCode();
+ }
+
+ public ConfigServerSpec(String hostName, int configServerPort, int httpPort, int zooKeeperPort) {
+ this.hostName = hostName;
+ this.configServerPort = configServerPort;
+ this.httpPort = httpPort;
+ this.zooKeeperPort = zooKeeperPort;
+ }
+
+ public static List<com.yahoo.config.model.api.ConfigServerSpec> fromConfig(ConfigserverConfig configserverConfig) {
+ List<com.yahoo.config.model.api.ConfigServerSpec> specs = new ArrayList<>();
+ for (ConfigserverConfig.Zookeeperserver server : configserverConfig.zookeeperserver()) {
+ // TODO We cannot be sure that http port always is rpcport + 1
+ specs.add(new ConfigServerSpec(server.hostname(), configserverConfig.rpcport(), configserverConfig.rpcport() + 1, server.port()));
+ }
+ return specs;
+ }
+
+ @Override
+ public String toString() {
+ return "hostname=" + hostName + ", rpc port=" + configServerPort + ", http port=" + httpPort + ", zookeeper port=" + zooKeeperPort;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/DelayedConfigResponses.java b/configserver/src/main/java/com/yahoo/vespa/config/server/DelayedConfigResponses.java
new file mode 100644
index 00000000000..1c6f75b62f0
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/DelayedConfigResponses.java
@@ -0,0 +1,224 @@
+// 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.server;
+
+import com.yahoo.concurrent.ThreadFactoryFactory;
+import com.yahoo.jrt.Target;
+import com.yahoo.jrt.TargetWatcher;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.config.provision.ApplicationId;
+
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.logging.Logger;
+
+/**
+ * Takes care of <i>delayed responses</i> in the config server.
+ * A delayed response is a response sent at request (server) timeout
+ * for a config which has not changed since the request was initiated.
+ *
+ * @author musum
+ */
+public class DelayedConfigResponses {
+ private static final Logger log = Logger.getLogger(DelayedConfigResponses.class.getName());
+ private final RpcServer rpcServer;
+
+ private final ScheduledExecutorService executorService;
+ private final boolean useJrtWatcher;
+
+ private Map<ApplicationId, MetricUpdater> metrics = new ConcurrentHashMap<>();
+
+ /* Requests that resolve to config that has not changed are put on this queue. When reloading
+ config, all requests on this queue are reprocessed as if they were a new request */
+ private final Map<ApplicationId, BlockingQueue<DelayedConfigResponse>> delayedResponses =
+ new ConcurrentHashMap<>();
+
+ public DelayedConfigResponses(RpcServer rpcServer, int numTimerThreads) {
+ this(rpcServer, numTimerThreads, true);
+ }
+
+ // Since JRT does not allow adding watcher for "fake" requests, we must be able to disable it for unit tests :(
+ DelayedConfigResponses(RpcServer rpcServer, int numTimerThreads, boolean useJrtWatcher) {
+ this.rpcServer = rpcServer;
+ ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(numTimerThreads, ThreadFactoryFactory.getThreadFactory(DelayedConfigResponses.class.getName()));
+ executor.setRemoveOnCancelPolicy(true);
+ this.executorService = executor;
+ this.useJrtWatcher = useJrtWatcher;
+ }
+
+ List<DelayedConfigResponse> allDelayedResponses() {
+ List<DelayedConfigResponse> responses = new ArrayList<>();
+ for (Map.Entry<ApplicationId, BlockingQueue<DelayedConfigResponse>> entry : delayedResponses.entrySet()) {
+ responses.addAll(entry.getValue());
+ }
+ return responses;
+ }
+
+ /**
+ * The run method of this class is run by a Timer when the timeout expires.
+ * The timer associated with this response must be cancelled first.
+ */
+ public class DelayedConfigResponse implements Runnable, TargetWatcher {
+
+ final JRTServerConfigRequest request;
+ private final BlockingQueue<DelayedConfigResponse> delayedResponsesQueue;
+ private final ApplicationId app;
+ private ScheduledFuture<?> future;
+
+ public DelayedConfigResponse(JRTServerConfigRequest req, BlockingQueue<DelayedConfigResponse> delayedResponsesQueue, ApplicationId app) {
+ this.request = req;
+ this.delayedResponsesQueue = delayedResponsesQueue;
+ this.app = app;
+ }
+
+ public synchronized void run() {
+ remove();
+ rpcServer.addToRequestQueue(request, true, null);
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, logPre()+"DelayedConfigResponse. putting on queue: " + request.getShortDescription());
+ }
+ }
+
+ /**
+ * Remove delayed response from its queue
+ */
+ private void remove() {
+ delayedResponsesQueue.remove(this);
+ removeWatcher();
+ }
+
+ public JRTServerConfigRequest getRequest() {
+ return request;
+ }
+
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Delayed response for ").append(logPre()).append(request.getShortDescription());
+ return sb.toString();
+ }
+
+ public ApplicationId getAppId() { return app; }
+
+ String logPre() {
+ return Tenants.logPre(app);
+ }
+
+ public synchronized boolean cancel() {
+ remove();
+ if (future == null) {
+ throw new IllegalStateException("Cannot cancel a task that has not been scheduled");
+ }
+ return future.cancel(false);
+ }
+
+ public synchronized void schedule(long delay) throws InterruptedException {
+ delayedResponsesQueue.put(this);
+ future = executorService.schedule(this, delay, TimeUnit.MILLISECONDS);
+ addWatcher();
+ }
+
+ /**
+ * Removes this delayed response if target is invalid.
+ *
+ * @param target a Target that has become invalid (i.e, client has closed connection)
+ * @see DelayedConfigResponses
+ */
+ @Override
+ public void notifyTargetInvalid(Target target) {
+ cancel();
+ }
+
+ private void addWatcher() {
+ if (useJrtWatcher) {
+ request.getRequest().target().addWatcher(this);
+ }
+ }
+
+ private void removeWatcher() {
+ if (useJrtWatcher) {
+ request.getRequest().target().removeWatcher(this);
+ }
+ }
+ }
+
+ /**
+ * Creates a DelayedConfigResponse object for taking care of requests that should
+ * not be responded to right away. Puts the object on the delayedResponsesQueue.
+ *
+ * NOTE: This method is called from multiple threads, so everything here needs to be
+ * thread safe!
+ *
+ * @param request a JRTConfigRequest
+ */
+ public final void delayResponse(JRTServerConfigRequest request, GetConfigContext context) {
+ if (request.isDelayedResponse()) {
+ log.log(LogLevel.DEBUG, context.logPre()+"Request already delayed");
+ } else {
+ createQueueIfNotExists(context);
+ BlockingQueue<DelayedConfigResponse> delayedResponsesQueue = delayedResponses.get(context.applicationId());
+ DelayedConfigResponse response = new DelayedConfigResponse(request, delayedResponsesQueue, context.applicationId());
+ request.setDelayedResponse(true);
+ try {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, context.logPre()+"Putting on delayedRequests queue (" + delayedResponsesQueue.size() + " elements): " +
+ response.getRequest().getShortDescription());
+ }
+ // Config will be resolved in the run() method of DelayedConfigResponse,
+ // when the timer expires or config is updated/reloaded.
+ response.schedule(Math.max(0, request.getTimeout()));
+ metricDelayedResponses(context.applicationId(), delayedResponsesQueue.size());
+ } catch (InterruptedException e) {
+ log.log(LogLevel.WARNING, context.logPre()+"Interrupted when putting on delayed requests queue.");
+ }
+ }
+ }
+
+ private synchronized void metricDelayedResponses(ApplicationId app, int elems) {
+ if ( ! metrics.containsKey(app)) {
+ metrics.put(app, rpcServer.metricUpdaterFactory().getOrCreateMetricUpdater(Metrics.createDimensions(app)));
+ }
+ metrics.get(app).setDelayedResponses(elems);
+ }
+
+ private synchronized void createQueueIfNotExists(GetConfigContext context) {
+ if ( ! delayedResponses.containsKey(context.applicationId())) {
+ delayedResponses.put(context.applicationId(), new LinkedBlockingQueue<>());
+ }
+ }
+
+ public void stop() {
+ executorService.shutdown();
+ }
+
+ /**
+ * Drains delayed responses queue and returns responses in an array
+ *
+ * @return and array of DelayedConfigResponse objects
+ */
+ public List<DelayedConfigResponse> drainQueue(ApplicationId app) {
+ ArrayList<DelayedConfigResponse> ret = new ArrayList<>();
+
+ if (delayedResponses.containsKey(app)) {
+ BlockingQueue<DelayedConfigResponse> queue = delayedResponses.get(app);
+ queue.drainTo(ret);
+ }
+ metrics.remove(app);
+ return ret;
+ }
+
+ public String toString() {
+ return "DelayedConfigResponses. Average Size=" + size();
+ }
+
+ public int size() {
+ int totalQueueSize = 0;
+ int numQueues = 0;
+ for (Map.Entry<ApplicationId, BlockingQueue<DelayedConfigResponse>> e : delayedResponses.entrySet()) {
+ numQueues++;
+ totalQueueSize+=e.getValue().size();
+ }
+ return (numQueues > 0) ? (totalQueueSize / numQueues) : 0;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/DeployHandlerLogger.java b/configserver/src/main/java/com/yahoo/vespa/config/server/DeployHandlerLogger.java
new file mode 100644
index 00000000000..2c652d74b97
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/DeployHandlerLogger.java
@@ -0,0 +1,48 @@
+// 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.server;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.log.LogLevel;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Slime;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A {@link DeployLogger} which persists messages as a {@link Slime} tree, and holds a tenant and application name.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class DeployHandlerLogger implements DeployLogger {
+ private static final Logger log = Logger.getLogger(DeployHandlerLogger.class.getName());
+
+ private final Cursor logroot;
+ private final boolean verbose;
+ private final ApplicationId app;
+
+ public DeployHandlerLogger(Cursor root, boolean verbose, ApplicationId app) {
+ logroot = root;
+ this.verbose = verbose;
+ this.app = app;
+ }
+
+ @Override
+ public void log(Level level, String message) {
+ if ((level == LogLevel.FINE ||
+ level == LogLevel.DEBUG ||
+ level == LogLevel.SPAM) &&
+ !verbose) {
+ return;
+ }
+ String fullMsg = Tenants.logPre(app)+message;
+ Cursor entry = logroot.addObject();
+ entry.setLong("time", System.currentTimeMillis());
+ entry.setString("level", level.getName());
+ entry.setString("message", fullMsg);
+ // Also tee to a normal log, Vespa log for example, but use level fine
+ log.log(LogLevel.FINE, fullMsg);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigContext.java b/configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigContext.java
new file mode 100644
index 00000000000..e27a1c56dc3
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigContext.java
@@ -0,0 +1,57 @@
+// 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.server;
+
+import com.yahoo.vespa.config.protocol.Trace;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+
+/**
+ * Contains the context for serving getconfig requests so that this information does not have to be looked up multiple times.
+ *
+ * @author lulf
+ * @since 5.8
+ */
+public class GetConfigContext {
+
+ private final ApplicationId app;
+ private final RequestHandler requestHandler;
+ private final Trace trace;
+
+ private GetConfigContext(ApplicationId app, RequestHandler handler, Trace trace) {
+ this.app = app;
+ this.requestHandler = handler;
+ this.trace = trace;
+ }
+
+ public TenantName tenant() {
+ return app.tenant();
+ }
+
+ public ApplicationId applicationId() {
+ return app;
+ }
+
+ public Trace trace() {
+ return trace;
+ }
+
+ public RequestHandler requestHandler() {
+ return requestHandler;
+ }
+
+ public static GetConfigContext create(ApplicationId app, RequestHandler handler, Trace trace) {
+ return new GetConfigContext(app, handler, trace);
+ }
+
+ public static GetConfigContext testContext(ApplicationId app) {
+ return new GetConfigContext(app, null, null);
+ }
+
+ /**
+ * Helper to produce a log preamble with the tenant and app id
+ * @return log msg preamble
+ */
+ public String logPre() {
+ return Tenants.logPre(app);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigProcessor.java b/configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigProcessor.java
new file mode 100644
index 00000000000..eb5fedf9a40
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigProcessor.java
@@ -0,0 +1,176 @@
+// 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.server;
+
+import com.yahoo.cloud.config.SentinelConfig;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Version;
+import com.yahoo.jrt.Request;
+import com.yahoo.log.LogLevel;
+import com.yahoo.net.LinuxInetAddress;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.ErrorCode;
+import com.yahoo.vespa.config.UnknownConfigIdException;
+import com.yahoo.vespa.config.protocol.*;
+import com.yahoo.vespa.config.util.ConfigUtils;
+
+import java.net.UnknownHostException;
+import java.util.Optional;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+* @author musum
+* @since 5.1
+*/
+class GetConfigProcessor implements Runnable {
+
+ private static final Logger log = Logger.getLogger(GetConfigProcessor.class.getName());
+ private static final String localHostName;
+
+ private final JRTServerConfigRequest request;
+ /* True only when this request has expired its server timeout and we need to respond to the client */
+ private boolean forceResponse = false;
+ private final RpcServer rpcServer;
+ private String logPre = "";
+
+ GetConfigProcessor(RpcServer rpcServer, JRTServerConfigRequest request, boolean forceResponse) {
+ this.rpcServer = rpcServer;
+ this.request = request;
+ this.forceResponse = forceResponse;
+ }
+
+ private void respond(JRTServerConfigRequest request) {
+ final Request req = request.getRequest();
+ if (req.isError()) {
+ Level logLevel = (req.errorCode() == ErrorCode.APPLICATION_NOT_LOADED) ? LogLevel.DEBUG : LogLevel.INFO;
+ log.log(logLevel, logPre + req.errorMessage());
+ }
+ rpcServer.respond(request);
+ }
+
+ private void handleError(JRTServerConfigRequest request, int errorCode, String message) {
+ String target = "(unknown)";
+ try {
+ target = request.getRequest().target().toString();
+ } catch (IllegalStateException e) {
+ //ignore when no target
+ }
+ request.addErrorResponse(errorCode, logPre + "Failed request (" + message + ") from " + target);
+ respond(request);
+ }
+
+ // TODO: Increment statistics (Metrics) failed counters when requests fail
+ public void run() {
+ //Request has already been detached
+ 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());
+ respond(request);
+ return;
+ }
+ Trace trace = request.getRequestTrace();
+ if (logDebug(trace)) {
+ debugLog(trace, "GetConfigProcessor.run() on " + localHostName);
+ }
+
+ Optional<TenantName> tenant = rpcServer.resolveTenant(request, trace);
+
+ // If we are certain that this request is from a node that no longer belongs to this application,
+ // fabricate an empty request to cause the sentinel to stop all running services
+ if (rpcServer.isHostedVespa() && rpcServer.allTenantsLoaded() && !tenant.isPresent() && isSentinelConfigRequest(request)) {
+ returnEmpty(request);
+ return;
+ }
+
+ GetConfigContext context = rpcServer.createGetConfigContext(tenant, request, trace);
+ if (context == null || ! context.requestHandler().hasApplication(context.applicationId(), Optional.<Version>empty())) {
+ handleError(request, ErrorCode.APPLICATION_NOT_LOADED, "No application exists");
+ return;
+ }
+
+ Optional<Version> vespaVersion = rpcServer.useRequestVersion() ?
+ request.getVespaVersion().map(VespaVersion::toString).map(Version::fromString) :
+ Optional.empty();
+ if (logDebug(trace)) {
+ debugLog(trace, "Using version " + getPrintableVespaVersion(vespaVersion));
+ }
+
+ if ( ! context.requestHandler().hasApplication(context.applicationId(), vespaVersion)) {
+ handleError(request, ErrorCode.UNKNOWN_VESPA_VERSION, "Unknown Vespa version in request: " + getPrintableVespaVersion(vespaVersion));
+ return;
+ }
+
+ this.logPre = Tenants.logPre(context.applicationId());
+ ConfigResponse config;
+ try {
+ config = rpcServer.resolveConfig(request, context, vespaVersion);
+ } catch (UnknownConfigDefinitionException e) {
+ handleError(request, ErrorCode.UNKNOWN_DEFINITION, "Unknown config definition " + request.getConfigKey());
+ return;
+ } catch (UnknownConfigIdException e) {
+ handleError(request, ErrorCode.ILLEGAL_CONFIGID, "Illegal config id " + request.getConfigKey().getConfigId());
+ return;
+ } catch (Exception e) {
+ log.log(Level.SEVERE, "Unexpected error handling config request", e);
+ handleError(request, ErrorCode.INTERNAL_ERROR, "Internal error " + e.getMessage());
+ return;
+ }
+
+ // config == null is not an error, but indicates that the config will be returned later.
+ if ((config != null) && (!config.hasEqualConfig(request) || config.hasNewerGeneration(request) || forceResponse)) {
+ // debugLog(trace, "config response before encoding:" + config.toString());
+ request.addOkResponse(request.payloadFromResponse(config), config.getGeneration(), config.getConfigMd5());
+ if (logDebug(trace)) {
+ debugLog(trace, "return response: " + request.getShortDescription());
+ }
+ respond(request);
+ } else {
+ if (logDebug(trace)) {
+ debugLog(trace, "delaying response " + request.getShortDescription());
+ }
+ rpcServer.delayResponse(request, context);
+ }
+ }
+
+ private boolean isSentinelConfigRequest(JRTServerConfigRequest request) {
+ return request.getConfigKey().getName().equals(SentinelConfig.getDefName()) &&
+ request.getConfigKey().getNamespace().equals(SentinelConfig.getDefNamespace());
+ }
+
+ private static String getPrintableVespaVersion(Optional<Version> vespaVersion) {
+ return (vespaVersion.isPresent() ? vespaVersion.get().toString() : "LATEST");
+ }
+
+ private void returnEmpty(JRTServerConfigRequest request) {
+ ConfigPayload emptyPayload = ConfigPayload.empty();
+ String configMd5 = ConfigUtils.getMd5(emptyPayload);
+ ConfigResponse config = SlimeConfigResponse.fromConfigPayload(emptyPayload, null, 0, configMd5);
+ request.addOkResponse(request.payloadFromResponse(config), config.getGeneration(), config.getConfigMd5());
+ respond(request);
+ }
+
+ /**
+ * Done in a static block to prevent people invoking this directly.
+ * Do not call java.net.Inet4AddressImpl.getLocalHostName() on each request, as this causes CPU bottlenecks.
+ */
+ static {
+ String hostName = "unknown";
+ try {
+ hostName = LinuxInetAddress.getLocalHost().getHostName();
+ } catch (UnknownHostException e) {
+ // ignore if it fails
+ }
+ localHostName = hostName;
+ }
+
+ static boolean logDebug(Trace trace) {
+ return trace.shouldTrace(RpcServer.TRACELEVEL_DEBUG) || log.isLoggable(LogLevel.DEBUG);
+ }
+
+ private void debugLog(Trace trace, String message) {
+ if (logDebug(trace)) {
+ log.log(LogLevel.DEBUG, logPre + message);
+ trace.trace(RpcServer.TRACELEVEL_DEBUG, logPre + message);
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/GlobalComponentRegistry.java b/configserver/src/main/java/com/yahoo/vespa/config/server/GlobalComponentRegistry.java
new file mode 100644
index 00000000000..7ea65173a53
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/GlobalComponentRegistry.java
@@ -0,0 +1,41 @@
+// 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.server;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.provision.Provisioner;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.config.server.application.PermanentApplicationPackage;
+import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.vespa.config.server.session.SessionPreparer;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+import com.yahoo.vespa.curator.Curator;
+
+import java.util.Optional;
+
+/**
+ * Interface representing all global config server components used within the config server.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public interface GlobalComponentRegistry {
+
+ Curator getCurator();
+ ConfigCurator getConfigCurator();
+ Metrics getMetrics();
+ ConfigServerDB getServerDB();
+ SessionPreparer getSessionPreparer();
+ ConfigserverConfig getConfigserverConfig();
+ TenantListener getTenantListener();
+ ReloadListener getReloadListener();
+ SuperModelGenerationCounter getSuperModelGenerationCounter();
+ ConfigDefinitionRepo getConfigDefinitionRepo();
+ PermanentApplicationPackage getPermanentApplicationPackage();
+ HostRegistries getHostRegistries();
+ ModelFactoryRegistry getModelFactoryRegistry();
+ Optional<Provisioner> getHostProvisioner();
+ Zone getZone();
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistries.java b/configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistries.java
new file mode 100644
index 00000000000..dc411626b39
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistries.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.server;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+
+import java.util.HashMap;
+
+/**
+ * Component to hold host registries.
+ *
+ * @author musum
+ */
+public class HostRegistries {
+
+ private final HostRegistry<TenantName> tenantHostRegistry = new HostRegistry<>();
+ private final HashMap<TenantName, HostRegistry<ApplicationId>> applicationHostRegistries = new HashMap<>();
+
+ public HostRegistry<TenantName> getTenantHostRegistry() {
+ return tenantHostRegistry;
+ }
+
+ public HostRegistry<ApplicationId> getApplicationHostRegistry(TenantName tenant) {
+ return applicationHostRegistries.get(tenant);
+ }
+
+ public HostRegistry<ApplicationId> createApplicationHostRegistry(TenantName tenant) {
+ HostRegistry<ApplicationId> applicationIdHostRegistry = new HostRegistry<>();
+ applicationHostRegistries.put(tenant, applicationIdHostRegistry);
+ return applicationIdHostRegistry;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistry.java b/configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistry.java
new file mode 100644
index 00000000000..a62e0059c2a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistry.java
@@ -0,0 +1,95 @@
+// 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.server;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Logger;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.yahoo.log.LogLevel;
+
+/**
+ * A host registry that create mappings between some type T and a list of hosts, represented as
+ * strings.
+ * TODO: Maybe we should have a Host type, but using String for now.
+ * TODO: Is there a generalized version of this pattern? Need some sort mix of Bimap and Multimap
+ *
+ * @author lulf
+ * @since 5.3
+ */
+public class HostRegistry<T> implements HostValidator<T> {
+
+ private static final Logger log = Logger.getLogger(HostRegistry.class.getName());
+
+ private final Map<T, Collection<String>> key2HostsMap = new ConcurrentHashMap<>();
+ private final Map<String, T> host2KeyMap = new ConcurrentHashMap<>();
+
+ public T getKeyForHost(String hostName) {
+ return host2KeyMap.get(hostName);
+ }
+
+ public void update(T key, Collection<String> newHosts) {
+ verifyHosts(key, newHosts);
+ log.log(LogLevel.DEBUG, "Setting hosts for key(" + key + "), newHosts(" + newHosts + "), currentHosts(" + getCurrentHosts(key) + ")");
+ Collection<String> removedHosts = getRemovedHosts(newHosts, getCurrentHosts(key));
+ removeHosts(removedHosts);
+ addHosts(key, newHosts);
+ }
+
+ public void verifyHosts(T key, Collection<String> newHosts) {
+ for (String host : newHosts) {
+ if (hostAlreadyTaken(host, key)) {
+ throw new IllegalArgumentException("'" + key + "' tried to allocate host '" + host + "', but the host is already taken by '" + host2KeyMap.get(host) + "'");
+ }
+ }
+ }
+
+ public void removeHostsForKey(T key) {
+ for (Iterator<Map.Entry<T, Collection<String>>> it = key2HostsMap.entrySet().iterator(); it.hasNext(); ) {
+ Map.Entry<T, Collection<String>> entry = it.next();
+ if (entry.getKey().equals(key)) {
+ Collection<String> hosts = entry.getValue();
+ it.remove();
+ removeHosts(hosts);
+ }
+ }
+ }
+
+ public Collection<String> getAllHosts() {
+ return Collections.unmodifiableCollection(new ArrayList<>(host2KeyMap.keySet()));
+ }
+
+ Collection<String> getCurrentHosts(T key) {
+ return key2HostsMap.containsKey(key) ? new ArrayList<>(key2HostsMap.get(key)) : new ArrayList<String>();
+ }
+
+ private boolean hostAlreadyTaken(String host, T key) {
+ return host2KeyMap.containsKey(host) && !key.equals(host2KeyMap.get(host));
+ }
+
+ private static Collection<String> getRemovedHosts(final Collection<String> newHosts, Collection<String> previousHosts) {
+ return Collections2.filter(previousHosts, new Predicate<String>() {
+ @Override
+ public boolean apply(String host) {
+ return !newHosts.contains(host);
+ }
+ });
+ }
+
+ private void removeHosts(Collection<String> removedHosts) {
+ for (String host : removedHosts) {
+ log.log(LogLevel.DEBUG, "Removing " + host);
+ host2KeyMap.remove(host);
+ }
+ }
+
+ private void addHosts(T key, Collection<String> newHosts) {
+ for (String host : newHosts) {
+ log.log(LogLevel.DEBUG, "Adding " + host);
+ host2KeyMap.put(host, key);
+ }
+ key2HostsMap.put(key, new ArrayList<>(newHosts));
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/HostValidator.java b/configserver/src/main/java/com/yahoo/vespa/config/server/HostValidator.java
new file mode 100644
index 00000000000..67292e281bb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/HostValidator.java
@@ -0,0 +1,17 @@
+// 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.server;
+
+import java.util.Collection;
+
+/**
+ * A read only host registry that has mappings from a host to some type T.
+ * strings.
+ *
+ * @author lulf
+ * @since 5.9
+ */
+public interface HostValidator<T> {
+
+ void verifyHosts(T key, Collection<String> newHosts);
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistry.java b/configserver/src/main/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistry.java
new file mode 100644
index 00000000000..fd5529cdffd
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistry.java
@@ -0,0 +1,109 @@
+// 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.server;
+
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.provision.Provisioner;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.config.server.application.PermanentApplicationPackage;
+import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.session.SessionPreparer;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+import com.yahoo.vespa.curator.Curator;
+
+import java.util.Optional;
+
+/**
+ * Registry containing all the "static"/"global" components in a config server in one place.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class InjectedGlobalComponentRegistry implements GlobalComponentRegistry {
+
+ private final Curator curator;
+ private final ConfigCurator configCurator;
+ private final Metrics metrics;
+ private final ModelFactoryRegistry modelFactoryRegistry;
+ private final ConfigServerDB serverDB;
+ private final SessionPreparer sessionPreparer;
+ private final RpcServer rpcServer;
+ private final ConfigserverConfig configserverConfig;
+ private final SuperModelGenerationCounter superModelGenerationCounter;
+ private final ConfigDefinitionRepo defRepo;
+ private final PermanentApplicationPackage permanentApplicationPackage;
+ private final HostRegistries hostRegistries;
+ private final Optional<Provisioner> hostProvisioner;
+ private final Zone zone;
+
+ @Inject
+ public InjectedGlobalComponentRegistry(Curator curator,
+ ConfigCurator configCurator,
+ Metrics metrics,
+ ModelFactoryRegistry modelFactoryRegistry,
+ ConfigServerDB serverDB,
+ SessionPreparer sessionPreparer,
+ RpcServer rpcServer,
+ ConfigserverConfig configserverConfig,
+ SuperModelGenerationCounter superModelGenerationCounter,
+ ConfigDefinitionRepo defRepo,
+ PermanentApplicationPackage permanentApplicationPackage,
+ HostRegistries hostRegistries,
+ HostProvisionerProvider hostProvisionerProvider,
+ Zone zone) {
+ this.curator = curator;
+ this.configCurator = configCurator;
+ this.metrics = metrics;
+ this.modelFactoryRegistry = modelFactoryRegistry;
+ this.serverDB = serverDB;
+ this.sessionPreparer = sessionPreparer;
+ this.rpcServer = rpcServer;
+ this.configserverConfig = configserverConfig;
+ this.superModelGenerationCounter = superModelGenerationCounter;
+ this.defRepo = defRepo;
+ this.permanentApplicationPackage = permanentApplicationPackage;
+ this.hostRegistries = hostRegistries;
+ this.hostProvisioner = hostProvisionerProvider.getHostProvisioner();
+ this.zone = zone;
+ }
+
+ @Override
+ public Curator getCurator() { return curator; }
+ @Override
+ public ConfigCurator getConfigCurator() { return configCurator; }
+ @Override
+ public Metrics getMetrics() { return metrics; }
+ @Override
+ public ConfigServerDB getServerDB() { return serverDB; }
+ @Override
+ public SessionPreparer getSessionPreparer() { return sessionPreparer; }
+ @Override
+ public ConfigserverConfig getConfigserverConfig() { return configserverConfig; }
+ @Override
+ public TenantListener getTenantListener() { return rpcServer; }
+ @Override
+ public ReloadListener getReloadListener() { return rpcServer; }
+ @Override
+ public SuperModelGenerationCounter getSuperModelGenerationCounter() { return superModelGenerationCounter; }
+ @Override
+ public ConfigDefinitionRepo getConfigDefinitionRepo() { return defRepo; }
+ @Override
+ public PermanentApplicationPackage getPermanentApplicationPackage() { return permanentApplicationPackage; }
+ @Override
+ public HostRegistries getHostRegistries() { return hostRegistries; }
+ @Override
+ public ModelFactoryRegistry getModelFactoryRegistry() { return modelFactoryRegistry; }
+
+ @Override
+ public Optional<Provisioner> getHostProvisioner() {
+ return hostProvisioner;
+ }
+
+ @Override
+ public Zone getZone() {
+ return zone;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/LZ4ConfigResponseFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/LZ4ConfigResponseFactory.java
new file mode 100644
index 00000000000..66a2dc32eea
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/LZ4ConfigResponseFactory.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.server;
+
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.text.Utf8Array;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.LZ4PayloadCompressor;
+import com.yahoo.vespa.config.protocol.CompressionInfo;
+import com.yahoo.vespa.config.protocol.CompressionType;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.protocol.SlimeConfigResponse;
+import com.yahoo.vespa.config.util.ConfigUtils;
+
+/**
+ * Compressor that compresses config payloads to lz4.
+ *
+ * @author lulf
+ * @since 5.19
+ */
+public class LZ4ConfigResponseFactory implements ConfigResponseFactory {
+
+ private static LZ4PayloadCompressor compressor = new LZ4PayloadCompressor();
+
+ @Override
+ public ConfigResponse createResponse(ConfigPayload payload, InnerCNode defFile, long generation) {
+ Utf8Array rawPayload = payload.toUtf8Array(true);
+ String configMd5 = ConfigUtils.getMd5(rawPayload);
+ CompressionInfo info = CompressionInfo.create(CompressionType.LZ4, rawPayload.getByteLength());
+ Utf8Array compressed = new Utf8Array(compressor.compress(rawPayload.getBytes()));
+ return new SlimeConfigResponse(compressed, defFile, generation, configMd5, info);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/PathProvider.java b/configserver/src/main/java/com/yahoo/vespa/config/server/PathProvider.java
new file mode 100644
index 00000000000..ca715d66a05
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/PathProvider.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.server;
+
+import com.google.inject.Inject;
+import com.yahoo.path.Path;
+
+/**
+ * Temporary provider of root path for components that will soon get them injected from a parent class.
+ *
+ * @author lulf
+ * * @since 5.1.24
+ */
+public class PathProvider {
+ private final Path root;
+ // Path for Vespa-related data stored in Zookeeper (subpaths are relative to this path)
+ // NOTE: This should not be exposed, as this path can be different in testing, depending on how we configure it.
+ private static final String APPS_ZK_NODE = "sessions";
+ private static final String VESPA_ZK_PATH = "/vespa/config";
+ //private static final String VESPA_ZK_PATH = "/config/v2/tenants/default";
+ private static final String LIVEAPP_ZK_NODE = "liveapp";
+
+ @Inject
+ public PathProvider() {
+ root = Path.fromString(VESPA_ZK_PATH);
+ }
+
+ public PathProvider(Path root) {
+ this.root = root;
+ }
+
+ public Path getRoot() {
+ return root;
+ }
+
+ public Path getSessionDirs() {
+ return root.append(APPS_ZK_NODE);
+ }
+
+ public Path getSessionDir(long sessionId) {
+ return getSessionDirs().append(String.valueOf(sessionId));
+ }
+
+ public Path getLiveApp() {
+ return root.append(LIVEAPP_ZK_NODE);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ReloadHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ReloadHandler.java
new file mode 100644
index 00000000000..8d7eb8a9d7c
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ReloadHandler.java
@@ -0,0 +1,26 @@
+// 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.server;
+
+import com.yahoo.config.provision.ApplicationId;
+
+/**
+ * Interface representing a reload handler.
+ *
+ * @author lulf
+ * @since 5.1.24
+ */
+public interface ReloadHandler {
+ /**
+ * Reload config with the one contained in the application.
+ *
+ * @param applicationSet The set of applications to set as active.
+ */
+ public void reloadConfig(ApplicationSet applicationSet);
+
+ /**
+ * Remove an application and resources related to it.
+ *
+ * @param applicationId to be removed
+ */
+ public void removeApplication(ApplicationId applicationId);
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ReloadListener.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ReloadListener.java
new file mode 100644
index 00000000000..f519a656c8f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ReloadListener.java
@@ -0,0 +1,51 @@
+// 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.server;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+
+import java.util.Collection;
+
+/**
+ * A ReloadListener is used to signal to a component that config has been
+ * reloaded. It only exists because the RpcServer cannot distinguish between a
+ * successful reload of a new application and a reload of the same application.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public interface ReloadListener {
+
+ /**
+ * Signal the listener that config has been reloaded.
+ *
+ * @param tenant Name of tenant for which config was reloaded.
+ * @param application the {@link com.yahoo.vespa.config.server.application.Application} that will be reloaded
+ */
+ public void configReloaded(TenantName tenant, ApplicationSet application);
+
+ /**
+ * Signal the listener that hosts used by by a particular tenant.
+ *
+ * @param tenant Name of tenant.
+ * @param newHosts a {@link Collection} of hosts used by tenant.
+ */
+ void hostsUpdated(TenantName tenant, Collection<String> newHosts);
+
+ /**
+ * Verify that given hosts are available for use by tenant.
+ * TODO: Does not belong here...
+ *
+ * @param tenant tenant that wants to allocate hosts.
+ * @param newHosts a {@link java.util.Collection} of hosts that tenant wants to allocate.
+ * @throws java.lang.IllegalArgumentException if one or more of the hosts are in use by another tenant.
+ */
+ void verifyHostsAreAvailable(TenantName tenant, Collection<String> newHosts);
+
+ /**
+ * Notifies listener that application with id {@link ApplicationId} has been removed.
+ *
+ * @param applicationId The {@link ApplicationId} of the removed application.
+ */
+ void applicationRemoved(ApplicationId applicationId);
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/RequestHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/RequestHandler.java
new file mode 100644
index 00000000000..691035839bb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/RequestHandler.java
@@ -0,0 +1,81 @@
+// 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.server;
+
+import java.util.Optional;
+import java.util.Set;
+
+import com.yahoo.config.provision.Version;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.GetConfigRequest;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+
+/**
+ * Instances of this can serve misc config related requests
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public interface RequestHandler {
+
+ /**
+ * Resolves a config. Mandatory subclass hook for getConfig().
+ * @param appId The application id to use
+ * @param req a config request
+ * @param vespaVersion vespa version
+ * @return The resolved config if it exists, else null.
+ */
+ public ConfigResponse resolveConfig(ApplicationId appId, GetConfigRequest req, Optional<Version> vespaVersion);
+
+ /**
+ * Lists all configs (name, configKey) in the config model.
+ * @param appId application id to use
+ * @param vespaVersion optional vespa version
+ * @param recursive If true descend into all levels
+ * @return set of keys
+ */
+ public Set<ConfigKey<?>> listConfigs(ApplicationId appId, Optional<Version> vespaVersion, boolean recursive);
+
+ /**
+ * Lists all configs (name, configKey) of the given key. The config id of the key is interpreted as a prefix to match.
+ * @param appId application id to use
+ * @param vespaVersion optional vespa version
+ * @param key def key to match
+ * @param recursive If true descend into all levels
+ * @return set of keys
+ */
+ public Set<ConfigKey<?>> listNamedConfigs(ApplicationId appId, Optional<Version> vespaVersion, ConfigKey<?> key, boolean recursive);
+
+ /**
+ * Lists all available configs produced
+ * @param appId application id to use
+ * @param vespaVersion optional vespa version
+ * @return set of keys
+ */
+ public Set<ConfigKey<?>> allConfigsProduced(ApplicationId appId, Optional<Version> vespaVersion);
+
+ /**
+ * List all config ids present
+ * @param appId application id to use
+ * @param vespaVersion optional vespa version
+ * @return a Set containing all config ids available
+ */
+ public Set<String> allConfigIds(ApplicationId appId, Optional<Version> vespaVersion);
+
+ /**
+ * True if application loaded
+ * @param appId The application id to use
+ * @param vespaVersion optional vespa version
+ * @return true if app loaded
+ */
+ boolean hasApplication(ApplicationId appId, Optional<Version> vespaVersion);
+
+ /**
+ * Resolve {@link ApplicationId} for a given hostname. Returns a default {@link ApplicationId} if no applications
+ * are found for that host.
+ *
+ * @param hostName hostname of client.
+ * @return an {@link ApplicationId} instance.
+ */
+ ApplicationId resolveApplicationId(String hostName);
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/RotationsCache.java b/configserver/src/main/java/com/yahoo/vespa/config/server/RotationsCache.java
new file mode 100644
index 00000000000..2a686e2dee3
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/RotationsCache.java
@@ -0,0 +1,69 @@
+// 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.server;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Rotation;
+import com.yahoo.path.Path;
+
+import com.yahoo.vespa.curator.Curator;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Rotations for an application. Persisted in ZooKeeper.
+ *
+ * @author musum
+ */
+public class RotationsCache {
+
+ private final Path path;
+ private final Curator curator;
+
+ public RotationsCache(Curator curator, Path tenantPath) {
+ this.curator = curator;
+ this.path = tenantPath.append("rotationsCache/");
+ }
+
+ public Set<Rotation> readRotationsFromZooKeeper(ApplicationId applicationId) {
+ ObjectMapper objectMapper = new ObjectMapper();
+ Path fullPath = path.append(applicationId.serializedForm());
+ Set<Rotation> ret = new LinkedHashSet<>();
+ try {
+ if (curator != null && curator.exists(fullPath)) {
+ byte[] data = curator.framework().getData().forPath(fullPath.getAbsolute());
+ if (data.length > 0) {
+ Set<String> rotationIds = objectMapper.readValue(data, new TypeReference<Set<String>>() {
+ });
+ ret.addAll(rotationIds.stream().map(Rotation::new).collect(Collectors.toSet()));
+ }
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Error reading rotations from ZooKeeper (" + fullPath + ")", e);
+ }
+ return ret;
+ }
+
+ public void writeRotationsToZooKeeper(ApplicationId applicationId, Set<Rotation> rotations) {
+ if (rotations.size() > 0) {
+ final ObjectMapper objectMapper = new ObjectMapper();
+ final Path cachePath = path.append(applicationId.serializedForm());
+ final String absolutePath = cachePath.getAbsolute();
+ try {
+ curator.create(cachePath);
+ final Set<String> rotationIds = rotations.stream().map(Rotation::getId).collect(Collectors.toSet());
+ final byte[] data = objectMapper.writeValueAsBytes(rotationIds);
+ curator.framework().setData().forPath(absolutePath, data);
+ } catch (Exception e) {
+ throw new RuntimeException("Error writing rotations to ZooKeeper (" + absolutePath + ")", e);
+ }
+ }
+ }
+
+ public void deleteRotationFromZooKeeper(ApplicationId applicationId) {
+ curator.delete(path.append(applicationId.serializedForm()));
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/RpcServer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/RpcServer.java
new file mode 100644
index 00000000000..f49268dd800
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/RpcServer.java
@@ -0,0 +1,407 @@
+// 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.server;
+
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.concurrent.ThreadFactoryFactory;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Version;
+import com.yahoo.jrt.Acceptor;
+import com.yahoo.jrt.Int32Value;
+import com.yahoo.jrt.ListenFailedException;
+import com.yahoo.jrt.Method;
+import com.yahoo.jrt.Request;
+import com.yahoo.jrt.Spec;
+import com.yahoo.jrt.StringValue;
+import com.yahoo.jrt.Supervisor;
+import com.yahoo.jrt.Transport;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.ErrorCode;
+import com.yahoo.vespa.config.JRTMethods;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.protocol.JRTConfigRequest;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequest;
+import com.yahoo.vespa.config.protocol.JRTServerConfigRequestV3;
+import com.yahoo.vespa.config.protocol.Trace;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdaterFactory;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletionService;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorCompletionService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+/**
+ * An RPC server class that handles the config protocol RPC method "getConfigV3".
+ * Mandatory hooks need to be implemented by subclasses.
+ *
+ * @author <a href="musum@yahoo-inc.com">Harald Musum</a>
+ */
+public class RpcServer implements Runnable, ReloadListener, TenantListener {
+
+ static final int TRACELEVEL = 6;
+ static final int TRACELEVEL_DEBUG = 9;
+ private static final String THREADPOOL_NAME = "rpcserver worker pool";
+ private static final long SHUTDOWN_TIMEOUT = 60;
+ private final Supervisor supervisor = new Supervisor(new Transport());
+ private Spec spec = null;
+ private boolean running = false;
+ private final boolean useRequestVersion;
+ private final boolean hostedVespa;
+
+ private static final Logger log = Logger.getLogger(RpcServer.class.getName());
+
+ final DelayedConfigResponses delayedConfigResponses;
+
+ private final HostRegistry<TenantName> hostRegistry;
+ private final Map<TenantName, TenantHandlerProvider> tenantProviders = new ConcurrentHashMap<>();
+ private final SuperModelController superModelController;
+ private final MetricUpdater metrics;
+ private final MetricUpdaterFactory metricUpdaterFactory;
+
+ private final ThreadPoolExecutor executorService;
+ private volatile boolean allTenantsLoaded = false;
+
+ /**
+ * Creates an RpcServer listening on the specified <code>port</code>.
+ *
+ * @param config The config to use for setting up this server
+ */
+ @Inject
+ public RpcServer(ConfigserverConfig config, SuperModelController superModelController, MetricUpdaterFactory metrics, HostRegistries hostRegistries) {
+ this.superModelController = superModelController;
+ this.metricUpdaterFactory = metrics;
+ this.supervisor.setMaxOutputBufferSize(config.maxoutputbuffersize());
+ this.metrics = metrics.getOrCreateMetricUpdater(Collections.<String, String>emptyMap());
+ BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(config.maxgetconfigclients());
+ executorService = new ThreadPoolExecutor(config.numthreads(), config.numthreads(), 0, TimeUnit.SECONDS, workQueue, ThreadFactoryFactory.getThreadFactory(THREADPOOL_NAME));
+ delayedConfigResponses = new DelayedConfigResponses(this, config.numDelayedResponseThreads());
+ spec = new Spec(null, config.rpcport());
+ hostRegistry = hostRegistries.getTenantHostRegistry();
+ this.useRequestVersion = config.useVespaVersionInRequest();
+ this.hostedVespa = config.hostedVespa();
+ setUpHandlers();
+ }
+
+ /**
+ * Handles RPC method "config.v3.getConfig" requests.
+ * Uses the template pattern to call methods in classes that extend RpcServer.
+ *
+ * @param req a Request
+ */
+ @SuppressWarnings({"UnusedDeclaration"})
+ public final void getConfigV3(Request req) {
+ if (log.isLoggable(LogLevel.SPAM)) {
+ log.log(LogLevel.SPAM, "getConfigV3");
+ }
+ req.detach();
+ JRTServerConfigRequestV3 request = JRTServerConfigRequestV3.createFromRequest(req);
+ addToRequestQueue(request);
+ }
+
+ /**
+ * Returns 0 if server is alive.
+ *
+ * @param req a Request
+ */
+ @SuppressWarnings("UnusedDeclaration")
+ 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) {
+ req.returnValues().add(new StringValue("Delayed responses queue size: " + delayedConfigResponses.size()));
+ }
+
+ public void run() {
+ log.log(LogLevel.DEBUG, "Ready for requests on " + spec);
+ try {
+ Acceptor acceptor = supervisor.listen(spec);
+ running = true;
+ supervisor.transport().join();
+ acceptor.shutdown().join();
+ } catch (ListenFailedException e) {
+ stop();
+ throw new RuntimeException("Could not listen at " + spec, e);
+ }
+ running = false;
+ }
+
+ public void stop() {
+ executorService.shutdown();
+ try {
+ executorService.awaitTermination(SHUTDOWN_TIMEOUT, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.interrupted(); // Ignore and continue shutdown.
+ }
+ delayedConfigResponses.stop();
+ supervisor.transport().shutdown().join();
+ }
+
+ /**
+ * Set up RPC method handlers.
+ */
+ private void setUpHandlers() {
+ // The getConfig method in this class will handle RPC calls for getting config
+ getSupervisor().addMethod(JRTMethods.createConfigV3GetConfigMethod(this, "getConfigV3"));
+ getSupervisor().addMethod(new Method("ping", "", "i",
+ this, "ping")
+ .methodDesc("ping")
+ .returnDesc(0, "ret code", "return code, 0 is OK"));
+ getSupervisor().addMethod(new Method("printStatistics", "", "s",
+ this, "printStatistics")
+ .methodDesc("printStatistics")
+ .returnDesc(0, "statistics", "Statistics for server"));
+ }
+
+ public boolean isRunning() {
+ return running;
+ }
+
+ /**
+ * Checks all delayed responses for config changes and waits until all has been answered.
+ * This method should be called when config is reloaded in the server.
+ */
+ @Override
+ public void configReloaded(TenantName tenant, ApplicationSet applicationSet) {
+ final ApplicationId applicationId = applicationSet.getId();
+ configReloaded(delayedConfigResponses.drainQueue(applicationId), Tenants.logPre(applicationId));
+ reloadSuperModel(tenant, applicationSet);
+ }
+
+ private void reloadSuperModel(TenantName tenant, ApplicationSet applicationSet) {
+ superModelController.reloadConfig(tenant, applicationSet);
+ configReloaded(delayedConfigResponses.drainQueue(ApplicationId.global()), Tenants.logPre(ApplicationId.global()));
+ }
+
+ private void configReloaded(List<DelayedConfigResponses.DelayedConfigResponse> responses, String logPre) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, logPre + "Start of configReload: " + responses.size() + " requests on delayed requests queue");
+ }
+ int responsesSent = 0;
+ CompletionService<Boolean> completionService = new ExecutorCompletionService<>(executorService);
+ while (!responses.isEmpty()) {
+ DelayedConfigResponses.DelayedConfigResponse delayedConfigResponse = responses.remove(0);
+ // Discard the ones that we have already answered
+ // Doing cancel here deals with the case where the timer is already running or has not run, so
+ // there is no need for any extra check.
+ if (delayedConfigResponse.cancel()) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ logRequestDebug(LogLevel.DEBUG, logPre + "Timer cancelled for ", delayedConfigResponse.request);
+ }
+ // Do not wait for this request if we were unable to execute
+ if (addToRequestQueue(delayedConfigResponse.request, false, completionService)) {
+ responsesSent++;
+ }
+ } else {
+ log.log(LogLevel.DEBUG, logPre + "Timer already cancelled or finished or never scheduled");
+ }
+ }
+
+ for (int i = 0; i < responsesSent; i++) {
+
+ try {
+ completionService.take();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ log.log(LogLevel.DEBUG, logPre + "Finished reloading " + responsesSent + " requests");
+ }
+
+ private void logRequestDebug(LogLevel level, String message, JRTServerConfigRequest request) {
+ if (log.isLoggable(level)) {
+ log.log(level, message + request.getShortDescription());
+ }
+ }
+
+ @Override
+ public void hostsUpdated(TenantName tenant, Collection<String> newHosts) {
+ log.log(LogLevel.DEBUG, "Updating hosts in tenant host registry '" + hostRegistry + "' with " + newHosts);
+ hostRegistry.update(tenant, newHosts);
+ }
+
+ @Override
+ public void verifyHostsAreAvailable(TenantName tenant, Collection<String> newHosts) {
+ hostRegistry.verifyHosts(tenant, newHosts);
+ }
+
+ @Override
+ public void applicationRemoved(ApplicationId applicationId) {
+ superModelController.removeApplication(applicationId);
+ configReloaded(delayedConfigResponses.drainQueue(applicationId), Tenants.logPre(applicationId));
+ configReloaded(delayedConfigResponses.drainQueue(ApplicationId.global()), Tenants.logPre(ApplicationId.global()));
+ }
+
+ public Spec getSpec() {
+ return spec;
+ }
+
+ public void respond(JRTServerConfigRequest request) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Trace at request return:\n" + request.getRequestTrace().toString());
+ }
+ request.getRequest().returnRequest();
+ }
+
+ /**
+ * Returns the tenant for this request, empty if there is no tenant for this request
+ * (which on hosted Vespa means that the requesting host is not currently active for any tenant)
+ */
+ public Optional<TenantName> resolveTenant(JRTServerConfigRequest request, Trace trace) {
+ if ("*".equals(request.getConfigKey().getConfigId())) return Optional.of(ApplicationId.global().tenant());
+ String hostname = request.getClientHostName();
+ TenantName tenant = hostRegistry.getKeyForHost(hostname);
+ if (tenant == null) {
+ if (GetConfigProcessor.logDebug(trace)) {
+ String message = "Did not find tenant for host '" + hostname + "', using " + TenantName.defaultName();
+ log.log(LogLevel.DEBUG, message);
+ log.log(LogLevel.DEBUG, "hosts in host registry: " + hostRegistry.getAllHosts());
+ trace.trace(6, message);
+ }
+ return Optional.empty();
+ }
+ return Optional.of(tenant);
+ }
+
+ public ConfigResponse resolveConfig(JRTServerConfigRequest request, GetConfigContext context, Optional<Version> vespaVersion) {
+ Trace trace = context.trace();
+ if (trace.shouldTrace(TRACELEVEL)) {
+ trace.trace(TRACELEVEL, "RpcServer.resolveConfig()");
+ }
+ RequestHandler handler = context.requestHandler();
+ return handler.resolveConfig(context.applicationId(), request, vespaVersion);
+
+ }
+
+ protected Supervisor getSupervisor() {
+ return supervisor;
+ }
+
+ Boolean addToRequestQueue(JRTServerConfigRequest request) {
+ return addToRequestQueue(request, false, null);
+ }
+
+ public Boolean addToRequestQueue(JRTServerConfigRequest request, boolean forceResponse, CompletionService<Boolean> completionService) {
+ // It's no longer delayed if we get here
+ request.setDelayedResponse(false);
+ //ConfigDebug.logDebug(log, System.currentTimeMillis(), request.getConfigKey(), "RpcServer.addToRequestQueue()");
+ try {
+ final GetConfigProcessor task = new GetConfigProcessor(this, request, forceResponse);
+ if (completionService == null) {
+ executorService.submit(task);
+ } else {
+ completionService.submit(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ task.run();
+ return true;
+ }
+ });
+ }
+ updateWorkQueueMetrics();
+ return true;
+ } catch (RejectedExecutionException e) {
+ request.addErrorResponse(ErrorCode.INTERNAL_ERROR, "getConfig request queue size is larger than configured max limit");
+ respond(request);
+ return false;
+ }
+ }
+
+ private void updateWorkQueueMetrics() {
+ int queued = executorService.getQueue().size();
+ metrics.setRpcServerQueueSize(queued);
+ }
+
+ /**
+ * Returns the context for this request, or null if the server is not properly set up with handlers
+ */
+ public GetConfigContext createGetConfigContext(Optional<TenantName> optionalTenant, JRTServerConfigRequest request, Trace trace) {
+ if ("*".equals(request.getConfigKey().getConfigId())) {
+ return GetConfigContext.create(ApplicationId.global(), superModelController, trace);
+ }
+ TenantName tenant = optionalTenant.orElse(TenantName.defaultName()); // perhaps needed for non-hosted?
+ if ( ! hasRequestHandler(tenant)) {
+ String msg = Tenants.logPre(tenant) + "Unable to find request handler for tenant. Requested from host '" + request.getClientHostName() + "'";
+ metrics.incUnknownHostRequests();
+ trace.trace(TRACELEVEL, msg);
+ log.log(LogLevel.WARNING, msg);
+ return null;
+ }
+ RequestHandler handler = getRequestHandler(tenant);
+ ApplicationId applicationId = handler.resolveApplicationId(request.getClientHostName());
+ if (trace.shouldTrace(TRACELEVEL_DEBUG)) {
+ trace.trace(TRACELEVEL_DEBUG, "Host '" + request.getClientHostName() + "' should have config from application '" + applicationId + "'");
+ }
+ return GetConfigContext.create(applicationId, handler, trace);
+ }
+
+ private boolean hasRequestHandler(TenantName tenant) {
+ return tenantProviders.containsKey(tenant);
+ }
+
+ private RequestHandler getRequestHandler(TenantName tenant) {
+ if (!tenantProviders.containsKey(tenant)) {
+ throw new IllegalStateException("No request handler for " + tenant);
+ }
+ return tenantProviders.get(tenant).getRequestHandler();
+ }
+
+ public void delayResponse(JRTServerConfigRequest request, GetConfigContext context) {
+ delayedConfigResponses.delayResponse(request, context);
+ }
+
+ @Override
+ public void onTenantDelete(TenantName tenant) {
+ log.log(LogLevel.DEBUG, Tenants.logPre(tenant)+"Tenant deleted, removing request handler and cleaning host registry");
+ if (tenantProviders.containsKey(tenant)) {
+ tenantProviders.remove(tenant);
+ }
+ hostRegistry.removeHostsForKey(tenant);
+ }
+
+ @Override
+ public void onTenantsLoaded() {
+ allTenantsLoaded = true;
+ superModelController.enable();
+ }
+
+ @Override
+ public void onTenantCreate(TenantName tenant, TenantHandlerProvider tenantHandlerProvider) {
+ log.log(LogLevel.DEBUG, Tenants.logPre(tenant)+"Tenant created, adding request handler");
+ tenantProviders.put(tenant, tenantHandlerProvider);
+ }
+
+ /** Returns true only after all tenants are loaded */
+ public boolean allTenantsLoaded() { return allTenantsLoaded; }
+
+ /** Returns true if this rpc server is currently running in a hosted Vespa configuration */
+ public boolean isHostedVespa() { return hostedVespa; }
+
+ MetricUpdaterFactory metricUpdaterFactory() {
+ return metricUpdaterFactory;
+ }
+
+ boolean useRequestVersion() {
+ return useRequestVersion;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ServerCache.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ServerCache.java
new file mode 100644
index 00000000000..8858d368edf
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ServerCache.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.server;
+
+import com.yahoo.vespa.config.ConfigCacheKey;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Cache that holds configs and config definitions. It has separate maps for the separate
+ * "types", for clarity.
+ *
+ * @author vegardh
+ */
+public class ServerCache {
+
+ private final Map<ConfigDefinitionKey, ConfigDefinition> defs = new ConcurrentHashMap<>();
+ /* Legacy user configs from configs/ dir in application package (NB! Only name, not key) */
+ private final Map<String, ConfigPayload> legacyUserCfgs = new ConcurrentHashMap<>();
+
+ // NOTE: The reason we do a double mapping here is to dedup configs that have the same md5.
+ private final Map<ConfigCacheKey, String> md5Sums = new ConcurrentHashMap<>();
+ private final Map<String, ConfigResponse> md5ToConfig = new ConcurrentHashMap<>();
+
+ public void addDef(ConfigDefinitionKey key, ConfigDefinition def) {
+ defs.put(key, def);
+ }
+
+ public void addLegacyUserConfig(String name, ConfigPayload config) {
+ legacyUserCfgs.put(name, config);
+ }
+
+ public ConfigPayload getLegacyUserConfig(String name) {
+ return legacyUserCfgs.get(name);
+ }
+
+ public void put(ConfigCacheKey key, ConfigResponse config, String configMd5) {
+ md5Sums.put(key, configMd5);
+ md5ToConfig.put(configMd5, config);
+ }
+
+ public ConfigResponse get(ConfigCacheKey key) {
+ String md5 = md5Sums.get(key);
+ if (md5 == null) return null;
+ return md5ToConfig.get(md5);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Cache\n");
+ sb.append("defs: ").append(defs.size()).append("\n");
+ sb.append("user cfgs: ").append(legacyUserCfgs.size()).append("\n");
+ sb.append("md5sums: ").append(md5Sums.size()).append("\n");
+ sb.append("md5ToConfig: ").append(md5ToConfig.size()).append("\n");
+
+ return sb.toString();
+ }
+
+ public ConfigDefinition getDef(ConfigDefinitionKey defKey) {
+ return defs.get(defKey);
+ }
+
+ /**
+ * The number of different {@link ConfigResponse} elements
+ * @return elems
+ */
+ public int configElems() {
+ return md5ToConfig.size();
+ }
+
+ /**
+ * The number of different key→checksum mappings
+ * @return elems
+ */
+ public int checkSumElems() {
+ return md5Sums.size();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/StaticConfigDefinitionRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/StaticConfigDefinitionRepo.java
new file mode 100644
index 00000000000..b3921da869a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/StaticConfigDefinitionRepo.java
@@ -0,0 +1,43 @@
+// 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.server;
+
+import com.google.inject.Inject;
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * A global pool of all config definitions that this server knows about. These objects can be shared
+ * by all tenants, as they are not modified.
+ *
+ * @author lulf
+ * @since 5.10
+ */
+public class StaticConfigDefinitionRepo implements ConfigDefinitionRepo {
+
+ private final ConfigDefinitionRepo repo;
+
+ // Only useful in tests that dont need full blown repo.
+ public StaticConfigDefinitionRepo() {
+ this.repo = new ConfigDefinitionRepo() {
+ @Override
+ public Map<ConfigDefinitionKey, ConfigDefinition> getConfigDefinitions() {
+ return Collections.emptyMap();
+ }
+ };
+ }
+
+ @Inject
+ public StaticConfigDefinitionRepo(ConfigServerDB serverDB) {
+ this.repo = new com.yahoo.config.model.application.provider.StaticConfigDefinitionRepo(serverDB.serverdefs());
+ }
+
+ @Override
+ public Map<ConfigDefinitionKey, ConfigDefinition> getConfigDefinitions() {
+ return repo.getConfigDefinitions();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelController.java b/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelController.java
new file mode 100644
index 00000000000..f6b00440e30
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelController.java
@@ -0,0 +1,161 @@
+// 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.server;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.provision.Version;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.GetConfigRequest;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.GenerationCounter;
+import com.yahoo.vespa.config.server.model.SuperModel;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import com.yahoo.cloud.config.ElkConfig;
+
+/**
+ * Controls the lifetime of the {@link SuperModel} and the {@link SuperModelRequestHandler}.
+ *
+ * @author lulf
+ * @since 5.9
+ */
+public class SuperModelController implements RequestHandler {
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(SuperModelController.class.getName());
+ private volatile SuperModelRequestHandler handler;
+ private final GenerationCounter generationCounter;
+ private final Zone zone;
+ private final long masterGeneration;
+ private final ConfigDefinitionRepo configDefinitionRepo;
+ private final ConfigResponseFactory responseFactory;
+ private final ElkConfig elkConfig;
+ private volatile boolean enabled = false;
+
+
+ public SuperModelController(GenerationCounter generationCounter, ConfigDefinitionRepo configDefinitionRepo, ConfigserverConfig configserverConfig, ElkConfig elkConfig) {
+ this.generationCounter = generationCounter;
+ this.configDefinitionRepo = configDefinitionRepo;
+ this.elkConfig = elkConfig;
+ this.masterGeneration = configserverConfig.masterGeneration();
+ this.responseFactory = ConfigResponseFactoryFactory.createFactory(configserverConfig);
+ this.zone = new Zone(configserverConfig);
+ this.handler = createNewHandler(Collections.emptyMap());
+ }
+
+ /**
+ * Signals that config has been reloaded for an {@link com.yahoo.vespa.config.server.application.Application}
+ * belonging to a tenant.
+ *
+ * TODO: This is a bit too complex I think.
+ *
+ * @param tenant Name of tenant owning the application.
+ * @param applicationSet The reloaded set of {@link com.yahoo.vespa.config.server.application.Application}.
+ */
+ public synchronized void reloadConfig(TenantName tenant, ApplicationSet applicationSet) {
+ Map<TenantName, Map<ApplicationId, Application>> newModels = createModelCopy();
+ if (!newModels.containsKey(tenant)) {
+ newModels.put(tenant, new LinkedHashMap<>());
+ }
+ // TODO: Should supermodel care about multiple versions?
+ newModels.get(tenant).put(applicationSet.getId(), applicationSet.getForVersionOrLatest(Optional.empty()));
+ handler = createNewHandler(newModels);
+ }
+
+ public synchronized void removeApplication(ApplicationId applicationId) {
+ Map<TenantName, Map<ApplicationId, Application>> newModels = createModelCopy();
+ if (newModels.containsKey(applicationId.tenant())) {
+ newModels.get(applicationId.tenant()).remove(applicationId);
+ if (newModels.get(applicationId.tenant()).isEmpty()) {
+ newModels.remove(applicationId.tenant());
+ }
+ }
+ handler = createNewHandler(newModels);
+ }
+
+ private SuperModelRequestHandler createNewHandler(Map<TenantName, Map<ApplicationId, Application>> newModels) {
+ long generation = generationCounter.get() + masterGeneration;
+ SuperModel model = new SuperModel(newModels, elkConfig, zone);
+ return new SuperModelRequestHandler(model, configDefinitionRepo, generation, responseFactory);
+ }
+
+ private Map<TenantName, Map<ApplicationId, Application>> getCurrentModels() {
+ if (handler != null) {
+ return handler.getSuperModel().getCurrentModels();
+ } else {
+ return new LinkedHashMap<>();
+ }
+ }
+
+ private Map<TenantName, Map<ApplicationId, Application>> createModelCopy() {
+ Map<TenantName, Map<ApplicationId, Application>> currentModels = getCurrentModels();
+ Map<TenantName, Map<ApplicationId, Application>> newModels = new LinkedHashMap<>();
+ for (Map.Entry<TenantName, Map<ApplicationId, Application>> entry : currentModels.entrySet()) {
+ Map<ApplicationId, Application> appMap = new LinkedHashMap<>();
+ newModels.put(entry.getKey(), appMap);
+ for (Map.Entry<ApplicationId, Application> appEntry : entry.getValue().entrySet()) {
+ appMap.put(appEntry.getKey(), appEntry.getValue());
+ }
+ }
+ return newModels;
+ }
+
+ public SuperModelRequestHandler getHandler() { return handler; }
+
+ @Override
+ public ConfigResponse resolveConfig(ApplicationId appId, GetConfigRequest req, Optional<Version> vespaVersion) {
+ log.log(LogLevel.DEBUG, "SuperModelController resolving " + req + " for app id '" + appId + "'");
+ if (handler != null) {
+ return handler.resolveConfig(req);
+ }
+ return null;
+ }
+
+ public <CONFIGTYPE extends ConfigInstance> CONFIGTYPE getConfig(Class<CONFIGTYPE> configClass, ApplicationId applicationId, String configId) throws IOException {
+ return handler.getConfig(configClass, applicationId, configId);
+ }
+
+ @Override
+ public Set<ConfigKey<?>> listConfigs(ApplicationId appId, Optional<Version> vespaVersion, boolean recursive) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Set<ConfigKey<?>> listNamedConfigs(ApplicationId appId, Optional<Version> vespaVersion, ConfigKey<?> key, boolean recursive) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Set<ConfigKey<?>> allConfigsProduced(ApplicationId appId, Optional<Version> vespaVersion) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Set<String> allConfigIds(ApplicationId appID, Optional<Version> vespaVersion) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean hasApplication(ApplicationId appId, Optional<Version> vespaVersion) {
+ return enabled && appId.equals(ApplicationId.global());
+ }
+
+ @Override
+ public ApplicationId resolveApplicationId(String hostName) {
+ return ApplicationId.global();
+ }
+
+ public void enable() {
+ enabled = true;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelGenerationCounter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelGenerationCounter.java
new file mode 100644
index 00000000000..589363467b0
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelGenerationCounter.java
@@ -0,0 +1,40 @@
+// 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.server;
+
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.GenerationCounter;
+import com.yahoo.vespa.curator.recipes.CuratorCounter;
+import com.yahoo.vespa.curator.Curator;
+
+/**
+ * Distributed global generation counter for the super model.
+ *
+ * @author lulf
+ * @since 5.9
+ */
+public class SuperModelGenerationCounter implements GenerationCounter {
+
+ private static final Path counterPath = Path.fromString("/config/v2/RPC/superModelGeneration");
+ private final CuratorCounter counter;
+
+ public SuperModelGenerationCounter(Curator curator) {
+ this.counter = new CuratorCounter(curator, counterPath.getAbsolute());
+ }
+
+ /**
+ * Increment counter and return next value. This method is thread safe and provides an atomic value
+ * across zookeeper clusters.
+ *
+ * @return incremented counter value.
+ */
+ public synchronized long increment() {
+ return counter.next();
+ }
+
+ /**
+ * @return current counter value.
+ */
+ public synchronized long get() {
+ return counter.get();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelRequestHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelRequestHandler.java
new file mode 100644
index 00000000000..c745379f9cb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelRequestHandler.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.server;
+
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.ConfigurationRuntimeException;
+import com.yahoo.config.codegen.DefParser;
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.GetConfigRequest;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.protocol.DefContent;
+import com.yahoo.vespa.config.server.model.SuperModel;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+/**
+ * Handler for global configs that must be resolved using the global SuperModel instance. Deals with
+ * reloading of config as well.
+ *
+ * @author lulf
+ * @since 5.9
+ */
+public class SuperModelRequestHandler {
+ private final SuperModel model;
+ private final long generation;
+ private final ConfigDefinitionRepo configDefinitionRepo;
+ private final ConfigResponseFactory responseFactory;
+
+ public SuperModelRequestHandler(SuperModel model, ConfigDefinitionRepo configDefinitionRepo, long generation, ConfigResponseFactory responseFactory) {
+ this.model = model;
+ this.configDefinitionRepo = configDefinitionRepo;
+ this.generation = generation;
+ this.responseFactory = responseFactory;
+ }
+
+ /**
+ * Resolves global config for given request.
+ *
+ * @param request The {@link com.yahoo.vespa.config.GetConfigRequest} to find config for.
+ * @return a {@link com.yahoo.vespa.config.protocol.ConfigResponse} containing the response for this request.
+ * @throws java.lang.IllegalArgumentException if no such config was found.
+ */
+ public ConfigResponse resolveConfig(GetConfigRequest request) {
+ ConfigKey<?> configKey = request.getConfigKey();
+ InnerCNode targetDef = getConfigDefinition(request.getConfigKey(), request.getDefContent());
+ try {
+ ConfigPayload payload = model.getConfig(configKey);
+ return responseFactory.createResponse(payload, targetDef, generation);
+ } catch (IOException e) {
+ throw new ConfigurationRuntimeException("Unable to resolve config", e);
+ }
+ }
+
+ private InnerCNode getConfigDefinition(ConfigKey<?> configKey, DefContent defContent) {
+ if (defContent.isEmpty()) {
+ ConfigDefinitionKey configDefinitionKey = new ConfigDefinitionKey(configKey.getName(), configKey.getNamespace());
+ ConfigDefinition configDefinition = configDefinitionRepo.getConfigDefinitions().get(configDefinitionKey);
+ if (configDefinition == null) {
+ throw new UnknownConfigDefinitionException("Unable to find config definition for '" + configKey.getNamespace() + "." + configKey.getName());
+ }
+ return configDefinition.getCNode();
+ } else {
+ DefParser dParser = new DefParser(configKey.getName(), new StringReader(defContent.asString()));
+ return dParser.getTree();
+ }
+ }
+
+ SuperModel getSuperModel() {
+ return model;
+ }
+
+ long getGeneration() { return generation; }
+
+ public <CONFIGTYPE extends ConfigInstance> CONFIGTYPE getConfig(Class<CONFIGTYPE> configClass, ApplicationId applicationId, String configId) throws IOException {
+ return model.getConfig(configClass, applicationId, configId);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/Tenant.java b/configserver/src/main/java/com/yahoo/vespa/config/server/Tenant.java
new file mode 100644
index 00000000000..cabf4bed323
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/Tenant.java
@@ -0,0 +1,182 @@
+// 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.server;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Deployer;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+import com.yahoo.vespa.config.server.session.RemoteSessionRepo;
+import com.yahoo.vespa.config.server.session.SessionFactory;
+import com.yahoo.vespa.curator.Curator;
+
+import java.time.Duration;
+import java.util.logging.Logger;
+
+/**
+ * Contains all tenant-level components for a single tenant, dealing with editing sessions and
+ * applications for a single tenant.
+ *
+ * @author vegardh
+ * @author lulf
+ * @since 5.1.26
+ */
+public class Tenant implements TenantHandlerProvider {
+
+ private static final Logger log = Logger.getLogger(Tenant.class.getName());
+ static final String SESSIONS = "sessions";
+ static final String APPLICATIONS = "applications";
+
+ private final TenantName name;
+ private final RemoteSessionRepo remoteSessionRepo;
+ private final Path path;
+ private final SessionFactory sessionFactory;
+ private final LocalSessionRepo localSessionRepo;
+ private final ApplicationRepo applicationRepo;
+ private final ActivateLock activateLock;
+ private final RequestHandler requestHandler;
+ private final ReloadHandler reloadHandler;
+ private final TenantFileSystemDirs tenantFileSystemDirs;
+ private final Curator curator;
+
+ Tenant(TenantName name,
+ Path path,
+ SessionFactory sessionFactory,
+ LocalSessionRepo localSessionRepo,
+ RemoteSessionRepo remoteSessionRepo,
+ RequestHandler requestHandler,
+ ReloadHandler reloadHandler,
+ ApplicationRepo applicationRepo,
+ Curator curator,
+ TenantFileSystemDirs tenantFileSystemDirs) {
+ this.name = name;
+ this.path = path;
+ this.requestHandler = requestHandler;
+ this.reloadHandler = reloadHandler;
+ this.remoteSessionRepo = remoteSessionRepo;
+ this.sessionFactory = sessionFactory;
+ this.localSessionRepo = localSessionRepo;
+ this.activateLock = new ActivateLock(curator, path);
+ this.applicationRepo = applicationRepo;
+ this.tenantFileSystemDirs = tenantFileSystemDirs;
+ this.curator = curator;
+ }
+
+ /**
+ * The reload handler for this
+ *
+ * @return handler
+ */
+ public ReloadHandler getReloadHandler() {
+ return reloadHandler;
+ }
+
+ /**
+ * The request handler for this
+ *
+ * @return handler
+ */
+ public RequestHandler getRequestHandler() {
+ return requestHandler;
+ }
+
+ /**
+ * The RemoteSessionRepo for this
+ *
+ * @return repo
+ */
+ public RemoteSessionRepo getRemoteSessionRepo() {
+ return remoteSessionRepo;
+ }
+
+ public TenantName getName() {
+ return name;
+ }
+
+ public Path getPath() {
+ return path;
+ }
+
+ public SessionFactory getSessionFactory() {
+ return sessionFactory;
+ }
+
+ public LocalSessionRepo getLocalSessionRepo() {
+ return localSessionRepo;
+ }
+
+ /**
+ * The activation lock for this
+ * @return lock
+ */
+ public ActivateLock getActivateLock() {
+ return activateLock;
+ }
+
+ @Override
+ public String toString() {
+ return getName().value();
+ }
+
+ public ApplicationRepo getApplicationRepo() {
+ return applicationRepo;
+ }
+
+ public Curator getCurator() {
+ return curator;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof Tenant)) {
+ return false;
+ }
+ Tenant that = (Tenant) other;
+ return name.equals(that.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ /**
+ * Closes any watchers, thread pools that may react to changes in tenant state. Also removes any local state
+ * in filesystem.
+ */
+ public void close() {
+ tenantFileSystemDirs.delete();
+ remoteSessionRepo.close();
+ applicationRepo.close();
+ }
+
+ /**
+ * Deletes a tenant from ZooKeeper and filesystem.
+ */
+ public void delete() {
+ localSessionRepo.deleteAllSessions();
+ curator.delete(path);
+ }
+
+ public void redeployApplications(Deployer deployer) {
+ // TODO: Configurable timeout
+ applicationRepo.listApplications().stream()
+ .forEach(applicationId -> redeployApplication(applicationId, deployer));
+ }
+
+ private void redeployApplication(ApplicationId applicationId, Deployer deployer) {
+ try {
+ log.log(LogLevel.DEBUG, "Redeploying " + applicationId);
+ deployer.deployFromLocalActive(applicationId, Duration.ofMinutes(30))
+ .ifPresent(deployment -> {
+ deployment.prepare();
+ deployment.activate();
+ });
+ } catch (Exception e) {
+ log.log(LogLevel.ERROR, "Redeploying " + applicationId + " failed", e);
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/TenantBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantBuilder.java
new file mode 100644
index 00000000000..78ec368f2fb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantBuilder.java
@@ -0,0 +1,201 @@
+// 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.server;
+
+import com.yahoo.concurrent.ThreadFactoryFactory;
+import com.yahoo.path.Path;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.application.ZKApplicationRepo;
+import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.vespa.config.server.session.*;
+import com.yahoo.vespa.config.server.zookeeper.SessionCounter;
+import com.yahoo.vespa.defaults.Defaults;
+
+import java.io.File;
+import java.time.Clock;
+import java.util.Collections;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Builder for helping out with tenant creation. Each of a tenants dependencies may be overridden for testing.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class TenantBuilder {
+ private final Path tenantPath;
+ private final GlobalComponentRegistry componentRegistry;
+ private final TenantName tenant;
+ private final Path sessionsPath;
+ private RemoteSessionRepo remoteSessionRepo;
+ private LocalSessionRepo localSessionRepo;
+ private SessionFactory sessionFactory;
+ private LocalSessionLoader localSessionLoader;
+ private ApplicationRepo applicationRepo;
+ private SessionCounter sessionCounter;
+ private ReloadHandler reloadHandler;
+ private RequestHandler requestHandler;
+ private RemoteSessionFactory remoteSessionFactory;
+ private TenantFileSystemDirs tenantFileSystemDirs;
+ private HostValidator<ApplicationId> hostValidator;
+
+ private TenantBuilder(GlobalComponentRegistry componentRegistry, TenantName tenant, Path zkPath) {
+ this.componentRegistry = componentRegistry;
+ this.tenantPath = zkPath;
+ this.tenant = tenant;
+ this.sessionsPath = tenantPath.append(Tenant.SESSIONS);
+ }
+
+ public static TenantBuilder create(GlobalComponentRegistry componentRegistry, TenantName tenant, Path zkPath) {
+ return new TenantBuilder(componentRegistry, tenant, zkPath);
+ }
+
+ public TenantBuilder withSessionFactory(SessionFactory sessionFactory) {
+ this.sessionFactory = sessionFactory;
+ return this;
+ }
+
+ public TenantBuilder withLocalSessionRepo(LocalSessionRepo localSessionRepo) {
+ this.localSessionRepo = localSessionRepo;
+ return this;
+ }
+
+ public TenantBuilder withRemoteSessionRepo(RemoteSessionRepo remoteSessionRepo) {
+ this.remoteSessionRepo = remoteSessionRepo;
+ return this;
+ }
+
+ public TenantBuilder withApplicationRepo(ApplicationRepo applicationRepo) {
+ this.applicationRepo = applicationRepo;
+ return this;
+ }
+
+ public TenantBuilder withRequestHandler(RequestHandler requestHandler) {
+ this.requestHandler = requestHandler;
+ return this;
+ }
+
+ public TenantBuilder withReloadHandler(ReloadHandler reloadHandler) {
+ this.reloadHandler = reloadHandler;
+ return this;
+ }
+
+ /**
+ * Create a real tenant from the properties given by this builder.
+ *
+ * @return a new {@link Tenant} instance.
+ * @throws Exception if building fails
+ */
+ public Tenant build() throws Exception {
+ createTenantRequestHandler();
+ createApplicationRepo();
+ createRemoteSessionFactory();
+ createRemoteSessionRepo();
+ createSessionCounter();
+ createServerDbDirs();
+ createSessionFactory();
+ createLocalSessionRepo();
+ return new Tenant(tenant,
+ tenantPath,
+ sessionFactory,
+ localSessionRepo,
+ remoteSessionRepo,
+ requestHandler,
+ reloadHandler,
+ applicationRepo,
+ componentRegistry.getCurator(),
+ tenantFileSystemDirs);
+ }
+
+ private void createLocalSessionRepo() {
+ if (localSessionRepo == null) {
+ localSessionRepo = new LocalSessionRepo(tenantFileSystemDirs, localSessionLoader, applicationRepo, Clock.systemUTC(), componentRegistry.getConfigserverConfig().sessionLifetime());
+ }
+ }
+
+ private void createSessionFactory() {
+ if (sessionFactory == null || localSessionLoader == null) {
+ SessionFactoryImpl impl = new SessionFactoryImpl(componentRegistry, sessionCounter, sessionsPath, applicationRepo, tenantFileSystemDirs, hostValidator, tenant);
+ if (sessionFactory == null) {
+ sessionFactory = impl;
+ }
+ if (localSessionLoader == null) {
+ localSessionLoader = impl;
+ }
+ }
+ }
+
+ private void createApplicationRepo() {
+ if (applicationRepo == null) {
+ applicationRepo = ZKApplicationRepo.create(componentRegistry.getCurator(), tenantPath.append(Tenant.APPLICATIONS), reloadHandler, tenant);
+ }
+ }
+
+ private void createSessionCounter() {
+ if (sessionCounter == null) {
+ sessionCounter = new SessionCounter(componentRegistry.getCurator(), tenantPath, sessionsPath);
+ }
+ }
+
+ private void createTenantRequestHandler() {
+ if (requestHandler == null || reloadHandler == null) {
+ TenantRequestHandler impl = new TenantRequestHandler(componentRegistry.getMetrics(),
+ tenant,
+ Collections.singletonList(componentRegistry.getReloadListener()),
+ ConfigResponseFactoryFactory.createFactory(componentRegistry.getConfigserverConfig()),
+ componentRegistry.getHostRegistries());
+ if (hostValidator == null) {
+ this.hostValidator = impl;
+ }
+ if (requestHandler == null) {
+ requestHandler = impl;
+ }
+ if (reloadHandler == null) {
+ reloadHandler = impl;
+ }
+ }
+ }
+
+ private void createRemoteSessionFactory() {
+ if (remoteSessionFactory == null) {
+ remoteSessionFactory = new RemoteSessionFactory(
+ componentRegistry,
+ sessionsPath,
+ tenant);
+ }
+ }
+
+ private void createRemoteSessionRepo() throws Exception {
+ if (remoteSessionRepo == null) {
+ remoteSessionRepo = RemoteSessionRepo.create(componentRegistry.getCurator(),
+ remoteSessionFactory,
+ reloadHandler,
+ sessionsPath,
+ applicationRepo,
+ componentRegistry.getMetrics().getOrCreateMetricUpdater(Metrics.createDimensions(tenant)),
+ createSingleThreadedExecutorService(RemoteSessionRepo.class.getName()));
+ }
+ }
+
+ private ExecutorService createSingleThreadedExecutorService(String executorName) {
+ return Executors.newSingleThreadExecutor(ThreadFactoryFactory.getThreadFactory(executorName + "-" + tenant.value()));
+ }
+
+ private void createServerDbDirs() {
+ if (tenantFileSystemDirs == null) {
+ tenantFileSystemDirs = new TenantFileSystemDirs(new File(Defaults.getDefaults().underVespaHome(componentRegistry.getServerDB().getConfigserverConfig().configServerDBDir())), tenant);
+ }
+ }
+
+
+ public LocalSessionRepo getLocalSessionRepo() {
+ return localSessionRepo;
+ }
+
+ public ApplicationRepo getApplicationRepo() {
+ return applicationRepo;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/TenantDebugger.java b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantDebugger.java
new file mode 100644
index 00000000000..ab5bf6db7b9
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantDebugger.java
@@ -0,0 +1,38 @@
+// 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.server;
+
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.curator.Curator;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.recipes.cache.TreeCache;
+import org.apache.curator.framework.recipes.cache.TreeCacheEvent;
+import org.apache.curator.framework.recipes.cache.TreeCacheListener;
+
+import java.util.logging.Logger;
+
+/**
+ * For debugging tenant issues in configserver. Activate by loading component.
+ *
+ * @author lulf
+ */
+public class TenantDebugger implements TreeCacheListener {
+ private final TreeCache cache;
+ private static final Logger log = Logger.getLogger(TenantDebugger.class.getName());
+
+ public TenantDebugger(Curator curator) throws Exception {
+ cache = new TreeCache(curator.framework(), "/config/v2/tenants");
+ cache.getListenable().addListener(this);
+ cache.start();
+ }
+
+ @Override
+ public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception {
+ switch (event.getType()) {
+ case NODE_ADDED:
+ case NODE_REMOVED:
+ case NODE_UPDATED:
+ log.log(LogLevel.INFO, event.toString());
+ break;
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/TenantHandlerProvider.java b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantHandlerProvider.java
new file mode 100644
index 00000000000..a9321709dd3
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantHandlerProvider.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.server;
+
+/**
+ * Represents something that can provide request and reload handlers of a tenant.
+ *
+ * @author lulf
+ * @since 5.3
+ */
+public interface TenantHandlerProvider {
+
+ RequestHandler getRequestHandler();
+ ReloadHandler getReloadHandler();
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/TenantListener.java b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantListener.java
new file mode 100644
index 00000000000..7037e24ff5d
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantListener.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.server;
+
+import com.yahoo.config.provision.TenantName;
+
+/**
+ * Interface for something that listens for created and deleted tenants.
+ *
+ * @author lulf
+ * @since 5.8
+ */
+public interface TenantListener {
+ /**
+ * Called whenever a new tenant is created.
+ *
+ * @param tenant name of newly created tenant.
+ * @param provider provider of request and reload handlers for new tenant.
+ */
+ public void onTenantCreate(TenantName tenant, TenantHandlerProvider provider);
+
+ /**
+ * Called whenever a tenant is deleted.
+ *
+ * @param tenant name of deleted tenant.
+ */
+ public void onTenantDelete(TenantName tenant);
+
+ /**
+ * Called when all tenants have been loaded at startup.
+ */
+ void onTenantsLoaded();
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/TenantRequestHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantRequestHandler.java
new file mode 100644
index 00000000000..c4e77abc6e9
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantRequestHandler.java
@@ -0,0 +1,213 @@
+// 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.server;
+
+import java.util.*;
+
+import com.yahoo.config.provision.Version;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.*;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.http.NotFoundException;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+
+/**
+ * A per tenant request handler, for handling reload (activate application) and getConfig requests for
+ * a set of applications belonging to a tenant.
+ *
+ * @author Harald Musum
+ * @since 5.1
+ */
+public class TenantRequestHandler implements RequestHandler, ReloadHandler, HostValidator<ApplicationId> {
+
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(TenantRequestHandler.class.getName());
+
+ private final Metrics metrics;
+ private final TenantName tenant;
+ private final List<ReloadListener> reloadListeners;
+ private final ConfigResponseFactory responseFactory;
+
+ private final HostRegistry<ApplicationId> hostRegistry;
+ private final ApplicationMapper applicationMapper = new ApplicationMapper();
+ private final MetricUpdater tenantMetricUpdater;
+
+ public TenantRequestHandler(Metrics metrics,
+ TenantName tenant,
+ List<ReloadListener> reloadListeners,
+ ConfigResponseFactory responseFactory,
+ HostRegistries hostRegistries) {
+ this.metrics = metrics;
+ this.tenant = tenant;
+ this.reloadListeners = reloadListeners;
+ this.responseFactory = responseFactory;
+ tenantMetricUpdater = metrics.getOrCreateMetricUpdater(Metrics.createDimensions(tenant));
+ hostRegistry = hostRegistries.createApplicationHostRegistry(tenant);
+ }
+
+ /**
+ * Gets a config for the given app, or null if not found
+ */
+ @Override
+ public ConfigResponse resolveConfig(ApplicationId appId, GetConfigRequest req, Optional<Version> vespaVersion) {
+ Application application = getApplication(appId, vespaVersion);
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, Tenants.logPre(appId) + "Resolving for tenant '" + tenant + "' with handler for application '" + application + "'");
+ }
+ return application.resolveConfig(req, responseFactory);
+ }
+
+ // For testing only
+ long getApplicationGeneration(ApplicationId appId, Optional<Version> vespaVersion) {
+ Application application = getApplication(appId, vespaVersion);
+ return application.getApplicationGeneration();
+ }
+
+ private void notifyReloadListeners(ApplicationSet applicationSet) {
+ for (ReloadListener reloadListener : reloadListeners) {
+ reloadListener.hostsUpdated(tenant, hostRegistry.getAllHosts());
+ reloadListener.configReloaded(tenant, applicationSet);
+ }
+ }
+
+ /**
+ * Activates the config of the given app. Notifies listeners
+ * @param applicationSet the {@link com.yahoo.vespa.config.server.ApplicationSet} to be reloaded
+ */
+ public void reloadConfig(ApplicationSet applicationSet) {
+ setLiveApp(applicationSet);
+ notifyReloadListeners(applicationSet);
+ }
+
+ @Override
+ public void removeApplication(ApplicationId applicationId) {
+ if (applicationMapper.hasApplication(applicationId)) {
+ applicationMapper.remove(applicationId);
+ hostRegistry.removeHostsForKey(applicationId);
+ reloadListenersOnRemove(applicationId);
+ tenantMetricUpdater.setApplications(applicationMapper.numApplications());
+ metrics.removeMetricUpdater(Metrics.createDimensions(applicationId));
+ }
+ }
+
+ private void reloadListenersOnRemove(ApplicationId applicationId) {
+ for (ReloadListener listener : reloadListeners) {
+ listener.applicationRemoved(applicationId);
+ listener.hostsUpdated(tenant, hostRegistry.getAllHosts());
+ }
+ }
+
+ private void setLiveApp(ApplicationSet applicationSet) {
+ ApplicationId id = applicationSet.getId();
+ final Collection<String> hostsForApp = applicationSet.getAllHosts();
+ hostRegistry.update(id, hostsForApp);
+ applicationSet.updateHostMetrics();
+ tenantMetricUpdater.setApplications(applicationMapper.numApplications());
+ applicationMapper.register(id, applicationSet);
+ }
+
+ @Override
+ public Set<ConfigKey<?>> listNamedConfigs(ApplicationId appId, Optional<Version> vespaVersion, ConfigKey<?> keyToMatch, boolean recursive) {
+ Application application = getApplication(appId, vespaVersion);
+ return listConfigs(application, keyToMatch, recursive);
+ }
+
+ private Set<ConfigKey<?>> listConfigs(Application application, ConfigKey<?> keyToMatch, boolean recursive) {
+ Set<ConfigKey<?>> ret = new LinkedHashSet<>();
+ for (ConfigKey<?> key : application.allConfigsProduced()) {
+ String configId = key.getConfigId();
+ if (recursive) {
+ key = new ConfigKey<>(key.getName(), configId, key.getNamespace());
+ } else {
+ // Include first part of id as id
+ key = new ConfigKey<>(key.getName(), configId.split("/")[0], key.getNamespace());
+ }
+ if (keyToMatch != null) {
+ String n = key.getName(); // Never null
+ String ns = key.getNamespace(); // Never null
+ if (n.equals(keyToMatch.getName()) &&
+ ns.equals(keyToMatch.getNamespace()) &&
+ configId.startsWith(keyToMatch.getConfigId()) &&
+ !(configId.equals(keyToMatch.getConfigId()))) {
+
+ if (!recursive) {
+ // For non-recursive, include the id segment we were searching for, and first part of the rest
+ key = new ConfigKey<>(key.getName(), appendOneLevelOfId(keyToMatch.getConfigId(), configId), key.getNamespace());
+ }
+ ret.add(key);
+ }
+ } else {
+ ret.add(key);
+ }
+ }
+ return ret;
+ }
+
+ @Override
+ public Set<ConfigKey<?>> listConfigs(ApplicationId appId, Optional<Version> vespaVersion, boolean recursive) {
+ Application application = getApplication(appId, vespaVersion);
+ return listConfigs(application, null, recursive);
+ }
+
+ /**
+ * Given baseIdSegment search/ and id search/qrservers/default.0, return search/qrservers
+ * @return id segment with one extra level from the id appended
+ */
+ String appendOneLevelOfId(String baseIdSegment, String id) {
+ if ("".equals(baseIdSegment)) return id.split("/")[0];
+ String theRest = id.substring(baseIdSegment.length());
+ if ("".equals(theRest)) return id;
+ theRest = theRest.replaceFirst("/", "");
+ String theRestFirstSeg = theRest.split("/")[0];
+ return baseIdSegment+"/"+theRestFirstSeg;
+ }
+
+ @Override
+ public Set<ConfigKey<?>> allConfigsProduced(ApplicationId appId, Optional<Version> vespaVersion) {
+ Application application = getApplication(appId, vespaVersion);
+ return application.allConfigsProduced();
+ }
+
+ private Application getApplication(ApplicationId appId, Optional<Version> vespaVersion) {
+ try {
+ return applicationMapper.getForVersion(appId, vespaVersion);
+ } catch (VersionDoesNotExistException ex) {
+ throw new NotFoundException(String.format("%sNo such application (id %s): %s", Tenants.logPre(tenant), appId, ex.getMessage()));
+ }
+ }
+
+ @Override
+ public Set<String> allConfigIds(ApplicationId appId, Optional<Version> vespaVersion) {
+ Application application = getApplication(appId, vespaVersion);
+ return application.allConfigIds();
+ }
+
+ @Override
+ public boolean hasApplication(ApplicationId appId, Optional<Version> vespaVersion) {
+ return hasHandler(appId, vespaVersion);
+ }
+
+ private boolean hasHandler(ApplicationId appId, Optional<Version> vespaVersion) {
+ return applicationMapper.hasApplicationForVersion(appId, vespaVersion);
+ }
+
+ @Override
+ public ApplicationId resolveApplicationId(String hostName) {
+ ApplicationId applicationId = hostRegistry.getKeyForHost(hostName);
+ if (applicationId == null) {
+ applicationId = ApplicationId.defaultId();
+ }
+ return applicationId;
+ }
+
+ @Override
+ public void verifyHosts(ApplicationId key, Collection<String> newHosts) {
+ hostRegistry.verifyHosts(key, newHosts);
+ for (ReloadListener reloadListener : reloadListeners) {
+ reloadListener.verifyHostsAreAvailable(tenant, newHosts);
+ }
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/Tenants.java b/configserver/src/main/java/com/yahoo/vespa/config/server/Tenants.java
new file mode 100644
index 00000000000..63ae8d1f743
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/Tenants.java
@@ -0,0 +1,358 @@
+// 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.server;
+
+import com.google.inject.Inject;
+import com.yahoo.concurrent.ThreadFactoryFactory;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Deployer;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.vespa.curator.Curator;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
+import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
+import org.apache.curator.framework.state.ConnectionState;
+import org.apache.curator.framework.state.ConnectionStateListener;
+import org.apache.zookeeper.KeeperException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+/**
+ * This component will monitor the set of tenants in the config server by watching in ZooKeeper.
+ * It will set up Tenant objects accordingly, which will manage the config sessions per tenant.
+ * This class will read the preexisting set of tenants from ZooKeeper at startup. (For now it will also
+ * create a default tenant since that will be used for API that do no know about tenants or have not yet
+ * implemented support for it).
+ *
+ * This instance is called from two different threads, the http handler threads and the zookeeper watcher threads.
+ * To create or delete a tenant, the handler calls {@link Tenants#createTenant} and {@link Tenants#deleteTenant} methods.
+ * This will delete shared state from zookeeper, and return, so it does not mean a tenant is immediately deleted.
+ *
+ * Once a tenant is deleted from zookeeper, the zookeeper watcher thread will get notified on all configservers, and
+ * shutdown and delete any per-configserver state.
+ *
+ * @author vegardh
+ * @author lulf
+ * @since 5.1.26
+ */
+public class Tenants implements ConnectionStateListener, PathChildrenCacheListener {
+
+ private static final Logger log = Logger.getLogger(Tenants.class.getName());
+
+ private static final TenantName DEFAULT_TENANT = TenantName.defaultName();
+ private static final List<TenantName> SYSTEM_TENANT_NAMES = Arrays.asList(
+ DEFAULT_TENANT,
+ ApplicationId.HOSTED_VESPA_TENANT);
+ private static final Path tenantsPath = Path.fromString("/config/v2/tenants/");
+ private static final Path vespaPath = Path.fromString("/vespa");
+
+ private final Map<TenantName, Tenant> tenants = new LinkedHashMap<>();
+ private final GlobalComponentRegistry globalComponentRegistry;
+ private final List<TenantListener> tenantListeners = Collections.synchronizedList(new ArrayList<>());
+ private final Curator curator;
+
+ private final MetricUpdater metricUpdater;
+ private final ExecutorService pathChildrenExecutor = Executors.newFixedThreadPool(1, ThreadFactoryFactory.getThreadFactory(Tenants.class.getName()));
+ private final Curator.DirectoryCache directoryCache;
+
+
+ /**
+ * New instance from the tenants in the given component registry's ZooKeeper. Will set watch when reading them.
+ *
+ * @param globalComponentRegistry a {@link com.yahoo.vespa.config.server.GlobalComponentRegistry}
+ * @throws Exception is creating the Tenants instance fails
+ */
+ @Inject
+ public Tenants(GlobalComponentRegistry globalComponentRegistry, Metrics metrics) throws Exception {
+ // Note: unit tests may want to use the constructor below to avoid setting watch by calling readTenants().
+ this.globalComponentRegistry = globalComponentRegistry;
+ this.curator = globalComponentRegistry.getCurator();
+ metricUpdater = metrics.getOrCreateMetricUpdater(Collections.<String, String>emptyMap());
+ this.tenantListeners.add(globalComponentRegistry.getTenantListener());
+ curator.framework().getConnectionStateListenable().addListener(this);
+
+ curator.create(tenantsPath);
+ createSystemTenants();
+ curator.create(vespaPath);
+
+ this.directoryCache = globalComponentRegistry.getCurator().createDirectoryCache(tenantsPath.getAbsolute(), false, false, pathChildrenExecutor);
+ directoryCache.start();
+ directoryCache.addListener(this);
+ tenantsChanged(readTenants());
+ notifyTenantsLoaded();
+ }
+
+ private void notifyTenantsLoaded() {
+ for (TenantListener tenantListener : tenantListeners) {
+ tenantListener.onTenantsLoaded();
+ }
+ }
+
+ /**
+ * New instance containing the given tenants. This will not watch in ZooKeeper.
+ * @param globalComponentRegistry a {@link com.yahoo.vespa.config.server.GlobalComponentRegistry} instance
+ * @param metrics a {@link com.yahoo.vespa.config.server.monitoring.Metrics} instance
+ * @param tenants a collection of {@link Tenant}s
+ */
+ public Tenants(GlobalComponentRegistry globalComponentRegistry, Metrics metrics, Collection<Tenant> tenants) {
+ this.globalComponentRegistry = globalComponentRegistry;
+ this.curator = globalComponentRegistry.getCurator();
+ metricUpdater = metrics.getOrCreateMetricUpdater(Collections.<String, String>emptyMap());
+ this.tenantListeners.add(globalComponentRegistry.getTenantListener());
+ curator.create(tenantsPath);
+ this.directoryCache = curator.createDirectoryCache(tenantsPath.getAbsolute(), false, false, pathChildrenExecutor);
+ this.tenants.putAll(addTenants(tenants));
+ }
+
+ // Pre-condition: tenants path needs to exist in zk
+ private LinkedHashMap<TenantName, Tenant> addTenants(Collection<Tenant> newTenants) {
+ final LinkedHashMap<TenantName, Tenant> sessionTenants = new LinkedHashMap<>();
+ for (Tenant t : newTenants) {
+ sessionTenants.put(t.getName(), t);
+ }
+ log.log(LogLevel.DEBUG, "Tenants at startup: " + sessionTenants);
+ metricUpdater.setTenants(tenants.size());
+ return sessionTenants;
+ }
+
+ /**
+ * Reads the set of tenants in patch cache.
+ *
+ * @return a set of tenant names
+ */
+ private Set<TenantName> readTenants() {
+ Set<TenantName> tenants = new LinkedHashSet<>();
+ for (String tenant : curator.getChildren(tenantsPath)) {
+ tenants.add(TenantName.from(tenant));
+ }
+ return tenants;
+ }
+
+ synchronized void tenantsChanged(Set<TenantName> newTenants) throws Exception {
+ log.log(LogLevel.DEBUG, "Tenants changed: " + newTenants);
+ checkForRemovedTenants(newTenants);
+ checkForAddedTenants(newTenants);
+ metricUpdater.setTenants(tenants.size());
+ }
+
+ private void checkForRemovedTenants(Set<TenantName> newTenants) {
+ Map<TenantName, Tenant> current = new LinkedHashMap<>(tenants);
+ for (Map.Entry<TenantName, Tenant> entry : current.entrySet()) {
+ TenantName tenant = entry.getKey();
+ if (!newTenants.contains(tenant)) {
+ notifyRemovedTenant(tenant);
+ entry.getValue().close();
+ tenants.remove(tenant);
+ }
+ }
+ }
+
+ private void checkForAddedTenants(Set<TenantName> newTenants)
+ throws Exception {
+ ExecutorService executor = Executors.newFixedThreadPool(globalComponentRegistry.getConfigserverConfig().numParallelTenantLoaders());
+ Map<TenantName, Tenant> addedTenants = new ConcurrentHashMap<>();
+ for (TenantName tenantName : newTenants) {
+ // Note: the http handler will check if the tenant exists, and throw accordingly
+ if (!tenants.containsKey(tenantName)) {
+ executor.execute(() -> {
+ try {
+ Tenant tenant = TenantBuilder.create(globalComponentRegistry, tenantName, getTenantPath(tenantName)).build();
+ notifyNewTenant(tenant);
+ addedTenants.put(tenantName, tenant);
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, "Error loading tenant '" + tenantName + "', skipping.", e);
+ }
+ });
+ }
+ }
+ executor.shutdown();
+ executor.awaitTermination(365, TimeUnit.DAYS); // Timeout should never happen
+ tenants.putAll(addedTenants);
+ }
+
+ /**
+ * The registered tenants. Creates a copy of the map to avoid it being modified outside, since it can
+ * change after this method has been called.
+ *
+ * @return tenant list
+ */
+ public synchronized Map<TenantName, Tenant> tenantsCopy() {
+ return new LinkedHashMap<>(tenants);
+ }
+
+ /**
+ * Returns a default (compatibility with single tenant config requests) tenant
+ *
+ * @return default tenant
+ */
+ public synchronized Tenant defaultTenant() {
+ return tenants.get(DEFAULT_TENANT);
+ }
+
+ private void notifyNewTenant(Tenant tenant) {
+ for (TenantListener listener : tenantListeners) {
+ listener.onTenantCreate(tenant.getName(), tenant);
+ }
+ }
+
+ private void notifyRemovedTenant(TenantName name) {
+ for (TenantListener listener : tenantListeners) {
+ listener.onTenantDelete(name);
+ }
+ }
+
+ /**
+ * Writes the default tenant into ZooKeeper. Will not fail if the node already exists,
+ * as this is OK and might happen when several config servers start at the same time and
+ * try to call this method.
+ */
+ public synchronized void createSystemTenants() {
+ for (final TenantName tenantName : SYSTEM_TENANT_NAMES) {
+ try {
+ createTenant(tenantName);
+ } catch (RuntimeException e) {
+ // Do nothing if we get NodeExistsException
+ if (e.getCause().getClass() != KeeperException.NodeExistsException.class) {
+ throw e;
+ }
+ }
+ }
+ }
+
+ /**
+ * Writes the given tenant into ZooKeeper, for watchers to react on
+ *
+ * @param name name of the tenant
+ * @return this Tenants
+ */
+ public synchronized Tenants createTenant(TenantName name) {
+ Path tenantPath = getTenantPath(name);
+ curator.createAtomically(tenantPath, tenantPath.append(Tenant.SESSIONS), tenantPath.append(Tenant.APPLICATIONS));
+ return this;
+ }
+
+ /**
+ * Removes the given tenant from ZooKeeper and filesystem. Assumes that tenant exists.
+ *
+ * @param name name of the tenant
+ * @return this Tenants instance
+ */
+ public synchronized Tenants deleteTenant(TenantName name) {
+ Tenant tenant = tenants.get(name);
+ tenant.delete();
+ return this;
+ }
+
+ // For unit testing
+ String tenantZkPath(TenantName tenant) {
+ return getTenantPath(tenant).getAbsolute();
+ }
+
+ /**
+ * A helper to format a log preamble for messages with a tenant and app id
+ * @param app the app
+ * @return the log string
+ */
+ public static String logPre(ApplicationId app) {
+ if (TenantName.defaultName().equals(app.tenant())) return "";
+ StringBuilder ret = new StringBuilder()
+ .append(logPre(app.tenant()))
+ .append("app:"+app.application().value())
+ .append(":"+app.instance().value())
+ .append(" ");
+ return ret.toString();
+ }
+
+ /**
+ * A helper to format a log preamble for messages with a tenant
+ * @param tenant tenant
+ * @return the log string
+ */
+ public static String logPre(TenantName tenant) {
+ if (DEFAULT_TENANT.equals(tenant)) return "";
+ StringBuilder ret = new StringBuilder()
+ .append("tenant:"+tenant.value())
+ .append(" ");
+ return ret.toString();
+ }
+
+ @Override
+ public void stateChanged(CuratorFramework framework, ConnectionState connectionState) {
+ switch (connectionState) {
+ case CONNECTED:
+ metricUpdater.incZKConnected();
+ break;
+ case SUSPENDED:
+ metricUpdater.incZKSuspended();
+ break;
+ case RECONNECTED:
+ metricUpdater.incZKReconnected();
+ break;
+ case LOST:
+ metricUpdater.incZKConnectionLost();
+ break;
+ case READ_ONLY:
+ // NOTE: Should not be relevant for configserver.
+ break;
+ }
+ }
+
+ @Override
+ public void childEvent(CuratorFramework framework, PathChildrenCacheEvent event) throws Exception {
+ switch (event.getType()) {
+ case CHILD_ADDED:
+ case CHILD_REMOVED:
+ tenantsChanged(readTenants());
+ break;
+ }
+ }
+
+ public void close() throws IOException {
+ directoryCache.close();
+ pathChildrenExecutor.shutdown();
+ }
+
+ public void redeployApplications(Deployer deployer) {
+ final int totalNumberOfApplications = tenantsCopy().values().stream()
+ .mapToInt(tenant -> tenant.getApplicationRepo().listApplications().size()).sum();
+ int applicationsRedeployed = 0;
+ for (Tenant tenant : tenantsCopy().values()) {
+ tenant.redeployApplications(deployer);
+ applicationsRedeployed += redeployProgress(tenant, applicationsRedeployed, totalNumberOfApplications);
+ }
+ }
+
+ private static int redeployProgress(Tenant tenant, int applicationsRedeployed, int totalNumberOfApplications) {
+ int size = tenant.getApplicationRepo().listApplications().size();
+ if (size > 0) {
+ log.log(LogLevel.INFO, String.format("Redeployed %s of %s applications", applicationsRedeployed + size, totalNumberOfApplications));
+ }
+ return size;
+ }
+
+ /**
+ * Gets zookeeper path for tenant data
+ * @param tenantName tenant name
+ * @return a {@link com.yahoo.path.Path} to the zookeeper data for a tenant
+ */
+ public static Path getTenantPath(TenantName tenantName) {
+ return tenantsPath.append(tenantName.value());
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/TimeoutBudget.java b/configserver/src/main/java/com/yahoo/vespa/config/server/TimeoutBudget.java
new file mode 100644
index 00000000000..fe02cc18bd8
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/TimeoutBudget.java
@@ -0,0 +1,59 @@
+// 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.server;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.concurrent.TimeUnit;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Handles a timeout logic by providing higher level abstraction for asking if there is time left.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class TimeoutBudget {
+ private final Clock clock;
+ private final Instant startTime;
+ private final List<Instant> measurements = new ArrayList<>();
+ private final Instant endTime;
+
+ public TimeoutBudget(Clock clock, Duration duration) {
+ this.clock = clock;
+ this.startTime = clock.instant();
+ this.endTime = startTime.plus(duration);
+ }
+
+ public Duration timeLeft() {
+ Instant now = clock.instant();
+ measurements.add(now);
+ Duration duration = Duration.between(now, endTime);
+ return duration.isNegative() ? Duration.ofMillis(0) : duration;
+ }
+
+ public boolean hasTimeLeft() {
+ Instant now = clock.instant();
+ measurements.add(now);
+ return now.isBefore(endTime);
+ }
+
+ public String timesUsed() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("[");
+ Instant prev = startTime;
+ for (Instant m : measurements) {
+ buf.append(Duration.between(prev, m).toMillis());
+ prev = m;
+ buf.append(" ms, ");
+ }
+ Instant now = clock.instant();
+ buf.append("total: ");
+ buf.append(Duration.between(startTime, now).toMillis());
+ buf.append(" ms]");
+ return buf.toString();
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/UncompressedConfigResponseFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/UncompressedConfigResponseFactory.java
new file mode 100644
index 00000000000..73e7acb5a88
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/UncompressedConfigResponseFactory.java
@@ -0,0 +1,27 @@
+// 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.server;
+
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.text.Utf8Array;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.protocol.CompressionInfo;
+import com.yahoo.vespa.config.protocol.CompressionType;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.protocol.SlimeConfigResponse;
+import com.yahoo.vespa.config.util.ConfigUtils;
+
+/**
+ * Simply returns an uncompressed payload.
+ *
+ * @author lulf
+ * @since 5.19
+ */
+public class UncompressedConfigResponseFactory implements ConfigResponseFactory {
+ @Override
+ public ConfigResponse createResponse(ConfigPayload payload, InnerCNode defFile, long generation) {
+ Utf8Array rawPayload = payload.toUtf8Array(true);
+ String configMd5 = ConfigUtils.getMd5(rawPayload);
+ CompressionInfo info = CompressionInfo.create(CompressionType.UNCOMPRESSED, rawPayload.getByteLength());
+ return new SlimeConfigResponse(rawPayload, defFile, generation, configMd5, info);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/UnknownConfigDefinitionException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/UnknownConfigDefinitionException.java
new file mode 100644
index 00000000000..182c25a84e2
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/UnknownConfigDefinitionException.java
@@ -0,0 +1,14 @@
+// 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.server;
+
+/**
+ * Indicates that a config definition (typically a def file schema) was unknown to the config server
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class UnknownConfigDefinitionException extends IllegalArgumentException {
+ public UnknownConfigDefinitionException(String s) {
+ super(s);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/VersionDoesNotExistException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/VersionDoesNotExistException.java
new file mode 100644
index 00000000000..d6a329491e6
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/VersionDoesNotExistException.java
@@ -0,0 +1,11 @@
+// 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.server;
+
+/**
+ * @author Vegard Sjonfjell
+ */
+public final class VersionDoesNotExistException extends RuntimeException {
+ public VersionDoesNotExistException(String message) {
+ super(message);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/Application.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/Application.java
new file mode 100644
index 00000000000..7116dda7322
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/Application.java
@@ -0,0 +1,243 @@
+// 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.server.application;
+
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.ConfigurationRuntimeException;
+import com.yahoo.config.model.api.Model;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Version;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.ConfigCacheKey;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.GetConfigRequest;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.protocol.DefContent;
+import com.yahoo.vespa.config.server.ConfigResponseFactory;
+import com.yahoo.vespa.config.server.ServerCache;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.UncompressedConfigResponseFactory;
+import com.yahoo.vespa.config.server.UnknownConfigDefinitionException;
+import com.yahoo.vespa.config.server.modelfactory.ModelResult;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.util.ConfigUtils;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A Vespa application for a specific version of Vespa. It holds data and metadata associated with
+ * a Vespa application, i.e. generation, vespamodel instance and zookeeper data, as well as methods for resolving config
+ * and other queries against the model.
+ *
+ * @author Harald Musum
+ * @since 2010-12-08
+ */
+public class Application implements ModelResult {
+
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(Application.class.getName());
+ private final long appGeneration; // The generation of the set of configs belonging to an application
+ private final Version vespaVersion;
+ private final Model model;
+ private final ServerCache cache;
+ private final MetricUpdater metricUpdater;
+ private final ApplicationId app;
+
+ public Application(Model model, ServerCache cache, long appGeneration, Version vespaVersion, MetricUpdater metricUpdater, ApplicationId app) {
+ Objects.requireNonNull(model, "The model cannot be null");
+ this.model = model;
+ this.cache = cache;
+ this.appGeneration = appGeneration;
+ this.vespaVersion = vespaVersion;
+ this.metricUpdater = metricUpdater;
+ this.app = app;
+ }
+
+ /**
+ * Returns the generation for the config we are currently serving
+ *
+ * @return the config generation
+ */
+ public Long getApplicationGeneration() { return appGeneration; }
+
+ // TODO: Return ApplicationName
+ public String getName() { return app.application().value(); }
+
+ /** Returns the application model, never null */
+ @Override
+ public Model getModel() { return model; }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("application '").append(app.application().value()).append("', ");
+ sb.append("generation ").append(appGeneration).append(", ");
+ sb.append("vespa version ").append(vespaVersion);
+ return sb.toString();
+ }
+
+ public ServerCache getCache() {
+ return cache;
+ }
+
+ public ApplicationId getId() {
+ return app;
+ }
+
+ public Version getVespaVersion() {
+ return vespaVersion;
+ }
+
+ /**
+ * The old style (deprecated) configs/ user overrides for this key
+ *
+ * @param key the key for the config to get user config for
+ * @return the user config value or null
+ */
+ private ConfigPayload getLegacyUserConfigs(ConfigCacheKey key) {
+ try {
+ if (logDebug()) {
+ debug("Looking up legacy user config for " + key);
+ }
+ return cache.getLegacyUserConfig(key.getKey().getName());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Gets a config from ZK. Returns null if not found.
+ */
+ public ConfigResponse resolveConfig(GetConfigRequest req, ConfigResponseFactory responseFactory) {
+ long start = System.currentTimeMillis();
+ metricUpdater.incrementRequests();
+ ConfigKey<?> configKey = req.getConfigKey();
+ String defMd5 = configKey.getMd5();
+ if (defMd5 == null || defMd5.isEmpty()) {
+ defMd5 = ConfigUtils.getDefMd5(req.getDefContent().asList());
+ }
+ ConfigCacheKey cacheKey = new ConfigCacheKey(configKey, defMd5);
+ if (logDebug()) {
+ debug("Resolving config " + cacheKey);
+ }
+
+ if (!req.noCache()) {
+ ConfigResponse config = cache.get(cacheKey);
+ if (config != null) {
+ if (logDebug()) {
+ debug("Found config " + cacheKey + " in cache");
+ }
+ metricUpdater.incrementProcTime(System.currentTimeMillis() - start);
+ return config;
+ }
+ }
+
+ // Try new ConfigInstance based API:
+ ConfigDefinitionWrapper configDefinitionWrapper = getTargetDef(req);
+ ConfigDefinition def = configDefinitionWrapper.getDef();
+ if (def == null) {
+ metricUpdater.incrementFailedRequests();
+ throw new UnknownConfigDefinitionException("Unable to find config definition for '" + configKey.getNamespace() + "." + configKey.getName());
+ }
+ configKey = new ConfigKey<>(configDefinitionWrapper.getDefKey().getName(), configKey.getConfigId(), configDefinitionWrapper.getDefKey().getNamespace());
+ ConfigPayload override = getLegacyUserConfigs(cacheKey);
+ ConfigPayload payload;
+ try {
+ if (logDebug()) {
+ debug("Resolving " + configKey + " with targetDef=" + def + ", override=" + override);
+ }
+
+ payload = model.getConfig(
+ configKey,
+ def,
+ override);
+ } catch (IOException e) {
+ metricUpdater.incrementFailedRequests();
+ throw new ConfigurationRuntimeException("Unable to resolve config", e);
+ }
+
+ ConfigResponse configResponse = responseFactory.createResponse(payload, def.getCNode(), appGeneration);
+ metricUpdater.incrementProcTime(System.currentTimeMillis() - start);
+ if (!req.noCache()) {
+ cache.put(cacheKey, configResponse, configResponse.getConfigMd5());
+ metricUpdater.setCacheConfigElems(cache.configElems());
+ metricUpdater.setCacheChecksumElems(cache.checkSumElems());
+ }
+ return configResponse;
+ }
+
+ private boolean logDebug() {
+ return log.isLoggable(LogLevel.DEBUG);
+ }
+
+ private void debug(String message) {
+ log.log(LogLevel.DEBUG, Tenants.logPre(getId())+message);
+ }
+
+ private ConfigDefinitionWrapper getTargetDef(GetConfigRequest req) {
+ ConfigKey<?> configKey = req.getConfigKey();
+ DefContent def = req.getDefContent();
+ ConfigDefinitionKey configDefinitionKey = new ConfigDefinitionKey(configKey.getName(), configKey.getNamespace());
+ if (def.isEmpty()) {
+ if (logDebug()) {
+ debug("No config schema in request for " + configKey);
+ }
+ ConfigDefinition ret = cache.getDef(configDefinitionKey);
+ return new ConfigDefinitionWrapper(configDefinitionKey, ret);
+ } else {
+ if (logDebug()) {
+ debug("Got config schema from request, length:" + def.asList().size() + " : " + configKey);
+ }
+ return new ConfigDefinitionWrapper(configDefinitionKey, new ConfigDefinition(configKey.getName(), def.asStringArray()));
+ }
+ }
+
+ public void updateHostMetrics(int numHosts) {
+ metricUpdater.setHosts(numHosts);
+ }
+
+ // For testing only
+ ConfigResponse resolveConfig(GetConfigRequest req) {
+ return resolveConfig(req, new UncompressedConfigResponseFactory());
+ }
+
+ public <CONFIGTYPE extends ConfigInstance> CONFIGTYPE getConfig(Class<CONFIGTYPE> configClass, String configId) throws IOException {
+ ConfigKey<CONFIGTYPE> key = new ConfigKey<>(configClass, configId);
+ ConfigPayload payload = model.getConfig(key, (ConfigDefinition)null, null);
+ return payload.toInstance(configClass, configId);
+ }
+
+ /**
+ * Wrapper class for holding config definition key and def, since when looking up
+ * we may end up changing the config definition key (fallback mechanism when using
+ * legacy config namespace (or not using config namespace))
+ */
+ private static class ConfigDefinitionWrapper {
+ private final ConfigDefinitionKey defKey;
+ private final ConfigDefinition def;
+
+ ConfigDefinitionWrapper(ConfigDefinitionKey defKey, ConfigDefinition def) {
+ this.defKey = defKey;
+ this.def = def;
+ }
+
+ public ConfigDefinitionKey getDefKey() {
+ return defKey;
+ }
+
+ public ConfigDefinition getDef() {
+ return def;
+ }
+ }
+
+ public Set<ConfigKey<?>> allConfigsProduced() {
+ return model.allConfigsProduced();
+ }
+
+ public Set<String> allConfigIds() {
+ return model.allConfigIds();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationConvergenceChecker.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationConvergenceChecker.java
new file mode 100644
index 00000000000..a36167517d8
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationConvergenceChecker.java
@@ -0,0 +1,262 @@
+// 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.server.application;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ModelConfig;
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import org.glassfish.jersey.client.proxy.WebResourceFactory;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+/**
+ * Checks for convergence of config generation for a given application.
+ *
+ * @author lulf
+ */
+public class ApplicationConvergenceChecker extends AbstractComponent {
+ private static final String statePath = "/state/v1/";
+ private static final String configSubPath = "config";
+ private static final String configPath = statePath + configSubPath;
+ private final StateApiFactory stateApiFactory;
+ private final Client client = ClientBuilder.newClient();
+
+ private final static Set<String> serviceTypes = new HashSet<>(Arrays.asList(
+ "container",
+ "qrserver",
+ "docprocservice",
+ "searchnode",
+ "storagenode",
+ "distributor"
+ ));
+
+ @Inject
+ public ApplicationConvergenceChecker() {
+ this(ApplicationConvergenceChecker::createStateApi);
+ }
+
+ public ApplicationConvergenceChecker(StateApiFactory stateApiFactory) {
+ this.stateApiFactory = stateApiFactory;
+ }
+
+ // TODO: Remove this function once the other has taken over (list)
+ private void waitForConfigConverged(ModelConfig config, long wantedGeneration, TimeoutBudget timeoutBudget) {
+
+ config.hosts().stream()
+ .forEach(host -> host.services().stream()
+ .filter(service -> serviceTypes.contains(service.type()))
+ .forEach(service -> {
+ Optional<Integer> statePort = getStatePort(service);
+ if (statePort.isPresent()) {
+ URI serviceUri = getServiceUri(host.name(), statePort.get());
+ try {
+ waitForServiceGenerationConverged(serviceUri, wantedGeneration, timeoutBudget);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }));
+ }
+
+ public void waitForConfigConverged(Application application, TimeoutBudget timeoutBudget) throws IOException {
+ ModelConfig config = application.getConfig(ModelConfig.class, "");
+ waitForConfigConverged(config, application.getApplicationGeneration(), timeoutBudget);
+ }
+
+ private Optional<Integer> getStatePort(ModelConfig.Hosts.Services service) { return service.ports().stream()
+ .filter(port -> port.tags().contains("state"))
+ .map(ModelConfig.Hosts.Services.Ports::number)
+ .findFirst();
+ }
+
+ private URI getServiceUri(String host, int port) {
+ return URI.create("http://" + host + ":" + port);
+ }
+
+ private void waitForServiceGenerationConverged(URI serviceUri, long wantedGeneration, TimeoutBudget timeoutBudget) throws InterruptedException {
+ long generation = -1;
+ do {
+ try {
+ generation = getServiceGeneration(serviceUri);
+ if (generation >= wantedGeneration)
+ return;
+ } catch (Exception e) {
+ // Try again
+ }
+ Thread.sleep(100);
+ } while (timeoutBudget.hasTimeLeft());
+ StringBuilder message = new StringBuilder("Timed out waiting for service to use config generation ").
+ append(wantedGeneration).
+ append(" (checking ").
+ append(serviceUri).append(configPath).
+ append("), ");
+ if (generation == -1) {
+ message.append("could not connect.");
+ } else {
+ message.append("generation was ").append(generation).append(".");
+ }
+ throw new ConfigNotConvergedException(message.toString());
+ }
+
+ public long generationFromContainerState(JsonNode state) {
+ return state.get("config").get("generation").asLong();
+ }
+
+ private static StateApi createStateApi(Client client, URI uri) {
+ WebTarget target = client.target(uri);
+ return WebResourceFactory.newResource(StateApi.class, target);
+ }
+
+ private long getServiceGeneration(URI serviceUri) {
+ StateApi state = stateApiFactory.createStateApi(client, serviceUri);
+ return generationFromContainerState(state.config());
+ }
+
+ @Override
+ public void deconstruct() {
+ client.close();
+ }
+
+ private boolean hostInApplication(Application application, String hostPort) {
+ final ModelConfig config;
+ try {
+ config = application.getConfig(ModelConfig.class, "");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ final List<ModelConfig.Hosts> hosts = config.hosts();
+ for (ModelConfig.Hosts host : hosts) {
+ if (hostPort.startsWith(host.name())) {
+ for (ModelConfig.Hosts.Services service : host.services()) {
+ for (ModelConfig.Hosts.Services.Ports port : service.ports()) {
+ if (hostPort.equals(host.name() + ":" + port.number())) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ public HttpResponse nodeConvergenceCheck(Application application, String hostFromRequest, URI uri) {
+ JSONObject answer = new JSONObject();
+ JSONObject debug = new JSONObject();
+ try {
+ answer.put("url", uri);
+ debug.put("wantedGeneration", application.getApplicationGeneration());
+ debug.put("host", hostFromRequest);
+
+ if (!hostInApplication(application, hostFromRequest)) {
+ debug.put("problem", "Host:port (service) no longer part of application, refetch list of services.");
+ answer.put("debug", debug);
+ return new JsonHttpResponse(410, answer);
+ }
+ final long generation = getServiceGeneration(URI.create("http://" + hostFromRequest));
+ debug.put("currentGeneration", generation);
+ answer.put("debug", debug);
+ answer.put("converged", generation >= application.getApplicationGeneration());
+ return new JsonHttpResponse(200, answer);
+ } catch(JSONException e) {
+ try {
+ answer.put("error", e.getMessage());
+ } catch (JSONException e1) {
+ throw new RuntimeException("Fail while creating error message ", e1);
+ }
+ return new JsonHttpResponse(500, answer);
+ }
+ }
+
+ private static class JsonHttpResponse extends HttpResponse {
+
+ private final JSONObject answer;
+
+ JsonHttpResponse(int returncode, JSONObject answer) {
+ super(returncode);
+ this.answer = answer;
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ outputStream.write(answer.toString().getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+ }
+
+ public HttpResponse listConfigConvergence(Application application, URI uri) {
+ final JSONObject answer = new JSONObject();
+ final JSONArray nodes = new JSONArray();
+ final ModelConfig config;
+ try {
+ config = application.getConfig(ModelConfig.class, "");
+ } catch (IOException e) {
+ throw new RuntimeException("failed on get config model", e);
+ }
+ config.hosts().stream()
+ .forEach(host -> {
+ host.services().stream()
+ .filter(service -> serviceTypes.contains(service.type()))
+ .forEach(service -> {
+ Optional<Integer> statePort = getStatePort(service);
+ if (statePort.isPresent()) {
+ JSONObject hostNode = new JSONObject();
+ try {
+ hostNode.put("host", host.name());
+ hostNode.put("port", statePort.get());
+ hostNode.put("url", uri.toString() + "/" + host.name() + ":" + statePort.get());
+ hostNode.put("type", service.type());
+
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ nodes.put(hostNode);
+ }
+ });
+ });
+ try {
+ answer.put("services", nodes);
+ JSONObject debug = new JSONObject();
+ debug.put("wantedVersion", application.getApplicationGeneration());
+ answer.put("debug", debug);
+ answer.put("url", uri.toString());
+ return new JsonHttpResponse(200, answer);
+ } catch (JSONException e) {
+ try {
+ answer.put("error", e.getMessage());
+ } catch (JSONException e1) {
+ throw new RuntimeException("Failed while creating error message ", e1);
+ }
+ return new JsonHttpResponse(500, answer);
+ }
+ }
+
+ @Path(statePath)
+ public interface StateApi {
+ @Path(configSubPath)
+ @GET
+ JsonNode config();
+ }
+
+ public interface StateApiFactory {
+ StateApi createStateApi(Client client, URI serviceUri);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationRepo.java
new file mode 100644
index 00000000000..a9296f26eeb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationRepo.java
@@ -0,0 +1,53 @@
+// 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.server.application;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.transaction.Transaction;
+
+import java.util.List;
+
+/**
+ * Manages the list of active applications in a config server.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public interface ApplicationRepo {
+
+ /**
+ * List the active applications in this config server.
+ *
+ * @return a list of {@link com.yahoo.config.provision.ApplicationId}s that are active.
+ */
+ public List<ApplicationId> listApplications();
+
+ /**
+ * Register active application and adds it to the repo. If it already exists it is overwritten.
+ *
+ * @param applicationId An {@link com.yahoo.config.provision.ApplicationId} that represents an active application.
+ * @param sessionId Id of the session containing the application package for this id.
+ */
+ Transaction createPutApplicationTransaction(ApplicationId applicationId, long sessionId);
+
+ /**
+ * Return the stored session id for a given application.
+ *
+ * @param applicationId an {@link ApplicationId}
+ * @return session id of given application id.
+ * @throws IllegalArgumentException if the application does not exist
+ */
+ long getSessionIdForApplication(ApplicationId applicationId);
+
+ /**
+ * Deletes an application from the repo if it exists.
+ *
+ * @param applicationId an {@link ApplicationId} to delete.
+ */
+ void deleteApplication(ApplicationId applicationId);
+
+ /**
+ * Closes the application repo. Once a repo has been closed, it should not be used again.
+ */
+ void close();
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ConfigNotConvergedException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ConfigNotConvergedException.java
new file mode 100644
index 00000000000..91e57d504a5
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ConfigNotConvergedException.java
@@ -0,0 +1,11 @@
+// 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.server.application;
+
+/**
+ * @author lulf
+ */
+public class ConfigNotConvergedException extends RuntimeException {
+ public ConfigNotConvergedException(String message) {
+ super(message);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/LogServerLogGrabber.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/LogServerLogGrabber.java
new file mode 100644
index 00000000000..bd60527ca79
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/LogServerLogGrabber.java
@@ -0,0 +1,120 @@
+// 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.server.application;
+
+import com.yahoo.cloud.config.ModelConfig;
+import com.yahoo.component.AbstractComponent;
+import com.google.inject.Inject;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.config.server.http.HttpErrorResponse;
+import com.yahoo.yolean.Exceptions;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+/**
+ * Fetches log entries from logserver with level errors and fatal. The logserver only return
+ * a log entry once over this API so doing repeated call will not give the same results.
+ *
+ * @author dybdahl
+ */
+public class LogServerLogGrabber extends AbstractComponent {
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(LogServerLogGrabber.class.getName());
+
+ @Inject
+ public LogServerLogGrabber() {}
+
+ private Optional<Integer> getErrorLogPort(ModelConfig.Hosts.Services service) {
+ return service.ports().stream()
+ .filter(port -> port.tags().toLowerCase().contains("last-errors-holder"))
+ .map(ModelConfig.Hosts.Services.Ports::number)
+ .findFirst();
+ }
+
+ private class LogServerConnectionInfo {
+ String hostName;
+ int port;
+ }
+
+ public HttpResponse grabLog(Application application) {
+
+ final ModelConfig config;
+ try {
+ config = application.getConfig(ModelConfig.class, "");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ final LogServerConnectionInfo logServerConnectionInfo = new LogServerConnectionInfo();
+
+ config.hosts().stream()
+ .forEach(host -> host.services().stream()
+ .filter(service -> service.type().equals("logserver"))
+ .forEach(logService -> {
+ Optional<Integer> logPort = getErrorLogPort(logService);
+ if (logPort.isPresent()) {
+ if (logServerConnectionInfo.hostName != null) {
+ throw new RuntimeException("Found several log server ports.");
+ }
+ logServerConnectionInfo.hostName = host.name();
+ logServerConnectionInfo.port = logPort.get();
+ }
+ }));
+
+ if (logServerConnectionInfo.hostName == null) {
+ return new HttpResponse(503) {
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ PrintWriter printWriter = new PrintWriter(outputStream);
+ printWriter.print("Did not find any log server in config model.");
+ printWriter.close();
+ }
+ };
+ }
+ log.log(LogLevel.DEBUG, "Requested error logs, pulling from logserver on " + logServerConnectionInfo.hostName + " "
+ + logServerConnectionInfo.port);
+ final String response;
+ try {
+ response = readLog(logServerConnectionInfo.hostName, logServerConnectionInfo.port);
+ log.log(LogLevel.DEBUG, "Requested error logs was " + response.length() + " characters");
+ } catch (IOException e) {
+ return HttpErrorResponse.internalServerError(Exceptions.toMessageString(e));
+ }
+
+ return new HttpResponse(200) {
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ outputStream.write(response.getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+ };
+ }
+
+ private String readLog(String host, int port) throws IOException {
+ Socket socket = new Socket(host, port);
+ BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+ StringBuilder data = new StringBuilder();
+
+ int bufferSize = 4096;
+ int charsRead;
+ do {
+ char[] buffer = new char[bufferSize];
+ charsRead = in.read(buffer);
+ data.append(new String(buffer, 0, charsRead));
+ } while (charsRead == bufferSize);
+ in.close();
+ socket.close();
+ return data.toString();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/PermanentApplicationPackage.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/PermanentApplicationPackage.java
new file mode 100644
index 00000000000..abffb002a76
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/PermanentApplicationPackage.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.server.application;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.log.LogLevel;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.vespa.defaults.Defaults;
+
+import java.io.File;
+import java.util.Optional;
+import java.util.logging.Logger;
+
+/**
+ * A global permanent application package containing configuration info that is always used during deploy.
+ *
+ * @author lulf
+ * @since 5.15
+ */
+public class PermanentApplicationPackage {
+
+ private static final Logger log = Logger.getLogger(PermanentApplicationPackage.class.getName());
+ private final Optional<ApplicationPackage> applicationPackage;
+
+ public PermanentApplicationPackage(ConfigserverConfig config) {
+ File app = new File(Defaults.getDefaults().underVespaHome(config.applicationDirectory()));
+ applicationPackage = Optional.<ApplicationPackage>ofNullable(app.exists() ?
+ FilesApplicationPackage.fromFile(app) : null);
+ if (applicationPackage.isPresent()) {
+ log.log(LogLevel.DEBUG, "Detected permanent application package in '" +
+ Defaults.getDefaults().underVespaHome(config.applicationDirectory()) +
+ "'. This might add extra services to config models");
+ }
+ }
+
+ /**
+ * Get the permanent application package.
+ *
+ * @return An {@link Optional} of the application package, as it may not exist.
+ */
+ public Optional<ApplicationPackage> applicationPackage() {
+ return applicationPackage;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ZKApplicationRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ZKApplicationRepo.java
new file mode 100644
index 00000000000..6567f8439a5
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ZKApplicationRepo.java
@@ -0,0 +1,172 @@
+// 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.server.application;
+
+import com.yahoo.concurrent.ThreadFactoryFactory;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.text.Utf8;
+import com.yahoo.transaction.Transaction;
+import com.yahoo.vespa.config.server.ReloadHandler;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.curator.transaction.CuratorOperations;
+import com.yahoo.vespa.curator.transaction.CuratorTransaction;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
+import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.logging.Logger;
+
+/**
+ * Application repo backed by zookeeper.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ZKApplicationRepo implements ApplicationRepo, PathChildrenCacheListener {
+
+ private static final Logger log = Logger.getLogger(ZKApplicationRepo.class.getName());
+ private final Curator curator;
+ private final Path root;
+ private final ExecutorService pathChildrenExecutor = Executors.newFixedThreadPool(1, ThreadFactoryFactory.getThreadFactory(ZKApplicationRepo.class.getName()));
+ private final Curator.DirectoryCache directoryCache;
+ private final ReloadHandler reloadHandler;
+ private final TenantName tenant;
+
+ private ZKApplicationRepo(Curator curator, Path root, ReloadHandler reloadHandler, TenantName tenant) throws Exception {
+ this.curator = curator;
+ this.root = root;
+ this.reloadHandler = reloadHandler;
+ this.tenant = tenant;
+ rewriteApplicationIds();
+ this.directoryCache = curator.createDirectoryCache(root.getAbsolute(), false, false, pathChildrenExecutor);
+ this.directoryCache.start();
+ this.directoryCache.addListener(this);
+ }
+
+ private void rewriteApplicationIds() {
+ try {
+ List<String> appNodes = curator.framework().getChildren().forPath(root.getAbsolute());
+ for (String appNode : appNodes) {
+ Optional<ApplicationId> appId = parseApplication(appNode);
+ appId.filter(id -> shouldBeRewritten(appNode, id))
+ .ifPresent(id -> rewriteApplicationId(id, appNode, readSessionId(id, appNode)));
+ }
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, "Error rewriting application ids on upgrade", e);
+ }
+ }
+
+ private long readSessionId(ApplicationId appId, String appNode) {
+ String path = root.append(appNode).getAbsolute();
+ try {
+ return Long.parseLong(Utf8.toString(curator.framework().getData().forPath(path)));
+ } catch (Exception e) {
+ throw new IllegalArgumentException(Tenants.logPre(appId) + "Unable to read the session id from '" + path + "'", e);
+ }
+ }
+
+ private boolean shouldBeRewritten(String appNode, ApplicationId appId) {
+ return !appNode.equals(appId.serializedForm());
+ }
+
+ private void rewriteApplicationId(ApplicationId appId, String origNode, long sessionId) {
+ String newPath = root.append(appId.serializedForm()).getAbsolute();
+ String oldPath = root.append(origNode).getAbsolute();
+ try (CuratorTransaction transaction = new CuratorTransaction(curator)) {
+ if (curator.framework().checkExists().forPath(newPath) == null) {
+ transaction.add(CuratorOperations.create(newPath, Utf8.toAsciiBytes(sessionId)));
+ }
+ transaction.add(CuratorOperations.delete(oldPath));
+ transaction.commit();
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, "Error rewriting application id from " + origNode + " to " + appId.serializedForm());
+ }
+ }
+
+ public static ApplicationRepo create(Curator curator, Path root, ReloadHandler reloadHandler, TenantName tenant) {
+ try {
+ return new ZKApplicationRepo(curator, root, reloadHandler, tenant);
+ } catch (Exception e) {
+ throw new RuntimeException(Tenants.logPre(tenant)+"Error creating application repo", e);
+ }
+ }
+
+ @Override
+ public List<ApplicationId> listApplications() {
+ try {
+ List<String> appNodes = curator.framework().getChildren().forPath(root.getAbsolute());
+ List<ApplicationId> applicationIds = new ArrayList<>();
+ for (String appNode : appNodes) {
+ parseApplication(appNode).ifPresent(applicationIds::add);
+ }
+ return applicationIds;
+ } catch (Exception e) {
+ throw new RuntimeException(Tenants.logPre(tenant)+"Unable to list applications", e);
+ }
+ }
+
+ private Optional<ApplicationId> parseApplication(String appNode) {
+ try {
+ return Optional.of(ApplicationId.fromSerializedForm(tenant, appNode));
+ } catch (IllegalArgumentException e) {
+ log.log(LogLevel.INFO, Tenants.logPre(tenant)+"Unable to parse application with id '" + appNode + "', ignoring.");
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public Transaction createPutApplicationTransaction(ApplicationId applicationId, long sessionId) {
+ if (listApplications().contains(applicationId)) {
+ return new CuratorTransaction(curator).add(CuratorOperations.setData(root.append(applicationId.serializedForm()).getAbsolute(), Utf8.toAsciiBytes(sessionId)));
+ } else {
+ return new CuratorTransaction(curator).add(CuratorOperations.create(root.append(applicationId.serializedForm()).getAbsolute(), Utf8.toAsciiBytes(sessionId)));
+ }
+ }
+
+ @Override
+ public long getSessionIdForApplication(ApplicationId applicationId) {
+ return readSessionId(applicationId, applicationId.serializedForm());
+ }
+
+ @Override
+ public void deleteApplication(ApplicationId applicationId) {
+ Path path = root.append(applicationId.serializedForm());
+ curator.delete(path);
+ }
+
+ @Override
+ public void close() {
+ directoryCache.close();
+ pathChildrenExecutor.shutdown();
+ }
+
+
+ @Override
+ public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
+ switch (event.getType()) {
+ case CHILD_ADDED:
+ applicationAdded(ApplicationId.fromSerializedForm(tenant, Path.fromString(event.getData().getPath()).getName()));
+ break;
+ case CHILD_REMOVED:
+ applicationRemoved(ApplicationId.fromSerializedForm(tenant, Path.fromString(event.getData().getPath()).getName()));
+ break;
+ }
+ }
+
+ private void applicationRemoved(ApplicationId applicationId) {
+ reloadHandler.removeApplication(applicationId);
+ log.log(LogLevel.DEBUG, Tenants.logPre(applicationId)+"Application removed: " + applicationId);
+ }
+
+ private void applicationAdded(ApplicationId applicationId) {
+ log.log(LogLevel.DEBUG, Tenants.logPre(applicationId)+"Application " + applicationId + " was added to repo");
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActions.java b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActions.java
new file mode 100644
index 00000000000..7c91a57c3af
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActions.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.server.configchange;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+
+import java.util.List;
+
+/**
+ * Contains an aggregated view of which actions that must be performed to handle config
+ * changes between the current active model and the next model to prepare.
+ * The actions are split into restart and re-feed actions.
+ *
+ * @author geirst
+ * @since 5.44
+ */
+public class ConfigChangeActions {
+
+ private final RestartActions restartActions;
+ private final RefeedActions refeedActions;
+
+ public ConfigChangeActions() {
+ this.restartActions = new RestartActions();
+ this.refeedActions = new RefeedActions();
+ }
+
+ public ConfigChangeActions(List<ConfigChangeAction> actions) {
+ this.restartActions = new RestartActions(actions);
+ this.refeedActions = new RefeedActions(actions);
+ }
+
+ public RestartActions getRestartActions() {
+ return restartActions;
+ }
+
+ public RefeedActions getRefeedActions() {
+ return refeedActions;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsSlimeConverter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsSlimeConverter.java
new file mode 100644
index 00000000000..5a426182b1a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsSlimeConverter.java
@@ -0,0 +1,70 @@
+// 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.server.configchange;
+
+import com.yahoo.config.model.api.ServiceInfo;
+import com.yahoo.slime.Cursor;
+
+import java.util.Set;
+
+/**
+ * Class used to convert a ConfigChangeActions instance to Slime.
+ *
+ * @author geirst
+ * @since 5.44
+ */
+public class ConfigChangeActionsSlimeConverter {
+ private final ConfigChangeActions actions;
+
+ public ConfigChangeActionsSlimeConverter(ConfigChangeActions actions) {
+ this.actions = actions;
+ }
+
+ public void toSlime(Cursor root) {
+ Cursor actionsCursor = root.setObject("configChangeActions");
+ restartActionsToSlime(actionsCursor);
+ refeedActionsToSlime(actionsCursor);
+ }
+
+ private void restartActionsToSlime(Cursor actionsCursor) {
+ Cursor restartCursor = actionsCursor.setArray("restart");
+ for (RestartActions.Entry entry : actions.getRestartActions().getEntries()) {
+ Cursor entryCursor = restartCursor.addObject();
+ entryCursor.setString("clusterName", entry.getClusterName());
+ entryCursor.setString("clusterType", entry.getClusterType());
+ entryCursor.setString("serviceType", entry.getServiceType());
+ messagesToSlime(entryCursor, entry.getMessages());
+ servicesToSlime(entryCursor, entry.getServices());
+ }
+ }
+
+ private void refeedActionsToSlime(Cursor actionsCursor) {
+ Cursor refeedCursor = actionsCursor.setArray("refeed");
+ for (RefeedActions.Entry entry : actions.getRefeedActions().getEntries()) {
+ Cursor entryCursor = refeedCursor.addObject();
+ entryCursor.setString("name", entry.name());
+ entryCursor.setBool("allowed", entry.allowed());
+ entryCursor.setString("documentType", entry.getDocumentType());
+ entryCursor.setString("clusterName", entry.getClusterName());
+ messagesToSlime(entryCursor, entry.getMessages());
+ servicesToSlime(entryCursor, entry.getServices());
+ }
+ }
+
+ private static void messagesToSlime(Cursor entryCursor, Set<String> messages) {
+ Cursor messagesCursor = entryCursor.setArray("messages");
+ for (String message : messages) {
+ messagesCursor.addString(message);
+ }
+ }
+
+ private static void servicesToSlime(Cursor entryCursor, Set<ServiceInfo> services) {
+ Cursor servicesCursor = entryCursor.setArray("services");
+ for (ServiceInfo service : services) {
+ Cursor serviceCursor = servicesCursor.addObject();
+ serviceCursor.setString("serviceName", service.getServiceName());
+ serviceCursor.setString("serviceType", service.getServiceType());
+ serviceCursor.setString("configId", service.getConfigId());
+ serviceCursor.setString("hostName", service.getHostName());
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActions.java b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActions.java
new file mode 100644
index 00000000000..87b7b466fbe
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActions.java
@@ -0,0 +1,91 @@
+// 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.server.configchange;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.api.ConfigChangeRefeedAction;
+import com.yahoo.config.model.api.ServiceInfo;
+
+import java.util.*;
+
+/**
+ * Represents all actions to re-feed document types in order to handle config changes.
+ *
+ * @author geirst
+ * @since 5.44
+ */
+public class RefeedActions {
+
+ public static class Entry {
+
+ private final String name;
+ private final boolean allowed;
+ private final String documentType;
+ private final String clusterName;
+ private final Set<ServiceInfo> services = new LinkedHashSet<>();
+ private final Set<String> messages = new TreeSet<>();
+
+ private Entry(String name, boolean allowed, String documentType, String clusterName) {
+ this.name = name;
+ this.allowed = allowed;
+ this.documentType = documentType;
+ this.clusterName = clusterName;
+ }
+
+ private Entry addService(ServiceInfo service) {
+ services.add(service);
+ return this;
+ }
+
+ private Entry addMessage(String message) {
+ messages.add(message);
+ return this;
+ }
+
+ public String name() { return name; }
+
+ public boolean allowed() { return allowed; }
+
+ public String getDocumentType() { return documentType; }
+
+ public String getClusterName() { return clusterName; }
+
+ public Set<ServiceInfo> getServices() { return services; }
+
+ public Set<String> getMessages() { return messages; }
+
+ }
+
+ private Entry addEntry(String name, boolean allowed, String documentType, ServiceInfo service) {
+ String clusterName = service.getProperty("clustername").orElse("");
+ String entryId = name + "." + allowed + "." + clusterName + "." + documentType;
+ Entry entry = actions.get(entryId);
+ if (entry == null) {
+ entry = new Entry(name, allowed, documentType, clusterName);
+ actions.put(entryId, entry);
+ }
+ return entry;
+ }
+
+ private final Map<String, Entry> actions = new TreeMap<>();
+
+ public RefeedActions() {
+ }
+
+ public RefeedActions(List<ConfigChangeAction> actions) {
+ for (ConfigChangeAction action : actions) {
+ if (action.getType().equals(ConfigChangeAction.Type.REFEED)) {
+ ConfigChangeRefeedAction refeedAction = (ConfigChangeRefeedAction) action;
+ for (ServiceInfo service : refeedAction.getServices()) {
+ addEntry(refeedAction.name(), refeedAction.allowed(), refeedAction.getDocumentType(), service).
+ addService(service).
+ addMessage(action.getMessage());
+ }
+ }
+ }
+ }
+
+ public List<Entry> getEntries() {
+ return new ArrayList<>(actions.values());
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActionsFormatter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActionsFormatter.java
new file mode 100644
index 00000000000..ae72c61bcdb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActionsFormatter.java
@@ -0,0 +1,33 @@
+// 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.server.configchange;
+
+/**
+ * Class used to format re-feed actions for human readability.
+ *
+ * @author geirst
+ * @since 5.44
+ */
+public class RefeedActionsFormatter {
+
+ private final RefeedActions actions;
+
+ public RefeedActionsFormatter(RefeedActions actions) {
+ this.actions = actions;
+ }
+
+ public String format() {
+ StringBuilder builder = new StringBuilder();
+ for (RefeedActions.Entry entry : actions.getEntries()) {
+ if (entry.allowed())
+ builder.append("(allowed) ");
+ builder.append(entry.name() + ": Consider removing data and re-feed document type '" + entry.getDocumentType() +
+ "' in cluster '" + entry.getClusterName() + "' because:\n");
+ int counter = 1;
+ for (String message : entry.getMessages()) {
+ builder.append(" " + (counter++) + ") " + message + "\n");
+ }
+ }
+ return builder.toString();
+ }
+
+} \ No newline at end of file
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActions.java b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActions.java
new file mode 100644
index 00000000000..6c2c080e6e4
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActions.java
@@ -0,0 +1,95 @@
+// 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.server.configchange;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.api.ServiceInfo;
+
+import java.util.*;
+
+/**
+ * Represents all actions to restart services in order to handle a config change.
+ *
+ * @author geirst
+ * @since 5.44
+ */
+public class RestartActions {
+
+ public static class Entry {
+
+ private final String clusterName;
+ private final String clusterType;
+ private final String serviceType;
+ private final Set<ServiceInfo> services = new LinkedHashSet<>();
+ private final Set<String> messages = new TreeSet<>();
+
+ private Entry addService(ServiceInfo service) {
+ services.add(service);
+ return this;
+ }
+
+ private Entry addMessage(String message) {
+ messages.add(message);
+ return this;
+ }
+
+ private Entry(String clusterName, String clusterType, String serviceType) {
+ this.clusterName = clusterName;
+ this.clusterType = clusterType;
+ this.serviceType = serviceType;
+ }
+
+ public String getClusterName() {
+ return clusterName;
+ }
+
+ public String getClusterType() {
+ return clusterType;
+ }
+
+ public String getServiceType() {
+ return serviceType;
+ }
+
+ public Set<ServiceInfo> getServices() {
+ return services;
+ }
+
+ public Set<String> getMessages() {
+ return messages;
+ }
+
+ }
+
+ private Entry addEntry(ServiceInfo service) {
+ String clusterName = service.getProperty("clustername").orElse("");
+ String clusterType = service.getProperty("clustertype").orElse("");
+ String entryId = clusterType + "." + clusterName + "." + service.getServiceType();
+ Entry entry = actions.get(entryId);
+ if (entry == null) {
+ entry = new Entry(clusterName, clusterType, service.getServiceType());
+ actions.put(entryId, entry);
+ }
+ return entry;
+ }
+
+ private final Map<String, Entry> actions = new TreeMap<>();
+
+ public RestartActions() {
+ }
+
+ public RestartActions(List<ConfigChangeAction> actions) {
+ for (ConfigChangeAction action : actions) {
+ if (action.getType().equals(ConfigChangeAction.Type.RESTART)) {
+ for (ServiceInfo service : action.getServices()) {
+ addEntry(service).
+ addService(service).
+ addMessage(action.getMessage());
+ }
+ }
+ }
+ }
+
+ public List<Entry> getEntries() {
+ return new ArrayList<>(actions.values());
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActionsFormatter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActionsFormatter.java
new file mode 100644
index 00000000000..7d61f31ac47
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActionsFormatter.java
@@ -0,0 +1,30 @@
+// 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.server.configchange;
+
+/**
+ * Class used to format restart actions for human readability.
+ *
+ * @author geirst
+ * @since 5.44
+ */
+public class RestartActionsFormatter {
+
+ private final RestartActions actions;
+
+ public RestartActionsFormatter(RestartActions actions) {
+ this.actions = actions;
+ }
+
+ public String format() {
+ StringBuilder builder = new StringBuilder();
+ for (RestartActions.Entry entry : actions.getEntries()) {
+ builder.append("In cluster '" + entry.getClusterName() + "' of type '" + entry.getClusterType() + "':\n");
+ builder.append(" Restart services of type '" + entry.getServiceType() + "' because:\n");
+ int counter = 1;
+ for (String message : entry.getMessages()) {
+ builder.append(" " + counter++ + ") " + message + "\n");
+ }
+ }
+ return builder.toString();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/counter/package-info.java b/configserver/src/main/java/com/yahoo/vespa/config/server/counter/package-info.java
new file mode 100644
index 00000000000..219e1008f3a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/counter/package-info.java
@@ -0,0 +1,8 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author andreer
+ */
+@ExportPackage
+package com.yahoo.vespa.config.server.counter;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployer.java
new file mode 100644
index 00000000000..00a036df9d5
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployer.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.server.deploy;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Provisioner;
+import com.yahoo.vespa.config.server.ActivateLock;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.session.LocalSession;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+import com.yahoo.vespa.config.server.session.SilentDeployLogger;
+import com.yahoo.vespa.curator.Curator;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.util.Optional;
+
+/**
+ * The API for deploying applications.
+ * A class which needs to deploy applications can have an instance of this injected.
+ *
+ * @author bratseth
+ */
+public class Deployer implements com.yahoo.config.provision.Deployer {
+
+ private final Tenants tenants;
+ private final Optional<Provisioner> hostProvisioner;
+ private final ConfigserverConfig configserverConfig;
+ private final Curator curator;
+ private final Clock clock;
+ private final DeployLogger logger = new SilentDeployLogger();
+
+ public Deployer(Tenants tenants, HostProvisionerProvider hostProvisionerProvider,
+ ConfigserverConfig configserverConfig, Curator curator) {
+ this.tenants = tenants;
+ this.hostProvisioner = hostProvisionerProvider.getHostProvisioner();
+ this.configserverConfig = configserverConfig;
+ this.curator = curator;
+ this.clock = Clock.systemUTC();
+ }
+
+ /**
+ * Creates a new deployment from the active application, if available.
+ *
+ * @param application the active application to be redeployed
+ * @param timeout the timeout to use for each individual deployment operation
+ * @return a new deployment from the local active, or empty if a local active application
+ * was not present for this id (meaning it either is not active or active on another
+ * node in the config server cluster)
+ */
+ @Override
+ public Optional<com.yahoo.config.provision.Deployment> deployFromLocalActive(ApplicationId application, Duration timeout) {
+ Tenant tenant = tenants.tenantsCopy().get(application.tenant());
+ LocalSession activeSession = tenant.getLocalSessionRepo().getActiveSession(application);
+ if (activeSession == null) return Optional.empty();
+ TimeoutBudget timeoutBudget = new TimeoutBudget(clock, timeout);
+ LocalSession newSession = tenant.getSessionFactory().createSessionFromExisting(activeSession, logger, timeoutBudget);
+ tenant.getLocalSessionRepo().addSession(newSession);
+ return Optional.of(Deployment.unprepared(newSession,
+ tenant.getLocalSessionRepo(),
+ tenant.getPath(),
+ configserverConfig,
+ hostProvisioner,
+ new ActivateLock(curator, tenant.getPath()),
+ timeout, clock));
+ }
+
+ public Deployment deployFromPreparedSession(LocalSession session, ActivateLock lock, LocalSessionRepo localSessionRepo, Duration timeout) {
+ return Deployment.prepared(session,
+ localSessionRepo,
+ hostProvisioner,
+ lock,
+ timeout, clock);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployment.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployment.java
new file mode 100644
index 00000000000..cf366fd21f7
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployment.java
@@ -0,0 +1,202 @@
+// 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.server.deploy;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.application.api.ApplicationMetaData;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.provision.HostFilter;
+import com.yahoo.config.provision.Provisioner;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.transaction.NestedTransaction;
+import com.yahoo.transaction.Transaction;
+import com.yahoo.vespa.config.server.ActivateLock;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.http.InternalServerException;
+import com.yahoo.vespa.config.server.session.LocalSession;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+import com.yahoo.vespa.config.server.session.PrepareParams;
+import com.yahoo.vespa.config.server.session.Session;
+import com.yahoo.vespa.config.server.session.SilentDeployLogger;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.util.Optional;
+import java.util.logging.Logger;
+
+/**
+ * The process of deploying an application.
+ * Deployments are created by a {@link Deployer}.
+ * Instances of this are not multithread safe.
+ *
+ * @author lulf
+ * @author bratseth
+ */
+public class Deployment implements com.yahoo.config.provision.Deployment {
+
+ private static final Logger log = Logger.getLogger(Deployment.class.getName());
+
+ /** The session containing the application instance to activate */
+ private final LocalSession session;
+ private final LocalSessionRepo localSessionRepo;
+ /** The path to the tenant, or null if not available (only used during prepare) */
+ private final Path tenantPath;
+ /** The config server config, or null if not available (only used during prepare) */
+ private final ConfigserverConfig configserverConfig;
+ private final Optional<Provisioner> hostProvisioner;
+ private final ActivateLock activateLock;
+ private final Duration timeout;
+ private final Clock clock;
+ private final DeployLogger logger = new SilentDeployLogger();
+
+ private boolean prepared = false;
+
+ private boolean ignoreLockFailure = false;
+ private boolean ignoreSessionStaleFailure = false;
+
+ private Deployment(LocalSession session, LocalSessionRepo localSessionRepo, Path tenantPath, ConfigserverConfig configserverConfig,
+ Optional<Provisioner> hostProvisioner, ActivateLock activateLock,
+ Duration timeout, Clock clock, boolean prepared) {
+ this.session = session;
+ this.localSessionRepo = localSessionRepo;
+ this.tenantPath = tenantPath;
+ this.configserverConfig = configserverConfig;
+ this.hostProvisioner = hostProvisioner;
+ this.activateLock = activateLock;
+ this.timeout = timeout;
+ this.clock = clock;
+ this.prepared = prepared;
+ }
+
+ static Deployment unprepared(LocalSession session, LocalSessionRepo localSessionRepo, Path tenantPath, ConfigserverConfig configserverConfig,
+ Optional<Provisioner> hostProvisioner, ActivateLock activateLock,
+ Duration timeout, Clock clock) {
+ return new Deployment(session, localSessionRepo, tenantPath, configserverConfig, hostProvisioner, activateLock,
+ timeout, clock, false);
+ }
+
+ static Deployment prepared(LocalSession session, LocalSessionRepo localSessionRepo,
+ Optional<Provisioner> hostProvisioner, ActivateLock activateLock,
+ Duration timeout, Clock clock) {
+ return new Deployment(session, localSessionRepo, null, null, hostProvisioner, activateLock,
+ timeout, clock, true);
+ }
+
+ public Deployment setIgnoreLockFailure(boolean ignoreLockFailure) {
+ this.ignoreLockFailure = ignoreLockFailure;
+ return this;
+ }
+
+ public Deployment setIgnoreSessionStaleFailure(boolean ignoreSessionStaleFailure) {
+ this.ignoreSessionStaleFailure = ignoreSessionStaleFailure;
+ return this;
+ }
+
+ /** Prepares this. This does nothing if this is already prepared */
+ @Override
+ public void prepare() {
+ if (prepared) return;
+ TimeoutBudget timeoutBudget = new TimeoutBudget(clock, timeout);
+ session.prepare(logger,
+ /** Assumes that session has already set application id, see {@link com.yahoo.vespa.config.server.session.SessionFactoryImpl}. */
+ new PrepareParams(configserverConfig).applicationId(session.getApplicationId()).timeoutBudget(timeoutBudget),
+ Optional.empty(),
+ tenantPath);
+ this.prepared = true;
+ }
+
+ /** Activates this. If it is not already prepared, this will call prepare first. */
+ @Override
+ public void activate() {
+ if (! prepared)
+ prepare();
+
+ TimeoutBudget timeoutBudget = new TimeoutBudget(clock, timeout);
+ long sessionId = session.getSessionId();
+ validateSessionStatus(session);
+ try {
+ activateLock.acquire(timeoutBudget, ignoreLockFailure);
+ NestedTransaction transaction = new NestedTransaction();
+ transaction.add(deactivateCurrentActivateNew(localSessionRepo.getActiveSession(session.getApplicationId()), session, ignoreSessionStaleFailure));
+ if (hostProvisioner.isPresent() && !session.getApplicationId().isHostedVespaRoutingApplication()) {
+ ProvisionInfo info = session.getProvisionInfo();
+ hostProvisioner.get().activate(transaction, session.getApplicationId(), info.getHosts());
+ }
+ transaction.commit();
+ session.waitUntilActivated(timeoutBudget);
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new InternalServerException("Error activating application", e);
+ } finally {
+ activateLock.release();
+ }
+ final ApplicationMetaData metaData = session.getMetaData();
+ log.log(LogLevel.INFO, session.logPre() + "Session " + sessionId + " activated successfully. Config generation " + metaData.getGeneration());
+ }
+
+ /**
+ * Request a restart of services of this application on hosts matching the filter.
+ * This is sometimes needed after activation, but can also be requested without
+ * doing prepare and activate in the same session.
+ */
+ public void restart(HostFilter filter) {
+ hostProvisioner.get().restart(session.getApplicationId(), filter);
+ }
+
+ private long validateSessionStatus(LocalSession localSession) {
+ long sessionId = localSession.getSessionId();
+ if (Session.Status.NEW.equals(localSession.getStatus())) {
+ throw new IllegalStateException(localSession.logPre() + "Session " + sessionId + " is not prepared");
+ } else if (Session.Status.ACTIVATE.equals(localSession.getStatus())) {
+ throw new IllegalArgumentException(localSession.logPre() + "Session " + sessionId + " is already active");
+ }
+ return sessionId;
+ }
+
+ private Transaction deactivateCurrentActivateNew(LocalSession currentActiveSession, LocalSession session, boolean ignoreStaleSessionFailure) {
+ Transaction transaction = session.createActivateTransaction();
+ if (isValidSession(currentActiveSession)) {
+ checkIfActiveHasChanged(session, currentActiveSession, ignoreStaleSessionFailure);
+ checkIfActiveIsNewerThanSessionToBeActivated(session.getSessionId(), currentActiveSession.getSessionId());
+ transaction.add(currentActiveSession.createDeactivateTransaction().operations());
+ }
+ return transaction;
+ }
+
+ private boolean isValidSession(LocalSession session) {
+ return session != null;
+ }
+
+ private void checkIfActiveHasChanged(LocalSession session, LocalSession currentActiveSession, boolean ignoreStaleSessionFailure) {
+ long activeSessionAtCreate = session.getActiveSessionAtCreate();
+ log.log(LogLevel.DEBUG, currentActiveSession.logPre()+"active session id at create time=" + activeSessionAtCreate);
+ if (activeSessionAtCreate == 0) return; // No active session at create
+
+ long sessionId = session.getSessionId();
+ long currentActiveSessionSessionId = currentActiveSession.getSessionId();
+ log.log(LogLevel.DEBUG, currentActiveSession.logPre()+"sessionId=" + sessionId + ", current active session=" + currentActiveSessionSessionId);
+ if (currentActiveSession.isNewerThan(activeSessionAtCreate) &&
+ currentActiveSessionSessionId != sessionId) {
+ String errMsg = currentActiveSession.logPre()+"Cannot activate session " +
+ sessionId + " because the currently active session (" +
+ currentActiveSessionSessionId + ") has changed since session " + sessionId +
+ " was created (was " + activeSessionAtCreate + " at creation time)";
+ if (ignoreStaleSessionFailure) {
+ log.warning(errMsg+ " (Continuing because of force.)");
+ } else {
+ throw new IllegalStateException(errMsg);
+ }
+ }
+ }
+
+ // As of now, config generation is based on session id, and config generation must be an monotonically
+ // increasing number
+ private void checkIfActiveIsNewerThanSessionToBeActivated(long sessionId, long currentActiveSessionId) {
+ if (sessionId < currentActiveSessionId) {
+ throw new IllegalArgumentException("It is not possible to activate session " + sessionId +
+ ", because it is older than current active session (" + currentActiveSessionId + ")");
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java
new file mode 100644
index 00000000000..9a6f373a807
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java
@@ -0,0 +1,160 @@
+// 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.server.deploy;
+
+import com.yahoo.config.model.api.*;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Rotation;
+import com.yahoo.config.provision.Version;
+import com.yahoo.config.provision.Zone;
+
+import java.io.File;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Implementation of {@link ModelContext} for configserver.
+ *
+ * @author lulf
+ */
+public class ModelContextImpl implements ModelContext {
+ private final ApplicationPackage applicationPackage;
+ private final Optional<Model> previousModel;
+ private final Optional<ApplicationPackage> permanentApplicationPackage;
+ private final DeployLogger deployLogger;
+ private final ConfigDefinitionRepo configDefinitionRepo;
+ private final FileRegistry fileRegistry;
+ private final Optional<HostProvisioner> hostProvisioner;
+ private final ModelContext.Properties properties;
+ private final Optional<File> appDir;
+ Optional<Version> vespaVersion;
+
+ public ModelContextImpl(ApplicationPackage applicationPackage,
+ Optional<Model> previousModel,
+ Optional<ApplicationPackage> permanentApplicationPackage,
+ DeployLogger deployLogger,
+ ConfigDefinitionRepo configDefinitionRepo,
+ FileRegistry fileRegistry,
+ Optional<HostProvisioner> hostProvisioner,
+ ModelContext.Properties properties,
+ Optional<File> appDir,
+ Optional<Version> vespaVersion) {
+ this.applicationPackage = applicationPackage;
+ this.previousModel = previousModel;
+ this.permanentApplicationPackage = permanentApplicationPackage;
+ this.deployLogger = deployLogger;
+ this.configDefinitionRepo = configDefinitionRepo;
+ this.fileRegistry = fileRegistry;
+ this.hostProvisioner = hostProvisioner;
+ this.properties = properties;
+ this.appDir = appDir;
+ this.vespaVersion = vespaVersion;
+ }
+
+ @Override
+ public ApplicationPackage applicationPackage() {
+ return applicationPackage;
+ }
+
+ @Override
+ public Optional<Model> previousModel() {
+ return previousModel;
+ }
+
+ @Override
+ public Optional<ApplicationPackage> permanentApplicationPackage() {
+ return permanentApplicationPackage;
+ }
+
+ @Override
+ public Optional<HostProvisioner> hostProvisioner() {
+ return hostProvisioner;
+ }
+
+ @Override
+ public DeployLogger deployLogger() {
+ return deployLogger;
+ }
+
+ @Override
+ public ConfigDefinitionRepo configDefinitionRepo() {
+ return configDefinitionRepo;
+ }
+
+ @Override
+ public FileRegistry getFileRegistry() {
+ return fileRegistry;
+ }
+
+ @Override
+ public ModelContext.Properties properties() {
+ return properties;
+ }
+
+ @Override
+ public Optional<File> appDir() {
+ return appDir;
+ }
+
+ @Override
+ public Optional<Version> vespaVersion() { return vespaVersion; }
+
+ /**
+ * @author lulf
+ */
+ public static class Properties implements ModelContext.Properties {
+ private final ApplicationId applicationId;
+ private final boolean multitenant;
+ private final List<ConfigServerSpec> configServerSpecs;
+ private final boolean hostedVespa;
+ private final Zone zone;
+ private final Set<Rotation> rotations;
+
+ public Properties(ApplicationId applicationId,
+ boolean multitenant,
+ List<ConfigServerSpec> configServerSpecs,
+ boolean hostedVespa,
+ Zone zone,
+ Set<Rotation> rotations) {
+ this.applicationId = applicationId;
+ this.multitenant = multitenant;
+ this.configServerSpecs = configServerSpecs;
+ this.hostedVespa = hostedVespa;
+ this.zone = zone;
+ this.rotations = rotations;
+ }
+
+ @Override
+ public boolean multitenant() {
+ return multitenant;
+ }
+
+ @Override
+ public ApplicationId applicationId() {
+ return applicationId;
+ }
+
+ @Override
+ public List<ConfigServerSpec> configServerSpecs() {
+ return configServerSpecs;
+ }
+
+ @Override
+ public boolean hostedVespa() {
+ return hostedVespa;
+ }
+
+ @Override
+ public Zone zone() {
+ return zone;
+ }
+
+ @Override
+ public Set<Rotation> rotations() {
+ return rotations;
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/TenantFileSystemDirs.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/TenantFileSystemDirs.java
new file mode 100644
index 00000000000..336b50351bb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/TenantFileSystemDirs.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.
+package com.yahoo.vespa.config.server.deploy;
+
+import com.google.common.io.Files;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.io.IOUtils;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.ConfigServerDB;
+
+import java.io.File;
+
+/*
+ * Holds file system directories for a tenant
+ *
+ * @author tonytv
+ */
+public class TenantFileSystemDirs {
+
+ private final File serverDB;
+ private final TenantName tenant;
+
+ public TenantFileSystemDirs(File dir, TenantName tenant) {
+ this.serverDB = dir;
+ this.tenant = tenant;
+ ConfigServerDB.cr(path());
+ }
+
+ public static TenantFileSystemDirs createTestDirs(TenantName tenantName) {
+ return new TenantFileSystemDirs(Files.createTempDir(), tenantName);
+ }
+
+ public File path() {
+ return new File(serverDB, Path.fromString("tenants").append(tenant.value()).append("sessions").getRelative());
+ }
+
+ public File getUserApplicationDir(long generation) {
+ return new File(path(), String.valueOf(generation));
+ }
+
+ public String getPath() {
+ return serverDB.getPath();
+ }
+
+ public void delete() {
+ IOUtils.recursiveDeleteDir(new File(serverDB, Path.fromString("tenants").append(tenant.value()).getRelative()));
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClient.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClient.java
new file mode 100644
index 00000000000..9c6f21f3511
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClient.java
@@ -0,0 +1,378 @@
+// 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.server.deploy;
+
+import com.yahoo.config.application.api.ApplicationMetaData;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.UnparsedConfigDefinition;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.config.provision.Version;
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.server.zookeeper.ZKApplicationPackage;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.config.model.application.provider.PreGeneratedFileRegistry;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+import org.apache.commons.io.IOUtils;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * A class used for reading and writing application data to zookeeper.
+ *
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1
+ */
+public class ZooKeeperClient {
+
+ private final ConfigCurator configCurator;
+ private final DeployLogger logger;
+ private final boolean trace;
+ /* This is the generation that will be used for reading and writing application data. (1 more than last deployed application) */
+ private final Path rootPath;
+
+ static final ApplicationFile.PathFilter xmlFilter = new ApplicationFile.PathFilter() {
+ @Override
+ public boolean accept(Path path) {
+ return path.getName().endsWith(".xml");
+ }
+ };
+
+ public ZooKeeperClient(ConfigCurator configCurator, DeployLogger logger, boolean trace, Path rootPath) {
+ this.configCurator = configCurator;
+ this.logger = logger;
+ this.trace = trace;
+ this.rootPath = rootPath;
+ }
+
+ /**
+ * Sets up basic node structure in ZooKeeper and purges old data.
+ * This is the first operation on ZK during deploy-application.
+ *
+ * We have retries in this method because there have been cases of stray connection loss to ZK,
+ * even though the user has started the config server.
+ *
+ */
+ void setupZooKeeper() {
+ int retries = 5;
+ try {
+ while (retries > 0) {
+ try {
+ trace("Setting up ZooKeeper nodes for this application");
+ createZooKeeperNodes();
+ break;
+ } catch (RuntimeException e) {
+ logger.log(LogLevel.FINE, "ZK init failed, retrying: " + e);
+ retries--;
+ if (retries == 0) {
+ throw e;
+ }
+ Thread.sleep(100);
+ // Not reconnecting, ZK is supposed to handle that automatically
+ // as long as the session doesn't expire. We'll see.
+ }
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to initialize vespa model writing to config server(s) " +
+ System.getProperty("configsources") + "\n" +
+ "Please ensure that cloudconfig_server is started on the config server node(s), " +
+ "and check the vespa log for configserver errors. ", e);
+ }
+ }
+
+ /** Sets the app id and attempts to set up zookeeper. The app id must be ordered for purge to work OK. */
+ private void createZooKeeperNodes() {
+ if (!configCurator.exists(rootPath.getAbsolute())) {
+ configCurator.createNode(rootPath.getAbsolute());
+ }
+
+ for (String subPath : Arrays.asList(
+ ConfigCurator.DEFCONFIGS_ZK_SUBPATH,
+ ConfigCurator.USER_DEFCONFIGS_ZK_SUBPATH,
+ ConfigCurator.USERAPP_ZK_SUBPATH,
+ ZKApplicationPackage.fileRegistryNode)) {
+ // TODO The replaceFirst below is hackish.
+ configCurator.createNode(getZooKeeperAppPath(null).getAbsolute(), subPath.replaceFirst("/", ""));
+ }
+ }
+
+ /**
+ * Feeds def files and user config into ZK.
+ *
+ * @param app the application package to feed to zookeeper
+ */
+ void feedZooKeeper(ApplicationPackage app) {
+ trace("Feeding application config into ZooKeeper");
+ // gives lots and lots of debug output: // BasicConfigurator.configure();
+ try {
+ trace("zk operations: " + configCurator.getNumberOfOperations());
+ trace("zk operations: " + configCurator.getNumberOfOperations());
+ trace("Feeding user def files into ZooKeeper");
+ feedZKUserDefs(app);
+ trace("zk operations: " + configCurator.getNumberOfOperations());
+ trace("Feeding application package into ZooKeeper");
+ // TODO 1200 zk operations done in the below method
+ feedZKAppPkg(app);
+ feedSearchDefinitions(app);
+ feedZKUserIncludeDirs(app, app.getUserIncludeDirs());
+ trace("zk operations: " + configCurator.getNumberOfOperations());
+ trace("zk read operations: " + configCurator.getNumberOfReadOperations());
+ trace("zk write operations: " + configCurator.getNumberOfWriteOperations());
+ trace("Feeding sd from docproc bundle into ZooKeeper");
+ trace("zk operations: " + configCurator.getNumberOfOperations());
+ trace("Write application metadata into ZooKeeper");
+ feedZKAppMetaData(app.getMetaData());
+ trace("zk operations: " + configCurator.getNumberOfOperations());
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to write vespa model to config server(s) " + System.getProperty("configsources") + "\n" +
+ "Please ensure that cloudconfig_server is started on the config server node(s), " +
+ "and check the vespa log for configserver errors. ", e);
+ }
+ }
+
+ private void feedSearchDefinitions(ApplicationPackage app) throws IOException {
+ Collection<NamedReader> sds = app.getSearchDefinitions();
+ if (sds.isEmpty()) {
+ return;
+ }
+ Path zkPath = getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.SEARCH_DEFINITIONS_DIR);
+ configCurator.createNode(zkPath.getAbsolute());
+ // Ensures that ranking expressions and other files are also fed.
+ feedDirZooKeeper(app.getFile(ApplicationPackage.SEARCH_DEFINITIONS_DIR), zkPath, false);
+ for (NamedReader sd : sds) {
+ String name = sd.getName();
+ Reader reader = sd.getReader();
+ String data = com.yahoo.io.IOUtils.readAll(reader);
+ reader.close();
+ configCurator.putData(zkPath.getAbsolute(), name, data);
+ }
+ }
+
+ /**
+ * Puts the application package files into ZK
+ *
+ * @param app The application package to use as input.
+ * @throws java.io.IOException if not able to write to Zookeeper
+ */
+ void feedZKAppPkg(ApplicationPackage app) throws IOException {
+ ApplicationFile.PathFilter srFilter = new ApplicationFile.PathFilter() {
+ @Override
+ public boolean accept(Path path) {
+ return path.getName().endsWith(ApplicationPackage.RULES_NAME_SUFFIX);
+ }
+ };
+ // Copy app package files and subdirs into zk
+ // TODO: We should have a way of doing this which doesn't require repeating all the content
+ feedFileZooKeeper(app.getFile(Path.fromString(ApplicationPackage.SERVICES)),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH));
+ feedFileZooKeeper(app.getFile(Path.fromString(ApplicationPackage.HOSTS)),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH));
+ feedFileZooKeeper(app.getFile(Path.fromString(ApplicationPackage.DEPLOYMENT_FILE.getName())),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH));
+ feedFileZooKeeper(app.getFile(Path.fromString(ApplicationPackage.VALIDATION_OVERRIDES.getName())),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH));
+
+ feedDirZooKeeper(app.getFile(Path.fromString(ApplicationPackage.TEMPLATES_DIR)),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH),
+ true);
+ feedDirZooKeeper(app.getFile(ApplicationPackage.RULES_DIR),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.RULES_DIR),
+ srFilter, true);
+ feedDirZooKeeper(app.getFile(ApplicationPackage.QUERY_PROFILES_DIR),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.QUERY_PROFILES_DIR),
+ xmlFilter, true);
+ feedDirZooKeeper(app.getFile(ApplicationPackage.PAGE_TEMPLATES_DIR),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.PAGE_TEMPLATES_DIR),
+ xmlFilter, true);
+ feedDirZooKeeper(app.getFile(Path.fromString(ApplicationPackage.SEARCHCHAINS_DIR)),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.SEARCHCHAINS_DIR),
+ xmlFilter, true);
+ feedDirZooKeeper(app.getFile(Path.fromString(ApplicationPackage.DOCPROCCHAINS_DIR)),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.DOCPROCCHAINS_DIR),
+ xmlFilter, true);
+ feedDirZooKeeper(app.getFile(Path.fromString(ApplicationPackage.ROUTINGTABLES_DIR)),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.ROUTINGTABLES_DIR),
+ xmlFilter, true);
+ feedDirZooKeeper(app.getFile(Path.fromString(ApplicationPackage.FILES_DIR)),
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.FILES_DIR),
+ true);
+ }
+
+ private void feedDirZooKeeper(ApplicationFile file, Path zooKeeperAppPath, boolean recurse) throws IOException {
+ feedDirZooKeeper(file, zooKeeperAppPath, new ApplicationFile.PathFilter() {
+ @Override
+ public boolean accept(Path path) {
+ return true;
+ }
+ }, recurse);
+ }
+
+ private void feedDirZooKeeper(ApplicationFile dir, Path path, ApplicationFile.PathFilter filenameFilter, boolean recurse) throws IOException {
+ if (!dir.isDirectory()) {
+ logger.log(LogLevel.FINE, dir.getPath().getAbsolute()+" is not a directory. Not feeding the files into ZooKeeper.");
+ return;
+ }
+ for (ApplicationFile file: listFiles(dir, filenameFilter)) {
+ String name = file.getPath().getName();
+ if (name.startsWith(".")) continue; //.svn , .git ...
+ if ("CVS".equals(name)) continue;
+ if (file.isDirectory()) {
+ configCurator.createNode(path.append(name).getAbsolute());
+ if (recurse) {
+ feedDirZooKeeper(file, path.append(name), filenameFilter, recurse);
+ }
+ } else {
+ feedFileZooKeeper(file, path);
+ }
+ }
+ }
+
+ /**
+ * Like {@link ApplicationFile#listFiles(com.yahoo.config.application.api.ApplicationFile.PathFilter)} with a slightly different semantic. Never filter out directories.
+ */
+ private List<ApplicationFile> listFiles(ApplicationFile dir, ApplicationFile.PathFilter filter) {
+ List<ApplicationFile> rawList = dir.listFiles();
+ List<ApplicationFile> ret = new ArrayList<>();
+ if (rawList != null) {
+ for (ApplicationFile f : rawList) {
+ if (f.isDirectory()) {
+ ret.add(f);
+ } else {
+ if (filter.accept(f.getPath())) {
+ ret.add(f);
+ }
+ }
+ }
+ }
+ return ret;
+ }
+
+ private void feedFileZooKeeper(ApplicationFile file, Path zkPath) throws IOException {
+ if (!file.exists()) {
+ return;
+ }
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (InputStream inputStream = file.createInputStream()) {
+ IOUtils.copy(inputStream, baos);
+ baos.flush();
+ configCurator.putData(zkPath.append(file.getPath().getName()).getAbsolute(), baos.toByteArray());
+ }
+ }
+
+ private void feedZKUserIncludeDirs(ApplicationPackage applicationPackage, List<String> userIncludeDirs) throws IOException {
+ // User defined include directories
+ for (String userInclude : userIncludeDirs) {
+ ApplicationFile dir = applicationPackage.getFile(Path.fromString(userInclude));
+ final List<ApplicationFile> files = dir.listFiles();
+ if (files == null || files.isEmpty()) {
+ configCurator.createNode(getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH + "/" + userInclude).getAbsolute());
+ }
+ feedDirZooKeeper(dir,
+ getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH + "/" + userInclude),
+ xmlFilter, true);
+ }
+ }
+
+ /**
+ * Feeds all user-defined .def file from the application package into ZooKeeper (both into
+ * /defconfigs and /userdefconfigs
+ */
+ private void feedZKUserDefs(ApplicationPackage applicationPackage) {
+ Map<ConfigDefinitionKey, UnparsedConfigDefinition> configDefs = applicationPackage.getAllExistingConfigDefs();
+ for (Map.Entry<ConfigDefinitionKey, UnparsedConfigDefinition> entry : configDefs.entrySet()) {
+ ConfigDefinitionKey key = entry.getKey();
+ String contents = entry.getValue().getUnparsedContent();
+ feedDefToZookeeper(key.getName(), key.getNamespace(), getZooKeeperAppPath(ConfigCurator.USER_DEFCONFIGS_ZK_SUBPATH).getAbsolute(), contents);
+ feedDefToZookeeper(key.getName(), key.getNamespace(), getZooKeeperAppPath(ConfigCurator.DEFCONFIGS_ZK_SUBPATH).getAbsolute(), contents);
+ }
+ logger.log(LogLevel.FINE, configDefs.size() + " user config definitions");
+ }
+
+ private void feedDefToZookeeper(String name, String namespace, String path, String data) {
+ feedDefToZookeeper(name, namespace, "", path, com.yahoo.text.Utf8.toBytes(data));
+ }
+
+ private void feedDefToZookeeper(String name, String namespace, String version, String path, byte[] data) {
+ configCurator.putDefData(
+ ("".equals(namespace)) ? name : (namespace + "." + name),
+ version,
+ path,
+ data);
+ }
+
+ private void feedZKFileRegistry(Version vespaVersion, FileRegistry fileRegistry) {
+ trace("Feeding file registry data into ZooKeeper");
+ String exportedRegistry = PreGeneratedFileRegistry.exportRegistry(fileRegistry);
+
+ configCurator.putData(getZooKeeperAppPath(null).append(ZKApplicationPackage.fileRegistryNode).getAbsolute(),
+ vespaVersion.toSerializedForm(),
+ exportedRegistry);
+ }
+
+ /**
+ * Feeds application metadata to zookeeper. Used by vespamodel to create config
+ * for application metadata (used by ApplicationStatusHandler)
+ *
+ * @param metaData The application metadata.
+ */
+ private void feedZKAppMetaData(ApplicationMetaData metaData) {
+ configCurator.putData(getZooKeeperAppPath(ConfigCurator.META_ZK_PATH).getAbsolute(), metaData.asJsonString());
+ }
+
+ void cleanupZooKeeper() {
+ trace("Exception occurred. Cleaning up ZooKeeper");
+ try {
+ for (String subPath : Arrays.asList(
+ ConfigCurator.DEFCONFIGS_ZK_SUBPATH,
+ ConfigCurator.USER_DEFCONFIGS_ZK_SUBPATH,
+ ConfigCurator.USERAPP_ZK_SUBPATH)) {
+ configCurator.deleteRecurse(getZooKeeperAppPath(null).append(subPath).getAbsolute());
+ }
+ } catch (Exception e) {
+ logger.log(LogLevel.WARNING, "Could not clean up in zookeeper");
+ //Might be called in an exception handler before re-throw, so do not throw here.
+ }
+ }
+
+ /**
+ * Gets a full ZK app path based on id set in Admin object
+ *
+ *
+ * @param trailingPath trailing part of path to be appended to ZK app path
+ * @return a String with the full ZK application path including trailing path, if set
+ */
+ Path getZooKeeperAppPath(String trailingPath) {
+ if (trailingPath != null) {
+ return rootPath.append(trailingPath);
+ } else {
+ return rootPath;
+ }
+ }
+
+ void trace(String msg) {
+ if (trace) {
+ logger.log(LogLevel.FINE, msg);
+ }
+ }
+
+ private void feedProvisionInfo(Version version, ProvisionInfo info) throws IOException {
+ byte[] json = info.toJson();
+ configCurator.putData(rootPath.append(ZKApplicationPackage.allocatedHostsNode).append(version.toSerializedForm()).getAbsolute(), json);
+ }
+
+ public void feedZKFileRegistries(Map<Version, FileRegistry> fileRegistryMap) throws IOException {
+ for (Map.Entry<Version, FileRegistry> versionFileRegistryEntry : fileRegistryMap.entrySet()) {
+ feedZKFileRegistry(versionFileRegistryEntry.getKey(), versionFileRegistryEntry.getValue());
+ }
+ }
+
+ public void feedProvisionInfos(Map<Version, ProvisionInfo> provisionInfoMap) throws IOException {
+ for (Map.Entry<Version, ProvisionInfo> versionProvisionInfoEntry : provisionInfoMap.entrySet()) {
+ feedProvisionInfo(versionProvisionInfoEntry.getKey(), versionProvisionInfoEntry.getValue());
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperDeployer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperDeployer.java
new file mode 100644
index 00000000000..3a92c4a5ebe
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperDeployer.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.server.deploy;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.config.provision.Version;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * Interface for initializing zookeeper and deploying an application package to zookeeper.
+ * Initialize must be called before each deploy.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ZooKeeperDeployer {
+
+ private final ZooKeeperClient zooKeeperClient;
+
+ public ZooKeeperDeployer(ZooKeeperClient client) {
+ this.zooKeeperClient = client;
+ }
+
+ /**
+ * Deploys an application package to zookeeper. initialize() must be called before calling this method.
+ *
+ * @param applicationPackage The application package to persist.
+ * @param fileRegistryMap The file registries to persist.
+ * @param provisionInfoMap The provisioning infos to persist.
+ * @throws IOException if deploying fails
+ */
+ public void deploy(ApplicationPackage applicationPackage, Map<Version, FileRegistry> fileRegistryMap, Map<Version, ProvisionInfo> provisionInfoMap) throws IOException {
+ zooKeeperClient.setupZooKeeper();
+ zooKeeperClient.feedZooKeeper(applicationPackage);
+ zooKeeperClient.feedZKFileRegistries(fileRegistryMap);
+ zooKeeperClient.feedProvisionInfos(provisionInfoMap);
+ }
+
+ public void cleanup() {
+ zooKeeperClient.cleanupZooKeeper();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBHandler.java
new file mode 100644
index 00000000000..2f9c13587fe
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBHandler.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.
+package com.yahoo.vespa.config.server.filedistribution;
+
+import com.yahoo.config.FileReference;
+import com.yahoo.config.model.api.FileDistribution;
+import com.yahoo.vespa.filedistribution.FileDistributionManager;
+
+import java.util.*;
+
+/**
+ * Implements invoker of filedistribution using manager with JNI.
+ *
+ * @author tonytv
+ * @author lulf
+ * @since 5.1.14
+ */
+public class FileDBHandler implements FileDistribution {
+ private final FileDistributionManager manager;
+
+ public FileDBHandler(FileDistributionManager manager) {
+ this.manager = manager;
+ }
+
+ @Override
+ public void sendDeployedFiles(String hostName, Set<FileReference> fileReferences) {
+ List<String> referencesAsString = new ArrayList<>();
+ for (FileReference reference : fileReferences) {
+ referencesAsString.add(reference.value());
+ }
+ manager.setDeployedFiles(hostName, referencesAsString);
+ }
+
+ @Override
+ public void limitSendingOfDeployedFilesTo(Collection<String> hostNames) {
+ manager.limitSendingOfDeployedFilesTo(hostNames);
+ }
+
+ @Override
+ public void removeDeploymentsThatHaveDifferentApplicationId(Collection<String> targetHostnames) {
+ manager.removeDeploymentsThatHaveDifferentApplicationId(targetHostnames);
+ }
+
+ @Override
+ public void reloadDeployFileDistributor() {
+ manager.reloadDeployFileDistributor();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBRegistry.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBRegistry.java
new file mode 100644
index 00000000000..58d651ae33a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBRegistry.java
@@ -0,0 +1,54 @@
+// 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.server.filedistribution;
+
+import com.yahoo.config.FileReference;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.net.HostName;
+import com.yahoo.vespa.filedistribution.FileDistributionManager;
+import com.yahoo.config.model.application.provider.FileReferenceCreator;
+
+import java.util.*;
+
+/**
+ * @author tonytv
+ */
+public class FileDBRegistry implements FileRegistry {
+
+ private final FileDistributionManager manager;
+ private List<Entry> entries = new ArrayList<>();
+ private final Map<String, FileReference> fileReferenceCache = new HashMap<>();
+
+ public FileDBRegistry(FileDistributionManager manager) {
+ this.manager = manager;
+ }
+
+ @Override
+ public synchronized FileReference addFile(String relativePath) {
+ Optional<FileReference> cachedReference = Optional.ofNullable(fileReferenceCache.get(relativePath));
+ return cachedReference.orElseGet(() -> {
+ FileReference newRef = FileReferenceCreator.create(manager.addFile(relativePath));
+ entries.add(new Entry(relativePath, newRef));
+ fileReferenceCache.put(relativePath, newRef);
+ return newRef;
+ });
+ }
+
+ @Override
+ public String fileSourceHost() {
+ return HostName.getLocalhost();
+ }
+
+ @Override
+ public synchronized List<Entry> export() {
+ return entries;
+ }
+
+ @Override
+ public Set<String> allRelativePaths() {
+ Set<String> ret = new HashSet<>();
+ for (Entry entry : entries) {
+ ret.add(entry.relativePath);
+ }
+ return ret;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionLock.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionLock.java
new file mode 100644
index 00000000000..1082e0a7f6c
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionLock.java
@@ -0,0 +1,93 @@
+// 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.server.filedistribution;
+
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.curator.recipes.CuratorLock;
+import com.yahoo.vespa.curator.recipes.CuratorLockException;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Global filedistribution lock to ensure only one configserver may work on filedistribution.
+ * The implementation uses a combination of a {@link java.util.concurrent.locks.ReentrantLock} and
+ * a {@link CuratorLock} to ensure both mutual exclusion within the JVM and
+ * across JVMs via ZooKeeper.
+ *
+ * @author lulf
+ */
+public class FileDistributionLock implements Lock {
+ private final Lock processLock;
+ private final CuratorLock curatorLock;
+
+ public FileDistributionLock(Curator curator, String zkPath) {
+ this.processLock = new ReentrantLock();
+ this.curatorLock = new CuratorLock(curator, zkPath);
+ }
+
+ @Override
+ public void lock() {
+ processLock.lock();
+ try {
+ curatorLock.lock();
+ } catch (CuratorLockException e) {
+ processLock.unlock();
+ throw e;
+ }
+ }
+
+ @Override
+ public void lockInterruptibly() throws InterruptedException {
+ throw new UnsupportedOperationException();
+
+ }
+
+ @Override
+ public boolean tryLock() {
+ if (processLock.tryLock()) {
+ if (curatorLock.tryLock()) {
+ return true;
+ } else {
+ processLock.unlock();
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
+ TimeoutBudget budget = new TimeoutBudget(Clock.systemUTC(), Duration.ofMillis(unit.toMillis(timeout)));
+ if (processLock.tryLock(budget.timeLeft().toMillis(), TimeUnit.MILLISECONDS)) {
+ if (curatorLock.tryLock(budget.timeLeft().toMillis(), TimeUnit.MILLISECONDS)) {
+ return true;
+ } else {
+ processLock.unlock();
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void unlock() {
+ try {
+ curatorLock.unlock();
+ } finally {
+ processLock.unlock();
+ }
+ }
+
+ @Override
+ public Condition newCondition() {
+ throw new UnsupportedOperationException();
+ }
+}
+
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionProvider.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionProvider.java
new file mode 100644
index 00000000000..2453381131d
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionProvider.java
@@ -0,0 +1,55 @@
+// 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.server.filedistribution;
+
+import com.yahoo.config.model.api.FileDistribution;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.vespa.filedistribution.FileDistributionManager;
+
+import java.io.File;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * Provides file distribution registry and invoker.
+ *
+ * @author lulf
+ * @since 5.1.14
+ */
+public class FileDistributionProvider {
+
+ private final FileRegistry fileRegistry;
+ private final FileDistribution fileDistribution;
+
+ public FileDistributionProvider(File applicationDir, String zooKeepersSpec, String applicationId, Lock fileDistributionLock) {
+ ensureDirExists(FileDistribution.getDefaultFileDBPath());
+ final FileDistributionManager manager = new FileDistributionManager(
+ FileDistribution.getDefaultFileDBPath(),
+ applicationDir,
+ zooKeepersSpec,
+ applicationId,
+ fileDistributionLock);
+ this.fileDistribution = new FileDBHandler(manager);
+ this.fileRegistry = new FileDBRegistry(manager);
+ }
+
+ public FileDistributionProvider(FileRegistry fileRegistry, FileDistribution fileDistribution) {
+ this.fileRegistry = fileRegistry;
+ this.fileDistribution = fileDistribution;
+ }
+
+ public FileRegistry getFileRegistry() {
+ return fileRegistry;
+ }
+
+ public FileDistribution getFileDistribution() {
+ return fileDistribution;
+ }
+
+ private void ensureDirExists(File dir) {
+ if (!dir.exists()) {
+ boolean success = dir.mkdirs();
+ if (!success)
+ throw new RuntimeException("Could not create directory " + dir.getPath());
+ }
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDBHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDBHandler.java
new file mode 100644
index 00000000000..b61a1c8d9bc
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDBHandler.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.server.filedistribution;
+
+import com.yahoo.config.FileReference;
+import com.yahoo.config.model.api.FileDistribution;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class MockFileDBHandler implements FileDistribution {
+ public int sendDeployedFilesCalled = 0;
+ public int reloadDeployFileDistributorCalled = 0;
+ public int limitSendingOfDeployedFilesToCalled = 0;
+ public int removeDeploymentsThatHaveDifferentApplicationIdCalled = 0;
+
+ @Override
+ public void sendDeployedFiles(String hostName, Set<FileReference> fileReferences) {
+ sendDeployedFilesCalled++;
+ }
+
+ @Override
+ public void reloadDeployFileDistributor() {
+ reloadDeployFileDistributorCalled++;
+ }
+
+ @Override
+ public void limitSendingOfDeployedFilesTo(Collection<String> hostNames) {
+ limitSendingOfDeployedFilesToCalled++;
+ }
+
+ @Override
+ public void removeDeploymentsThatHaveDifferentApplicationId(Collection<String> targetHostnames) {
+ removeDeploymentsThatHaveDifferentApplicationIdCalled++;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDistributionProvider.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDistributionProvider.java
new file mode 100644
index 00000000000..588d7c259b6
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDistributionProvider.java
@@ -0,0 +1,19 @@
+// 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.server.filedistribution;
+
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class MockFileDistributionProvider extends FileDistributionProvider {
+
+ public MockFileDistributionProvider() {
+ super(new MockFileRegistry(), new MockFileDBHandler());
+ }
+
+ public MockFileDBHandler getMockFileDBHandler() {
+ return (MockFileDBHandler) getFileDistribution();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/BadRequestException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/BadRequestException.java
new file mode 100644
index 00000000000..2af55c343a1
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/BadRequestException.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.server.http;
+
+/**
+ * Exception that will create a http response with BAD_REQUEST response code (400)
+ *
+ * @author musum
+ * @since 5.1.17
+ */
+public class BadRequestException extends RuntimeException {
+
+ public BadRequestException(String message) {
+ super(message);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentHandler.java
new file mode 100644
index 00000000000..c2d255e1630
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentHandler.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.server.http;
+
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Slime;
+
+import java.io.InputStreamReader;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Requests for content and content status, both for prepared and active sessions,
+ * are handled by this class.
+ *
+ * @author musum
+ * @since 5.1.15
+ */
+public class ContentHandler {
+
+ public HttpResponse get(ContentRequest request) {
+ ContentRequest.ReturnType returnType = request.getReturnType();
+ String urlBase = request.getUrlBase("/content/");
+ if (ContentRequest.ReturnType.STATUS.equals(returnType)) {
+ return status(request, urlBase);
+ } else {
+ return content(request, urlBase);
+ }
+ }
+
+ public HttpResponse put(ContentRequest request) {
+ ApplicationFile file = request.getFile();
+ if (request.getPath().endsWith("/")) {
+ createDirectory(request, file);
+ } else {
+ createFile(request, file);
+ }
+ return createResponse(request);
+ }
+
+ public HttpResponse delete(ContentRequest request) {
+ ApplicationFile file = request.getExistingFile();
+ deleteFile(file);
+ return createResponse(request);
+ }
+
+ private HttpResponse content(ContentRequest request, String urlBase) {
+ ApplicationFile file = request.getExistingFile();
+ if (file.isDirectory()) {
+ return new SessionContentListResponse(urlBase, listSortedFiles(file, request.getPath(), request.isRecursive()));
+ }
+ return new SessionContentReadResponse(file);
+ }
+
+ private HttpResponse status(ContentRequest request, String urlBase) {
+ ApplicationFile file = request.getFile();
+ if (file.isDirectory()) {
+ return new SessionContentStatusListResponse(urlBase, listSortedFiles(file, request.getPath(), request.isRecursive()));
+ }
+ return new SessionContentStatusResponse(file, urlBase);
+ }
+
+ private static List<ApplicationFile> listSortedFiles(ApplicationFile file, String path, boolean recursive) {
+ if (!path.isEmpty() && !path.endsWith("/")) {
+ return Arrays.asList(file);
+ }
+ List<ApplicationFile> files = file.listFiles(recursive);
+ Collections.sort(files);
+ return files;
+ }
+
+ private void createFile(ContentRequest request, ApplicationFile file) {
+ if (!request.hasRequestBody()) {
+ throw new BadRequestException("Request must contain body when creating a file");
+ }
+ try {
+ file.writeFile(new InputStreamReader(request.getData()));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @SuppressWarnings("StatementWithEmptyBody")
+ private void createDirectory(ContentRequest request, ApplicationFile file) {
+ if (request.hasRequestBody()) {
+ // TODO: Enable when we have a good way to check if request contains a body
+ // return new HttpErrorResponse(HttpResponse.Status.BAD_REQUEST, "Request should not contain a body when creating directories");
+ }
+ file.createDirectory();
+ }
+
+ private void deleteFile(ApplicationFile file) {
+ try {
+ file.delete();
+ } catch (RuntimeException e) {
+ throw new BadRequestException("File '" + file.getPath() + "' is not an empty directory");
+ }
+ }
+
+ private HttpResponse createResponse(ContentRequest request) {
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+ root.setString("prepared", request.getUrlBase("/prepared"));
+ return new SessionResponse(slime, root);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentRequest.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentRequest.java
new file mode 100644
index 00000000000..d71987449bf
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentRequest.java
@@ -0,0 +1,95 @@
+// 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.server.http;
+
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.session.LocalSession;
+
+import java.io.InputStream;
+
+/**
+ * Represents a {@link ContentRequest}, and contains common functionality for content requests for all content handlers.
+ *
+ * @author lulf
+ * @since 5.3
+ */
+public abstract class ContentRequest {
+ private static final String RETURN_QUERY_PROPERTY = "return";
+
+ enum ReturnType {CONTENT, STATUS}
+
+ private final long sessionId;
+ private final String path;
+ private final ApplicationFile file;
+ private final HttpRequest request;
+
+ protected ContentRequest(HttpRequest request, LocalSession session) {
+ this.request = request;
+ this.sessionId = session.getSessionId();
+ this.path = getContentPath(request);
+ this.file = session.getApplicationFile(Path.fromString(path), getApplicationFileMode(request.getMethod()));
+ }
+
+ private LocalSession.Mode getApplicationFileMode(com.yahoo.jdisc.http.HttpRequest.Method method) {
+ switch (method) {
+ case GET:
+ case OPTIONS:
+ return LocalSession.Mode.READ;
+ default:
+ return LocalSession.Mode.WRITE;
+ }
+ }
+
+ ReturnType getReturnType() {
+ if (request.hasProperty(RETURN_QUERY_PROPERTY)) {
+ String type = request.getProperty(RETURN_QUERY_PROPERTY);
+ switch (type) {
+ case "content":
+ return ReturnType.CONTENT;
+ case "status":
+ return ReturnType.STATUS;
+ default:
+ throw new BadRequestException("return=" + type + " is an illegal argument. Only " +
+ ReturnType.CONTENT.name() + " and " + ReturnType.STATUS.name() + " are allowed");
+ }
+ } else {
+ return ReturnType.CONTENT;
+ }
+ }
+
+ protected abstract String getPathPrefix();
+ protected abstract String getContentPath(HttpRequest request);
+
+ String getUrlBase(String appendStr) {
+ return Utils.getUrlBase(request, getPathPrefix() + appendStr);
+ }
+
+ boolean isRecursive() {
+ return request.getBooleanProperty("recursive");
+ }
+
+ boolean hasRequestBody() {
+ return request.getData() != null;
+ }
+
+ InputStream getData() {
+ return request.getData();
+ }
+
+
+ String getPath() {
+ return path;
+ }
+
+ ApplicationFile getFile() {
+ return file;
+ }
+
+ ApplicationFile getExistingFile() {
+ if (!file.exists()) {
+ throw new NotFoundException("Session " + sessionId + " does not contain a file '" + path + "'");
+ }
+ return file;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigRequest.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigRequest.java
new file mode 100644
index 00000000000..04e286ac96f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigRequest.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.server.http;
+
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.yahoo.collections.Tuple2;
+import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.GetConfigRequest;
+import com.yahoo.vespa.config.protocol.DefContent;
+import com.yahoo.vespa.config.protocol.VespaVersion;
+import com.yahoo.vespa.config.server.RequestHandler;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.http.v2.HttpConfigRequests;
+import com.yahoo.vespa.config.server.http.v2.TenantRequest;
+import com.yahoo.vespa.config.util.ConfigUtils;
+
+/**
+ * A request to get config, bound to tenant and app id. Used by both v1 and v2 of the config REST API.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class HttpConfigRequest implements GetConfigRequest, TenantRequest {
+ private static final String DEFAULT_TENANT = "default";
+ private static final String HTTP_PROPERTY_NOCACHE = "noCache";
+ private final ConfigKey<?> key;
+ private final ApplicationId appId;
+ private final boolean noCache;
+
+ private HttpConfigRequest(ConfigKey<?> key, ApplicationId appId, boolean noCache) {
+ this.key = key;
+ this.appId = appId;
+ this.noCache = noCache;
+ }
+
+ private static ConfigKey<?> fromRequestV1(HttpRequest req) {
+ BindingMatch<?> bm = Utils.getBindingMatch(req, "http://*/config/v1/*/*"); // see jdisc-bindings.cfg
+ String conf = bm.group(2); // The port number is implicitly 1, it seems
+ String cId;
+ String cName;
+ String cNamespace;
+ if (bm.groupCount() >= 4) {
+ cId = bm.group(3);
+ } else {
+ cId = "";
+ }
+ Tuple2<String, String> nns = nameAndNamespace(conf);
+ cName = nns.first;
+ cNamespace = nns.second;
+ return new ConfigKey<>(cName, cId, cNamespace);
+ }
+
+ public static HttpConfigRequest createFromRequestV1(HttpRequest req) {
+ return new HttpConfigRequest(fromRequestV1(req), ApplicationId.defaultId(), req.getBooleanProperty(HTTP_PROPERTY_NOCACHE));
+ }
+
+ public static HttpConfigRequest createFromRequestV2(HttpRequest req) {
+ // Four bindings for this: with full app id or only name, with and without config id (like v1)
+ BindingMatch<?> bm = HttpConfigRequests.getBindingMatch(req,
+ "http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/*/*",
+ "http://*/config/v2/tenant/*/application/*/*/*");
+ if (bm.groupCount() > 6) return createFromRequestV2FullAppId(req, bm);
+ return createFromRequestV2SimpleAppId(req, bm);
+ }
+
+ // The URL pattern with only tenant and application given
+ private static HttpConfigRequest createFromRequestV2SimpleAppId(HttpRequest req, BindingMatch<?> bm) {
+ String cId;
+ String cName;
+ String cNamespace;
+ TenantName tenant = TenantName.from(bm.group(2));
+ ApplicationName application = ApplicationName.from(bm.group(3));
+ String conf = bm.group(4);
+ if (bm.groupCount() >= 6) {
+ cId = bm.group(5);
+ } else {
+ cId = "";
+ }
+ Tuple2<String, String> nns = nameAndNamespace(conf);
+ cName = nns.first;
+ cNamespace = nns.second;
+ return new HttpConfigRequest(new ConfigKey<>(cName, cId, cNamespace),
+ new ApplicationId.Builder().applicationName(application).tenant(tenant).build(),
+ req.getBooleanProperty(HTTP_PROPERTY_NOCACHE));
+ }
+
+ // The URL pattern with full app id given
+ private static HttpConfigRequest createFromRequestV2FullAppId(HttpRequest req, BindingMatch<?> bm) {
+ String cId;
+ String cName;
+ String cNamespace;
+ String tenant = bm.group(2);
+ String application = bm.group(3);
+ String environment = bm.group(4);
+ String region = bm.group(5);
+ String instance = bm.group(6);
+ String conf = bm.group(7);
+ if (bm.groupCount() >= 9) {
+ cId = bm.group(8);
+ } else {
+ cId = "";
+ }
+ Tuple2<String, String> nns = nameAndNamespace(conf);
+ cName = nns.first;
+ cNamespace = nns.second;
+
+ ApplicationId appId = new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName(application)
+ .instanceName(instance)
+ .build();
+ return new HttpConfigRequest(new ConfigKey<>(cName, cId, cNamespace), appId, req.getBooleanProperty(HTTP_PROPERTY_NOCACHE));
+ }
+
+ /**
+ * Throws an exception if bad config or config id
+ *
+ * @param requestKey a {@link com.yahoo.vespa.config.ConfigKey}
+ * @param requestHandler a {@link RequestHandler}
+ * @param appId appId
+ */
+ public static void validateRequestKey(ConfigKey<?> requestKey, RequestHandler requestHandler, ApplicationId appId) {
+ Set<ConfigKey<?>> allConfigsProduced = requestHandler.allConfigsProduced(appId, Optional.empty());
+ if (allConfigsProduced.isEmpty()) {
+ // This will happen if the configserver is starting up, but has not built a config model
+ throwModelNotReady();
+ }
+ if (configNameNotFound(requestKey, allConfigsProduced)) {
+ throw new NotFoundException("No such config: " + requestKey.getNamespace() + "." + requestKey.getName());
+ }
+ if (configIdNotFound(requestHandler, requestKey, appId)) {
+ throw new NotFoundException("No such config id: " + requestKey.getConfigId());
+ }
+ }
+
+ public static void throwModelNotReady() {
+ throw new NotFoundException("Config not available, verify that an application package has been deployed and activated.");
+ }
+
+ /**
+ * If the given config is produced by the model at all
+ *
+ * @return ok or not
+ */
+ private static boolean configNameNotFound(final ConfigKey<?> requestKey, Set<ConfigKey<?>> allConfigsProduced) {
+ return !Iterables.any(allConfigsProduced, new Predicate<ConfigKey<?>>() {
+ @Override
+ public boolean apply(@Nullable ConfigKey<?> k) {
+ return k.getName().equals(requestKey.getName()) && k.getNamespace().equals(requestKey.getNamespace());
+ }
+ });
+ }
+
+ private static boolean configIdNotFound(RequestHandler requestHandler, ConfigKey<?> requestKey, ApplicationId appId) {
+ return !requestHandler.allConfigIds(appId, Optional.empty()).contains(requestKey.getConfigId());
+ }
+
+ public static Tuple2<String, String> nameAndNamespace(String nsDotName) {
+ Tuple2<String, String> ret = ConfigUtils.getNameAndNamespaceFromString(nsDotName);
+ if ("".equals(ret.second)) throw new IllegalArgumentException("Illegal config, must be of form namespace.name.");
+ return ret;
+ }
+
+ @Override
+ public ConfigKey<?> getConfigKey() {
+ return key;
+ }
+
+ @Override
+ public DefContent getDefContent() {
+ return DefContent.fromList(Collections.<String>emptyList());
+ }
+
+ @Override
+ public Optional<VespaVersion> getVespaVersion() {
+ return Optional.empty();
+ }
+
+ @Override
+ public ApplicationId getApplicationId() {
+ return appId;
+ }
+
+ public boolean noCache() {
+ return noCache;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigResponse.java
new file mode 100644
index 00000000000..10f5243cfdb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigResponse.java
@@ -0,0 +1,42 @@
+// 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.server.http;
+
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.vespa.config.protocol.CompressionType;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import static com.yahoo.jdisc.http.HttpResponse.Status.OK;
+
+/**
+ * HTTP getConfig response
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class HttpConfigResponse extends HttpResponse {
+ public static final String JSON_CONTENT_TYPE = "application/json";
+ private final ConfigResponse config;
+
+ private HttpConfigResponse(ConfigResponse config) {
+ super(OK);
+ this.config = config;
+ }
+
+ public static HttpConfigResponse createFromConfig(ConfigResponse config) {
+ return new HttpConfigResponse(config);
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ config.serialize(outputStream, CompressionType.UNCOMPRESSED);
+ }
+
+ @Override
+ public String getContentType() {
+ return JSON_CONTENT_TYPE;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpErrorResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpErrorResponse.java
new file mode 100644
index 00000000000..b5886992f10
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpErrorResponse.java
@@ -0,0 +1,81 @@
+// 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.server.http;
+
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.log.LogLevel;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.logging.Logger;
+
+import static com.yahoo.jdisc.Response.Status.*;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class HttpErrorResponse extends HttpResponse {
+ Logger log = Logger.getLogger(HttpErrorResponse.class.getName());
+ private final Slime slime = new Slime();
+
+ public HttpErrorResponse(int code, final String errorType, final String msg) {
+ super(code);
+ final Cursor root = slime.setObject();
+ root.setString("error-code", errorType);
+ root.setString("message", msg);
+ if (code != 200) {
+ log.log(LogLevel.INFO, "Returning response with response code " + code + ", error-code:" + errorType + ", message=" + msg);
+ }
+ }
+
+ public enum errorCodes {
+ NOT_FOUND,
+ BAD_REQUEST,
+ METHOD_NOT_ALLOWED,
+ INTERNAL_SERVER_ERROR,
+ INVALID_APPLICATION_PACKAGE,
+ UNKNOWN_VESPA_VERSION,
+ OUT_OF_CAPACITY
+ }
+
+ public static HttpErrorResponse notFoundError(String msg) {
+ return new HttpErrorResponse(NOT_FOUND, errorCodes.NOT_FOUND.name(), msg);
+ }
+
+ public static HttpErrorResponse internalServerError(String msg) {
+ return new HttpErrorResponse(INTERNAL_SERVER_ERROR, errorCodes.INTERNAL_SERVER_ERROR.name(), msg);
+ }
+
+ public static HttpErrorResponse invalidApplicationPackage(String msg) {
+ return new HttpErrorResponse(BAD_REQUEST, errorCodes.INVALID_APPLICATION_PACKAGE.name(), msg);
+ }
+
+ public static HttpErrorResponse outOfCapacity(String msg) {
+ return new HttpErrorResponse(BAD_REQUEST, errorCodes.OUT_OF_CAPACITY.name(), msg);
+ }
+
+ public static HttpErrorResponse badRequest(String msg) {
+ return new HttpErrorResponse(BAD_REQUEST, errorCodes.BAD_REQUEST.name(), msg);
+ }
+
+ public static HttpErrorResponse methodNotAllowed(String msg) {
+ return new HttpErrorResponse(METHOD_NOT_ALLOWED, errorCodes.METHOD_NOT_ALLOWED.name(), msg);
+ }
+
+ public static HttpResponse unknownVespaVersion(String message) {
+ return new HttpErrorResponse(BAD_REQUEST, errorCodes.UNKNOWN_VESPA_VERSION.name(), message);
+ }
+
+ @Override
+ public void render(OutputStream stream) throws IOException {
+ new JsonFormat(true).encode(stream, slime);
+ }
+
+ //@Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpGetConfigHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpGetConfigHandler.java
new file mode 100644
index 00000000000..50a1a3877ef
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpGetConfigHandler.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.server.http;
+
+import com.google.inject.Inject;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.server.RequestHandler;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.config.provision.ApplicationId;
+
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
+/**
+ * HTTP handler for a v2 getConfig operation
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class HttpGetConfigHandler extends HttpHandler {
+ private final RequestHandler requestHandler;
+
+ public HttpGetConfigHandler(Executor executor, RequestHandler requestHandler, AccessLog accessLog) {
+ super(executor, accessLog);
+ this.requestHandler = requestHandler;
+ }
+
+ @Inject
+ public HttpGetConfigHandler(Executor executor, Tenants tenants, AccessLog accesslog) {
+ this(executor, tenants.defaultTenant().getRequestHandler(), accesslog);
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest req) {
+ HttpConfigRequest request = HttpConfigRequest.createFromRequestV1(req);
+ HttpConfigRequest.validateRequestKey(request.getConfigKey(), requestHandler, ApplicationId.defaultId());
+ return HttpConfigResponse.createFromConfig(resolveConfig(request));
+ }
+
+ private ConfigResponse resolveConfig(HttpConfigRequest request) {
+ log.log(LogLevel.DEBUG, "nocache=" + request.noCache());
+ ConfigResponse config = requestHandler.resolveConfig(ApplicationId.defaultId(), request, Optional.empty());
+ if (config == null) HttpConfigRequest.throwModelNotReady();
+ return config;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpHandler.java
new file mode 100644
index 00000000000..7eb6f9c2271
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpHandler.java
@@ -0,0 +1,131 @@
+// 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.server.http;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.LoggingRequestHandler;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.log.LogLevel;
+import com.yahoo.config.provision.OutOfCapacityException;
+import com.yahoo.yolean.Exceptions;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.concurrent.Executor;
+
+/**
+ * Super class for http handlers, that takes care of checking valid
+ * methods for a request. Handlers should subclass this method and
+ * implement the handleMETHOD methods that it supports.
+ *
+ * @author musum
+ * @since 5.1.14
+ */
+public class HttpHandler extends LoggingRequestHandler {
+
+ public HttpHandler(Executor executor, AccessLog accessLog) {
+ super(executor, accessLog);
+ }
+
+ @Override
+ public HttpResponse handle(HttpRequest request) {
+ log.log(LogLevel.DEBUG, request.getMethod() + " " + request.getUri().toString());
+ try {
+ switch (request.getMethod()) {
+ case POST:
+ return handlePOST(request);
+ case GET:
+ return handleGET(request);
+ case PUT:
+ return handlePUT(request);
+ case DELETE:
+ return handleDELETE(request);
+ default:
+ return createErrorResponse(request.getMethod());
+ }
+ } catch (NotFoundException e) {
+ return HttpErrorResponse.notFoundError(getMessage(e, request));
+ } catch (BadRequestException e) {
+ return HttpErrorResponse.badRequest(getMessage(e, request));
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ return HttpErrorResponse.badRequest(getMessage(e, request));
+ } catch (InvalidApplicationException e) {
+ return HttpErrorResponse.invalidApplicationPackage(getMessage(e, request));
+ } catch (OutOfCapacityException e) {
+ return HttpErrorResponse.outOfCapacity(getMessage(e, request));
+ } catch (InternalServerException e) {
+ return HttpErrorResponse.internalServerError(getMessage(e, request));
+ } catch (UnknownVespaVersionException e) {
+ return HttpErrorResponse.unknownVespaVersion(getMessage(e, request));
+ } catch (Exception e) {
+ e.printStackTrace();
+ return HttpErrorResponse.internalServerError(getMessage(e, request));
+ }
+ }
+
+ private String getMessage(Exception e, HttpRequest request) {
+ String message;
+ if (request.getBooleanProperty("debug")) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ e.printStackTrace(pw);
+ message = sw.toString();
+ } else {
+ message = Exceptions.toMessageString(e);
+ }
+ return message;
+ }
+
+ /**
+ * Default implementation of handler for GET requests. Returns an error response.
+ * Override this method to handle GET requests.
+ *
+ * @param request a {@link HttpRequest}
+ * @return an error response with response code 405
+ */
+ protected HttpResponse handleGET(HttpRequest request) {
+ return createErrorResponse(request.getMethod());
+ }
+
+ /**
+ * Default implementation of handler for POST requests. Returns an error response.
+ * Override this method to handle POST requests.
+ *
+ * @param request a {@link HttpRequest}
+ * @return an error response with response code 405
+ */
+ protected HttpResponse handlePOST(HttpRequest request) {
+ return createErrorResponse(request.getMethod());
+ }
+
+ /**
+ * Default implementation of handler for PUT requests. Returns an error response.
+ * Override this method to handle POST requests.
+ *
+ * @param request a {@link HttpRequest}
+ * @return an error response with response code 405
+ */
+ protected HttpResponse handlePUT(HttpRequest request) {
+ return createErrorResponse(request.getMethod());
+ }
+
+ /**
+ * Default implementation of handler for DELETE requests. Returns an error response.
+ * Override this method to handle DELETE requests.
+ *
+ * @param request a {@link HttpRequest}
+ * @return an error response with response code 405
+ */
+ protected HttpResponse handleDELETE(HttpRequest request) {
+ return createErrorResponse(request.getMethod());
+ }
+
+ /**
+ * Creates error response when method is not handled
+ *
+ * @return an error response with response code 405
+ */
+ private HttpResponse createErrorResponse(com.yahoo.jdisc.http.HttpRequest.Method method) {
+ return HttpErrorResponse.methodNotAllowed("Method '" + method + "' is not supported");
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListConfigsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListConfigsHandler.java
new file mode 100644
index 00000000000..a6e2c5bf050
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListConfigsHandler.java
@@ -0,0 +1,128 @@
+// 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.server.http;
+
+import com.google.inject.Inject;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.server.RequestHandler;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.config.provision.ApplicationId;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.*;
+import java.util.concurrent.Executor;
+
+import static com.yahoo.jdisc.http.HttpResponse.Status.*;
+
+
+/**
+ * Handler for a list configs operation. Lists all configs in model.
+ *
+ * @author vegardh
+ * @since 5.1.11
+ */
+public class HttpListConfigsHandler extends HttpHandler {
+ static final String RECURSIVE_QUERY_PROPERTY = "recursive";
+ private final RequestHandler requestHandler;
+
+ @Inject
+ public HttpListConfigsHandler(Executor executor, AccessLog accessLog, Tenants tenants) {
+ this(executor, accessLog, tenants.defaultTenant().getRequestHandler());
+ }
+
+ public HttpListConfigsHandler(Executor executor, AccessLog accessLog, RequestHandler requestHandler) {
+ super(executor, accessLog);
+ this.requestHandler = requestHandler;
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest req) {
+ boolean recursive = req.getBooleanProperty(RECURSIVE_QUERY_PROPERTY);
+ Set<ConfigKey<?>> configs = requestHandler.listConfigs(ApplicationId.defaultId(), Optional.empty(), recursive);
+ String urlBase = Utils.getUrlBase(req, "/config/v1/");
+ Set<ConfigKey<?>> allConfigs = requestHandler.allConfigsProduced(ApplicationId.defaultId(), Optional.empty());
+ return new ListConfigsResponse(configs, allConfigs, urlBase, recursive);
+ }
+
+ static class ListConfigsResponse extends HttpResponse {
+ private final List<ConfigKey<?>> configs;
+ private final Set<ConfigKey<?>> allConfigs;
+ private final String urlBase;
+ private final boolean recursive;
+
+ /**
+ * New list response
+ *
+ * @param configs the configs to include in the list
+ * @param urlBase for example "http://foo.com:19071/config/v1/ (configs are appended to the listed URLs based on configs list)
+ * @param recursive list recursively
+ */
+ public ListConfigsResponse(Set<ConfigKey<?>> configs, Set<ConfigKey<?>> allConfigs, String urlBase, boolean recursive) {
+ super(OK);
+ this.configs = new ArrayList<>(configs);
+ Collections.sort(this.configs);
+ this.allConfigs = allConfigs;
+ this.urlBase = urlBase;
+ this.recursive = recursive;
+ }
+
+ /**
+ * The listing URL for this config in this service
+ *
+ * @param key config key
+ * @param rec recursive
+ * @return url
+ */
+ String toUrl(ConfigKey<?> key, boolean rec) {
+ return urlBase + key.getNamespace() + "." + key.getName() + "/" + key.getConfigId() + (rec ? "" : "/");
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+ Cursor array;
+ if (!recursive) {
+ array = root.setArray("children");
+ for (ConfigKey<?> key : keysThatHaveAChildWithSameName(configs, allConfigs)) {
+ array.addString(toUrl(key, false));
+ }
+ }
+ array = root.setArray("configs");
+ for (ConfigKey<?> key : configs) {
+ array.addString(toUrl(key, true));
+ }
+ new JsonFormat(true).encode(outputStream, slime);
+ }
+
+ static Set<ConfigKey<?>> keysThatHaveAChildWithSameName(Collection<ConfigKey<?>> keys, Set<ConfigKey<?>> allConfigs) {
+ Set<ConfigKey<?>> ret = new LinkedHashSet<>();
+ for (ConfigKey<?> k : keys) {
+ if (ListConfigsResponse.hasAChild(k, allConfigs)) ret.add(k);
+ }
+ return ret;
+ }
+
+ static boolean hasAChild(ConfigKey<?> key, Set<ConfigKey<?>> keys) {
+ if ("".equals(key.getConfigId())) return false;
+ for (ConfigKey<?> k : keys) {
+ if (!k.getName().equals(key.getName())) continue;
+ if ("".equals(k.getConfigId())) continue;
+ if (k.getConfigId().equals(key.getConfigId())) continue;
+ if (k.getConfigId().startsWith(key.getConfigId())) return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListNamedConfigsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListNamedConfigsHandler.java
new file mode 100644
index 00000000000..31d6e84860d
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListNamedConfigsHandler.java
@@ -0,0 +1,60 @@
+// 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.server.http;
+
+import com.google.inject.Inject;
+import com.yahoo.collections.Tuple2;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.server.RequestHandler;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.config.provision.ApplicationId;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/**
+ * Handler for a list configs of given name operation. Lists all configs in model for a given config name.
+ *
+ * @author vegardh
+ * @since 5.1.11
+ */
+public class HttpListNamedConfigsHandler extends HttpHandler {
+ private final RequestHandler requestHandler;
+
+ public HttpListNamedConfigsHandler(Executor executor, RequestHandler requestHandler, AccessLog accessLog) {
+ super(executor, accessLog);
+ this.requestHandler = requestHandler;
+ }
+
+ @Inject
+ public HttpListNamedConfigsHandler(Executor executor, Tenants tenants, AccessLog accessLog) {
+ this(executor, tenants.defaultTenant().getRequestHandler(), accessLog);
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest req) {
+ boolean recursive = req.getBooleanProperty(HttpListConfigsHandler.RECURSIVE_QUERY_PROPERTY);
+ ConfigKey<?> listKey = parseReqToKey(req);
+ HttpConfigRequest.validateRequestKey(listKey, requestHandler, ApplicationId.defaultId());
+ Set<ConfigKey<?>> configs = requestHandler.listNamedConfigs(ApplicationId.defaultId(), Optional.empty(), listKey, recursive);
+ String urlBase = Utils.getUrlBase(req, "/config/v1/");
+ return new HttpListConfigsHandler.ListConfigsResponse(configs, requestHandler.allConfigsProduced(ApplicationId.defaultId(), Optional.empty()), urlBase, recursive);
+ }
+
+ private ConfigKey<?> parseReqToKey(HttpRequest req) {
+ BindingMatch<?> bm = Utils.getBindingMatch(req, "http://*/config/v1/*/*");
+ String config = bm.group(2); // See jdisc-bindings.cfg. The port number is implicitly 1, it seems.
+ Tuple2<String, String> nns = HttpConfigRequest.nameAndNamespace(config);
+ String name = nns.first;
+ String namespace = nns.second;
+ String idSegment = "";
+ if (bm.groupCount() == 4) {
+ idSegment = bm.group(3);
+ }
+ return new ConfigKey<>(name, idSegment, namespace);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/InternalServerException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/InternalServerException.java
new file mode 100644
index 00000000000..240f5814652
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/InternalServerException.java
@@ -0,0 +1,21 @@
+// 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.server.http;
+
+import java.io.IOException;
+
+/**
+ * Exception that will create a http response with INTERNAL_SERVER_ERROR response code (500)
+ *
+ * @author musum
+ * @since 5.1.17
+ */
+public class InternalServerException extends RuntimeException {
+
+ public InternalServerException(String message) {
+ super(message);
+ }
+
+ public InternalServerException(String message, Exception e) {
+ super(message, e);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/InvalidApplicationException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/InvalidApplicationException.java
new file mode 100644
index 00000000000..ba8f034777a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/InvalidApplicationException.java
@@ -0,0 +1,16 @@
+// 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.server.http;
+
+/**
+ * @author musum
+ */
+public class InvalidApplicationException extends RuntimeException {
+
+ public InvalidApplicationException(String message) {
+ super(message);
+ }
+
+ public InvalidApplicationException(String message, Throwable e) {
+ super(message, e);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/JSONResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/JSONResponse.java
new file mode 100644
index 00000000000..ae99481ccfa
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/JSONResponse.java
@@ -0,0 +1,35 @@
+// 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.server.http;
+
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Response that contains some utility stuff for rendering json.
+ *
+ * @author lulf
+ * @since 5.3
+ */
+public class JSONResponse extends HttpResponse {
+ private final Slime slime = new Slime();
+ protected final Cursor object;
+ public JSONResponse(int status) {
+ super(status);
+ this.object = slime.setObject();
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ new JsonFormat(true).encode(outputStream, slime);
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/NotFoundException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/NotFoundException.java
new file mode 100644
index 00000000000..327e792134a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/NotFoundException.java
@@ -0,0 +1,16 @@
+// 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.server.http;
+
+/**
+ * Exception that will create a http response with NOT_FOUND response code (404)
+ *
+ * @author musum
+ * @since 5.1.17
+ */
+public class NotFoundException extends RuntimeException {
+
+ public NotFoundException(String message) {
+ super(message);
+ }
+}
+
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionActiveHandlerBase.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionActiveHandlerBase.java
new file mode 100644
index 00000000000..53133b49c8a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionActiveHandlerBase.java
@@ -0,0 +1,55 @@
+// 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.server.http;
+
+import com.yahoo.config.provision.Provisioner;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.vespa.config.server.ActivateLock;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.deploy.Deployer;
+import com.yahoo.vespa.config.server.deploy.Deployment;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.session.LocalSession;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
+/**
+ * @author lulf
+ */
+public class SessionActiveHandlerBase extends SessionHandler {
+
+ public SessionActiveHandlerBase(Executor executor, AccessLog accessLog) {
+ super(executor, accessLog);
+ }
+
+ protected void activate(HttpRequest request,
+ LocalSessionRepo localSessionRepo,
+ ActivateLock activateLock,
+ TimeoutBudget timeoutBudget,
+ Optional<Provisioner> hostProvisioner,
+ LocalSession localSession) {
+ // TODO: Use an injected deployer from the callers of this instead
+ // TODO: And then get rid of the activateLock and localSessionRepo arguments in deployFromPreparedSession
+ Deployer deployer = new Deployer(null, HostProvisionerProvider.from(hostProvisioner), null, null);
+ Deployment deployment = deployer.deployFromPreparedSession(localSession, activateLock, localSessionRepo, timeoutBudget.timeLeft());
+ deployment.setIgnoreLockFailure(shouldIgnoreLockFailure(request));
+ deployment.setIgnoreSessionStaleFailure(shouldIgnoreSessionStaleFailure(request));
+ deployment.activate();
+ }
+
+ private boolean shouldIgnoreLockFailure(HttpRequest request) {
+ return request.getBooleanProperty("force");
+ }
+
+ /**
+ * True if this request should ignore activation failure because the session was made from an active session that is not active now
+ * @param request a {@link com.yahoo.container.jdisc.HttpRequest}
+ * @return true if ignore failure
+ */
+ private boolean shouldIgnoreSessionStaleFailure(HttpRequest request) {
+ return request.getBooleanProperty("force");
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentListResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentListResponse.java
new file mode 100644
index 00000000000..90b5a7fd869
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentListResponse.java
@@ -0,0 +1,34 @@
+// 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.server.http;
+
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+
+/**
+ * Represents a request for listing files within an application package.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+class SessionContentListResponse extends SessionResponse {
+ private final Slime slime = new Slime();
+
+ public SessionContentListResponse(String urlBase, List<ApplicationFile> files) {
+ super();
+ Cursor array = slime.setArray();
+ for (ApplicationFile file : files) {
+ array.addString(urlBase + file.getPath() + (file.isDirectory() ? "/" : ""));
+ }
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ new JsonFormat(true).encode(outputStream, slime);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentReadResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentReadResponse.java
new file mode 100644
index 00000000000..045251d0623
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentReadResponse.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.server.http;
+
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.container.jdisc.HttpResponse;
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import static com.yahoo.jdisc.http.HttpResponse.Status.*;
+
+/**
+ * Represents a response for a request to read contents of a file.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionContentReadResponse extends HttpResponse {
+ private final ApplicationFile file;
+
+ public SessionContentReadResponse(ApplicationFile file) {
+ super(OK);
+ this.file = file;
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ try (InputStream inputStream = file.createInputStream()) {
+ IOUtils.copyLarge(inputStream, outputStream, new byte[1]);
+ }
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpResponse.DEFAULT_MIME_TYPE;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusListResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusListResponse.java
new file mode 100644
index 00000000000..caf38517b6a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusListResponse.java
@@ -0,0 +1,41 @@
+// 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.server.http;
+
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.log.LogLevel;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.*;
+
+/**
+ * Status and md5sum for files within an application package.
+ *
+ * @author musum
+ * @since 5.1.15
+ */
+class SessionContentStatusListResponse extends SessionResponse {
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger("SessionContentStatusListResponse");
+
+ private final Slime slime = new Slime();
+
+ public SessionContentStatusListResponse(String urlBase, List<ApplicationFile> files) {
+ super();
+ Cursor array = slime.setArray();
+ for (ApplicationFile f : files) {
+ Cursor element = array.addObject();
+ element.setString("status", f.getMetaData().getStatus());
+ element.setString("md5", f.getMetaData().getMd5());
+ element.setString("name", urlBase + f.getPath());
+ log.log(LogLevel.DEBUG, "Adding file " + urlBase + f.getPath());
+ }
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ new JsonFormat(true).encode(outputStream, slime);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusResponse.java
new file mode 100644
index 00000000000..15c852b66c3
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusResponse.java
@@ -0,0 +1,54 @@
+// 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.server.http;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yahoo.config.application.api.ApplicationFile;
+
+import java.io.*;
+
+/**
+ * Represents a response for a request to show the status and md5sum of a file in the application package.
+ *
+ * @author musum
+ * @since 5.1.15
+ */
+public class SessionContentStatusResponse extends SessionResponse {
+ private final ApplicationFile file;
+ private final String urlBase;
+ private final ApplicationFile.MetaData metaData;
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ public SessionContentStatusResponse(ApplicationFile file, String urlBase) {
+ super();
+ this.file = file;
+ this.urlBase = urlBase;
+
+ ApplicationFile.MetaData metaData;
+ if (file == null) {
+ metaData = new ApplicationFile.MetaData(ApplicationFile.ContentStatusDeleted, "");
+ } else {
+ metaData = file.getMetaData();
+ }
+ if (metaData == null) {
+ throw new IllegalArgumentException("Could not find status for '" + file.getPath() + "'");
+ }
+ this.metaData = metaData;
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ mapper.writeValue(outputStream, new ResponseData(metaData.status, metaData.md5, urlBase + file.getPath()));
+ }
+
+ private static class ResponseData {
+ public final String status;
+ public final String md5;
+ public final String name;
+
+ private ResponseData(String status, String md5, String name) {
+ this.status = status;
+ this.md5 = md5;
+ this.name = name;
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreate.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreate.java
new file mode 100644
index 00000000000..1526a5b4e0e
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreate.java
@@ -0,0 +1,115 @@
+// 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.server.http;
+
+import com.google.common.io.Files;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.io.IOUtils;
+import com.yahoo.log.LogLevel;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.server.CompressedApplicationInputStream;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.session.LocalSession;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+import com.yahoo.vespa.config.server.session.SessionFactory;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Creates a session from an application package,
+ * or creates a new session from a previous session (with id or the "active" session).
+ * Handles /application/v2 requests
+ *
+ * @author lulf
+ * @author musum
+ * @since 5.1.27
+ */
+// TODO Rename class
+public class SessionCreate {
+ public final static String APPLICATION_X_GZIP = "application/x-gzip";
+ public final static String APPLICATION_ZIP = "application/zip";
+ public final static String contentTypeHeader = "Content-Type";
+
+ private final SessionFactory sessionFactory;
+ private final LocalSessionRepo localSessionRepo;
+ private final SessionCreateResponse responseCreator;
+
+ public SessionCreate(SessionFactory sessionFactory, LocalSessionRepo localSessionRepo, SessionCreateResponse responseCreator) {
+ this.sessionFactory = sessionFactory;
+ this.localSessionRepo = localSessionRepo;
+ this.responseCreator = responseCreator;
+ }
+
+ public HttpResponse createFromExisting(HttpRequest request, Slime deployLog, LocalSession fromSession, TenantName tenant, TimeoutBudget timeoutBudget) {
+ DeployLogger logger = SessionHandler.createLogger(deployLog, request,
+ new ApplicationId.Builder().tenant(tenant).applicationName("-").build());
+ LocalSession session = sessionFactory.createSessionFromExisting(fromSession, logger, timeoutBudget);
+ localSessionRepo.addSession(session);
+ return createResponse(request, session);
+ }
+
+ public HttpResponse create(HttpRequest request, Slime deployLog, TenantName tenant, TimeoutBudget timeoutBudget) {
+ validateDataAndHeader(request);
+ return createSession(request, deployLog, sessionFactory, localSessionRepo, tenant, timeoutBudget);
+ }
+
+ private HttpResponse createSession(HttpRequest request, Slime deployLog, SessionFactory sessionFactory, LocalSessionRepo localSessionRepo, TenantName tenant, TimeoutBudget timeoutBudget) {
+ File tempDir = Files.createTempDir();
+ File applicationDirectory = decompressApplication(request, tempDir);
+ DeployLogger logger = SessionHandler.createLogger(deployLog, request,
+ new ApplicationId.Builder().tenant(tenant).applicationName("-").build());
+ String name = getNameProperty(request, logger);
+ LocalSession session = sessionFactory.createSession(applicationDirectory, name, logger, timeoutBudget);
+ localSessionRepo.addSession(session);
+ HttpResponse response = createResponse(request, session);
+ cleanupApplicationDirectory(tempDir, logger);
+ return response;
+ }
+
+ private String getNameProperty(HttpRequest request, DeployLogger logger) {
+ String name = request.getProperty("name");
+ // TODO: Do we need validation of this parameter?
+ if (name == null) {
+ name = "default";
+ logger.log(LogLevel.INFO, "No application name given, using '" + name + "'");
+ }
+ return name;
+ }
+
+ private File decompressApplication(HttpRequest request, File tempDir) {
+ try (CompressedApplicationInputStream application = CompressedApplicationInputStream.createFromCompressedStream(request.getData(), request.getHeader(contentTypeHeader))) {
+ return application.decompress(tempDir);
+ } catch (IOException e) {
+ throw new InternalServerException("Unable to decompress data in body", e);
+ }
+ }
+
+ private void cleanupApplicationDirectory(File tempDir, DeployLogger logger) {
+ logger.log(LogLevel.DEBUG, "Deleting tmp dir '" + tempDir + "'");
+ if (!IOUtils.recursiveDeleteDir(tempDir)) {
+ logger.log(LogLevel.WARNING, "Not able to delete tmp dir '" + tempDir + "'");
+ }
+ }
+
+
+ private static void validateDataAndHeader(HttpRequest request) {
+ if (request.getData() == null) {
+ throw new BadRequestException("Request contains no data");
+ }
+ String header = request.getHeader(contentTypeHeader);
+ if (header == null) {
+ throw new BadRequestException("Request contains no " + contentTypeHeader + " header");
+ } else if (!(header.equals(APPLICATION_X_GZIP) || header.equals(APPLICATION_ZIP))) {
+ throw new BadRequestException("Request contains invalid " + contentTypeHeader + " header, only '" +
+ APPLICATION_X_GZIP + "' and '" + APPLICATION_ZIP + "' are supported");
+ }
+ }
+
+ private HttpResponse createResponse(HttpRequest request, LocalSession session) {
+ return responseCreator.createResponse(request.getHost(), request.getPort(), session.getSessionId());
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreateResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreateResponse.java
new file mode 100644
index 00000000000..72810ed394c
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreateResponse.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.server.http;
+
+import com.yahoo.container.jdisc.HttpResponse;
+
+/**
+ * Interface for creating responses for SessionCreateHandler.
+ *
+ * @author musum
+ * @since 5.1.27
+ */
+public interface SessionCreateResponse {
+
+ public HttpResponse createResponse(String hostName, int port, long sessionId);
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionHandler.java
new file mode 100644
index 00000000000..c8829cf9e4d
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionHandler.java
@@ -0,0 +1,112 @@
+// 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.server.http;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.server.DeployHandlerLogger;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.session.Session;
+import com.yahoo.vespa.config.server.session.SessionRepo;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.util.concurrent.Executor;
+
+
+/**
+ * Super class for session handlers, that takes care of checking valid
+ * methods for a request. Session handlers should subclass this method and
+ * implement the handleMETHOD methods that it supports.
+ *
+ * @author musum
+ * @since 5.1.14
+ */
+public class SessionHandler extends HttpHandler {
+ public SessionHandler(Executor executor, AccessLog accessLog) {
+ super(executor, accessLog);
+ }
+
+ /**
+ * Gets the raw session id from request (v2). Input request must have a valid path.
+ *
+ * @param request a request
+ * @return a session id
+ */
+ public static String getRawSessionIdV2(HttpRequest request) {
+ final String path = request.getUri().toString();
+ BindingMatch<?> bm = Utils.getBindingMatch(request, "http://*/application/v2/tenant/*/session/*/*");
+ if (bm.groupCount() < 4) {
+ // This would mean the subtype of this doesn't have the correct binding
+ throw new IllegalArgumentException("Can not get session id from request '" + path + "'");
+ }
+ return bm.group(3);
+ }
+
+ /**
+ * Gets session id (as a number) from request (v2). Input request must have a valid path.
+ *
+ * @param request a request
+ * @return a session id
+ */
+ static Long getSessionIdV2(HttpRequest request) {
+ try {
+ return Long.parseLong(getRawSessionIdV2(request));
+ } catch (NumberFormatException e) {
+ throw createSessionException(request);
+ }
+ }
+
+ private static BadRequestException createSessionException(HttpRequest request) {
+ return new BadRequestException("Session id in request is not a number, request was '" +
+ request.getUri().toString() + "'");
+ }
+
+ public static <SESSIONTYPE extends Session> SESSIONTYPE getSessionFromRequestV2(SessionRepo<SESSIONTYPE> sessionRepo, HttpRequest request) {
+ long sessionId = getSessionIdV2(request);
+ return getSessionFromRequest(sessionRepo, sessionId);
+ }
+
+ public static <SESSIONTYPE extends Session> SESSIONTYPE getSessionFromRequest(SessionRepo<SESSIONTYPE> sessionRepo, long sessionId) {
+ SESSIONTYPE session = sessionRepo.getSession(sessionId);
+ if (session == null) {
+ throw new NotFoundException("Session " + sessionId + " was not found");
+ }
+ return session;
+ }
+
+ protected static final Duration DEFAULT_ACTIVATE_TIMEOUT = Duration.ofMinutes(2);
+
+ public static TimeoutBudget getTimeoutBudget(HttpRequest request, Duration defaultTimeout) {
+ return new TimeoutBudget(Clock.systemUTC(), getRequestTimeout(request, defaultTimeout));
+ }
+
+
+ protected static Duration getRequestTimeout(HttpRequest request, Duration defaultTimeout) {
+ if (!request.hasProperty("timeout")) {
+ return defaultTimeout;
+ }
+ try {
+ return Duration.ofSeconds((long) Double.parseDouble(request.getProperty("timeout")));
+ } catch (Exception e) {
+ return defaultTimeout;
+ }
+ }
+
+ static DeployHandlerLogger createLogger(Slime deployLog, HttpRequest request, ApplicationId app) {
+ return createLogger(deployLog, request.getBooleanProperty("verbose"), app);
+ }
+
+ public static DeployHandlerLogger createLogger(Slime deployLog, boolean verbose, ApplicationId app) {
+ return new DeployHandlerLogger(deployLog.get().setArray("log"), verbose, app);
+ }
+
+ protected Slime createDeployLog() {
+ Slime deployLog = new Slime();
+ deployLog.setObject();
+ return deployLog;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionResponse.java
new file mode 100644
index 00000000000..5054e1129d1
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionResponse.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.server.http;
+
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import static com.yahoo.jdisc.http.HttpResponse.Status.OK;
+
+/**
+ * Superclass for responses from session HTTP handlers. Implements the
+ * render method.
+ *
+ * @author musum
+ * @since 5.1.14
+ */
+public class SessionResponse extends HttpResponse {
+ private final Slime slime;
+ protected final Cursor root;
+
+ public SessionResponse() {
+ super(OK);
+ slime = new Slime();
+ root = slime.setObject();
+ }
+
+ public SessionResponse(Slime slime, Cursor root) {
+ super(OK);
+ this.slime = slime;
+ this.root = root;
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ new JsonFormat(true).encode(outputStream, slime);
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/UnknownVespaVersionException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/UnknownVespaVersionException.java
new file mode 100644
index 00000000000..bfdbdd1d4b1
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/UnknownVespaVersionException.java
@@ -0,0 +1,17 @@
+// 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.server.http;
+
+/**
+ * @author musum
+ * @since 5.39
+ */
+public class UnknownVespaVersionException extends RuntimeException {
+
+ public UnknownVespaVersionException(String message) {
+ super(message);
+ }
+
+ public UnknownVespaVersionException(String message, Throwable e) {
+ super(message, e);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/Utils.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/Utils.java
new file mode 100644
index 00000000000..83f4c836d20
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/Utils.java
@@ -0,0 +1,72 @@
+// 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.server.http;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.jdisc.application.UriPattern;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+
+import java.net.URI;
+
+/**
+ * Utilities for handlers.
+ *
+ * @author musum
+ * @since 5.1.14
+ */
+public class Utils {
+
+ /**
+ * If request is an HTTP request and a jdisc request, return the {@link com.yahoo.jdisc.application.BindingMatch}
+ * of the request. Otherwise return a dummy match useful only for testing based on the <code>uriPattern</code>
+ * supplied.
+ *
+ * @param req an {@link com.yahoo.container.jdisc.HttpRequest}
+ * @param uriPattern a pattern to create a BindingMatch for in tests
+ * @return match
+ */
+ public static BindingMatch<?> getBindingMatch(HttpRequest req, String uriPattern) {
+ com.yahoo.jdisc.http.HttpRequest jDiscRequest = req.getJDiscRequest();
+ BindingMatch<?> bm = jDiscRequest.getBindingMatch();
+ if (bm == null) {
+ bm = new BindingMatch<>(
+ new UriPattern(uriPattern).match(URI.create(jDiscRequest.getUri().toString())),
+ new Object());
+ }
+ return bm;
+ }
+
+ public static String getUrlBase(HttpRequest request, String pathPrefix) {
+ return request.getUri().getScheme() + "://" + request.getHost() + ":" + request.getPort() + pathPrefix;
+ }
+
+ public static Tenant checkThatTenantExists(Tenants tenants, TenantName tenant) {
+ if (!tenants.tenantsCopy().containsKey(tenant)) {
+ throw new NotFoundException("Tenant '" + tenant + "' was not found.");
+ }
+ return tenants.tenantsCopy().get(tenant);
+ }
+
+ public static void checkThatTenantDoesNotExist(Tenants tenants, TenantName tenant) {
+ if (tenants.tenantsCopy().containsKey(tenant)) {
+ throw new BadRequestException("There already exists a tenant '" + tenant + "'");
+ }
+ }
+
+ public static TenantName getTenantFromRequest(HttpRequest request) {
+ BindingMatch<?> bm = getBindingMatch(request, "http://*/application/v2/tenant/*");
+ return TenantName.from(bm.group(2));
+ }
+
+ public static TenantName getTenantFromSessionRequest(HttpRequest request) {
+ BindingMatch<?> bm = getBindingMatch(request, "http://*/application/v2/tenant/*/session*");
+ return TenantName.from(bm.group(2));
+ }
+
+ public static TenantName getTenantFromApplicationsRequest(HttpRequest request) {
+ BindingMatch<?> bm = getBindingMatch(request, "http://*/application/v2/tenant/*/application*");
+ return TenantName.from(bm.group(2));
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentRequest.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentRequest.java
new file mode 100644
index 00000000000..ba7eff7c461
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentRequest.java
@@ -0,0 +1,51 @@
+// 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.server.http.v2;
+
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.http.ContentRequest;
+import com.yahoo.vespa.config.server.http.Utils;
+import com.yahoo.vespa.config.server.session.LocalSession;
+
+/**
+ * Represents a content request for an application.
+ *
+ * @author lulf
+ * @since 5.3
+ */
+class ApplicationContentRequest extends ContentRequest {
+
+ private static final String uriPattern = "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/content/*";
+ private final ApplicationId applicationId;
+ private final Zone zone;
+
+ private ApplicationContentRequest(HttpRequest request, LocalSession session, ApplicationId applicationId, Zone zone) {
+ super(request, session);
+ this.applicationId = applicationId;
+ this.zone = zone;
+ }
+
+ static ContentRequest create(HttpRequest request, LocalSession session, ApplicationId applicationId, Zone zone) {
+ return new ApplicationContentRequest(request, session, applicationId, zone);
+ }
+
+ @Override
+ protected String getContentPath(HttpRequest request) {
+ BindingMatch<?> bm = Utils.getBindingMatch(request, uriPattern);
+ return bm.group(7);
+ }
+
+ @Override
+ public String getPathPrefix() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("/application/v2/tenant/").append(applicationId.tenant().value());
+ sb.append("/application/").append(applicationId.application().value());
+ sb.append("/environment/").append(zone.environment().value());
+ sb.append("/region/").append(zone.region().value());
+ sb.append("/instance/").append(applicationId.instance().value());
+ return sb.toString();
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java
new file mode 100644
index 00000000000..458a3899d4c
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java
@@ -0,0 +1,256 @@
+// 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.server.http.v2;
+
+import com.yahoo.config.provision.*;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.server.RotationsCache;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.vespa.config.server.application.ApplicationConvergenceChecker;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.application.LogServerLogGrabber;
+import com.yahoo.vespa.config.server.http.*;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.session.LocalSession;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+import com.yahoo.vespa.config.server.session.RemoteSession;
+import com.yahoo.vespa.config.server.session.RemoteSessionRepo;
+
+import java.io.IOException;
+import java.time.Clock;
+import java.time.Duration;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
+/**
+ * Handler for deleting a currently active application for a tenant.
+ *
+ * @author musum
+ * @since 5.4
+ */
+public class ApplicationHandler extends HttpHandler {
+
+ private static final String REQUEST_PROPERTY_TIMEOUT = "timeout";
+ private final Tenants tenants;
+ private final ContentHandler contentHandler = new ContentHandler();
+ private final Optional<Provisioner> hostProvisioner;
+ private final ApplicationConvergenceChecker convergeChecker;
+ private final Zone zone;
+ private final LogServerLogGrabber logServerLogGrabber;
+
+ public ApplicationHandler(Executor executor, AccessLog accessLog, Tenants tenants,
+ HostProvisionerProvider hostProvisionerProvider, Zone zone,
+ ApplicationConvergenceChecker convergeChecker,
+ LogServerLogGrabber logServerLogGrabber) {
+ super(executor, accessLog);
+ this.tenants = tenants;
+ this.hostProvisioner = hostProvisionerProvider.getHostProvisioner();
+ this.zone = zone;
+ this.convergeChecker = convergeChecker;
+ this.logServerLogGrabber = logServerLogGrabber;
+ }
+
+ @Override
+ public HttpResponse handleDELETE(HttpRequest request) {
+ ApplicationId applicationId = getApplicationIdFromRequest(request);
+ Tenant tenant = verifyTenantAndApplication(applicationId);
+ ApplicationRepo applicationRepo = tenant.getApplicationRepo();
+ final long sessionId = applicationRepo.getSessionIdForApplication(applicationId);
+ final LocalSessionRepo localSessionRepo = tenant.getLocalSessionRepo();
+ final LocalSession session = localSessionRepo.getSession(sessionId);
+ if (session == null) {
+ return HttpErrorResponse.notFoundError("Unable to delete " + applicationId + " (session id " + sessionId + "):" +
+ "No local deployment for this application found on this config server");
+ }
+ log.log(LogLevel.INFO, "Deleting " + applicationId);
+ localSessionRepo.removeSession(session.getSessionId());
+ session.delete();
+ RotationsCache rotationsCache = new RotationsCache(tenant.getCurator(), tenant.getPath());
+ rotationsCache.deleteRotationFromZooKeeper(applicationId);
+ applicationRepo.deleteApplication(applicationId);
+ if (hostProvisioner.isPresent()) {
+ hostProvisioner.get().removed(applicationId);
+ }
+ return new DeleteApplicationResponse(Response.Status.OK, applicationId);
+ }
+
+
+ @Override
+ public HttpResponse handleGET(HttpRequest request) {
+ ApplicationId applicationId = getApplicationIdFromRequest(request);
+ Tenant tenant = verifyTenantAndApplication(applicationId);
+
+ if (isServiceConvergeRequest(request)) {
+ Application application = getApplication(tenant, applicationId);
+ return convergeChecker.nodeConvergenceCheck(application, getHostFromRequest(request), request.getUri());
+ }
+ if (isContentRequest(request)) {
+ LocalSession session = SessionHandler.getSessionFromRequest(tenant.getLocalSessionRepo(), tenant.getApplicationRepo().getSessionIdForApplication(applicationId));
+ return contentHandler.get(ApplicationContentRequest.create(request, session, applicationId, zone));
+ }
+ Application application = getApplication(tenant, applicationId);
+
+ // TODO: Remove this once the config convegence logic is moved to client and is live for all clusters.
+ if (isConvergeRequest(request)) {
+ try {
+ convergeChecker.waitForConfigConverged(application, new TimeoutBudget(Clock.systemUTC(), durationFromRequestTimeout(request)));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ if (isServiceConvergeListRequest(request)) {
+ return convergeChecker.listConfigConvergence(application, request.getUri());
+ }
+ return new GetApplicationResponse(Response.Status.OK, application.getApplicationGeneration());
+ }
+
+ @Override
+ public HttpResponse handlePOST(HttpRequest request) {
+ ApplicationId applicationId = getApplicationIdFromRequest(request);
+ Tenant tenant = verifyTenantAndApplication(applicationId);
+ if (request.getUri().getPath().endsWith("restart"))
+ return handlePostRestart(request, applicationId);
+ if (request.getUri().getPath().endsWith("log"))
+ return handlePostLog(request, applicationId, tenant);
+ throw new NotFoundException("Illegal POST request '" + request.getUri() + "': Must end by /restart or /log");
+ }
+
+ private HttpResponse handlePostRestart(HttpRequest request, ApplicationId applicationId) {
+ if (getBindingMatch(request).groupCount() != 7)
+ throw new NotFoundException("Illegal POST restart request '" + request.getUri() +
+ "': Must have 6 arguments but had " + ( getBindingMatch(request).groupCount()-1 ) );
+ if (hostProvisioner.isPresent())
+ hostProvisioner.get().restart(applicationId, hostFilterFrom(request));
+ return new JSONResponse(Response.Status.OK); // return empty
+ }
+
+ private HttpResponse handlePostLog(HttpRequest request, ApplicationId applicationId, Tenant tenant) {
+ if (getBindingMatch(request).groupCount() != 7)
+ throw new NotFoundException("Illegal POST log request '" + request.getUri() +
+ "': Must have 6 arguments but had " + ( getBindingMatch(request).groupCount()-1 ) );
+ Application application = getApplication(tenant, applicationId);
+ return logServerLogGrabber.grabLog(application);
+ }
+
+ private HostFilter hostFilterFrom(HttpRequest request) {
+ return HostFilter.from(request.getProperty("hostname"),
+ request.getProperty("flavor"),
+ request.getProperty("clusterType"),
+ request.getProperty("clusterId"));
+ }
+
+ private Tenant verifyTenantAndApplication(ApplicationId applicationId) {
+ Tenant tenant = Utils.checkThatTenantExists(tenants, applicationId.tenant());
+ List<ApplicationId> applicationIds = listApplicationIds(tenant);
+ if ( ! applicationIds.contains(applicationId)) {
+ throw new NotFoundException("No such application id: " + applicationId);
+ }
+ return tenant;
+ }
+
+ private Duration durationFromRequestTimeout(HttpRequest request) {
+ long timeoutInSeconds = 60;
+ if (request.hasProperty(REQUEST_PROPERTY_TIMEOUT)) {
+ timeoutInSeconds = Long.parseLong(request.getProperty(REQUEST_PROPERTY_TIMEOUT));
+ }
+ return Duration.ofSeconds(timeoutInSeconds);
+ }
+
+ private Application getApplication(Tenant tenant, ApplicationId applicationId) {
+ ApplicationRepo applicationRepo = tenant.getApplicationRepo();
+ RemoteSessionRepo remoteSessionRepo = tenant.getRemoteSessionRepo();
+ long sessionId = applicationRepo.getSessionIdForApplication(applicationId);
+ RemoteSession session = remoteSessionRepo.getSession(sessionId, 0);
+ return session.ensureApplicationLoaded().getForVersionOrLatest(Optional.empty());
+ }
+
+ private List<ApplicationId> listApplicationIds(Tenant tenant) {
+ ApplicationRepo applicationRepo = tenant.getApplicationRepo();
+ return applicationRepo.listApplications();
+ }
+
+ // Note: Update src/main/resources/configserver-app/services.xml if you do any changes to the bindings
+ private static BindingMatch<?> getBindingMatch(HttpRequest request) {
+ return HttpConfigRequests.getBindingMatch(request,
+ "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/content/*",
+ "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/log",
+ "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/restart",
+ "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/converge",
+ "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/serviceconverge",
+ "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/serviceconverge/*",
+ "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*",
+ "http://*/application/v2/tenant/*/application/*");
+ }
+
+ private static boolean isConvergeRequest(HttpRequest request) {
+ return getBindingMatch(request).groupCount() == 7 &&
+ request.getUri().getPath().endsWith("converge");
+ }
+
+ private static boolean isServiceConvergeListRequest(HttpRequest request) {
+ return getBindingMatch(request).groupCount() == 7 &&
+ request.getUri().getPath().endsWith("serviceconverge");
+ }
+
+ private static boolean isServiceConvergeRequest(HttpRequest request) {
+ return getBindingMatch(request).groupCount() == 8 &&
+ request.getUri().getPath().contains("/serviceconverge/");
+ }
+
+
+ private static boolean isContentRequest(HttpRequest request) {
+ return getBindingMatch(request).groupCount() > 7;
+ }
+
+ private static String getHostFromRequest(HttpRequest req) {
+ BindingMatch<?> bm = getBindingMatch(req);
+ return bm.group(7);
+ }
+
+ private static ApplicationId getApplicationIdFromRequest(HttpRequest req) {
+ // Two bindings for this: with full app id or only application name
+ BindingMatch<?> bm = getBindingMatch(req);
+ if (bm.groupCount() > 4) return createFromRequestFullAppId(bm);
+ return createFromRequestSimpleAppId(bm);
+ }
+
+ // The URL pattern with only tenant and application given
+ private static ApplicationId createFromRequestSimpleAppId(BindingMatch<?> bm) {
+ TenantName tenant = TenantName.from(bm.group(2));
+ ApplicationName application = ApplicationName.from(bm.group(3));
+ return new ApplicationId.Builder().tenant(tenant).applicationName(application).build();
+ }
+
+ // The URL pattern with full app id given
+ private static ApplicationId createFromRequestFullAppId(BindingMatch<?> bm) {
+ String tenant = bm.group(2);
+ String application = bm.group(3);
+ String instance = bm.group(6);
+ return new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName(application).instanceName(instance)
+ .build();
+ }
+
+ private static class DeleteApplicationResponse extends JSONResponse {
+ public DeleteApplicationResponse(int status, ApplicationId applicationId) {
+ super(status);
+ object.setString("message", "Application '" + applicationId + "' deleted");
+ }
+ }
+
+ private static class GetApplicationResponse extends JSONResponse {
+ public GetApplicationResponse(int status, long generation) {
+ super(status);
+ object.setLong("generation", generation);
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HostHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HostHandler.java
new file mode 100644
index 00000000000..086954c384f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HostHandler.java
@@ -0,0 +1,78 @@
+// 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.server.http.v2;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.server.GlobalComponentRegistry;
+import com.yahoo.vespa.config.server.HostRegistries;
+import com.yahoo.vespa.config.server.HostRegistry;
+import com.yahoo.vespa.config.server.http.*;
+
+import java.util.concurrent.Executor;
+
+
+/**
+ * Handler for getting tenant and application for a given hostname.
+ *
+ * @author musum
+ * @since 5.19
+ */
+public class HostHandler extends HttpHandler {
+ final HostRegistries hostRegistries;
+ private final Zone zone;
+
+ public HostHandler(Executor executor, AccessLog accessLog, GlobalComponentRegistry globalComponentRegistry) {
+ super(executor, accessLog);
+ this.hostRegistries = globalComponentRegistry.getHostRegistries();
+ this.zone = globalComponentRegistry.getZone();
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest request) {
+ String hostname = getBindingMatch(request).group(2);
+ log.log(LogLevel.DEBUG, "hostname=" + hostname);
+
+ HostRegistry<TenantName> tenantHostRegistry = hostRegistries.getTenantHostRegistry();
+ log.log(LogLevel.DEBUG, "hosts in tenant host registry '" + tenantHostRegistry + "' " + tenantHostRegistry.getAllHosts());
+ TenantName tenant = tenantHostRegistry.getKeyForHost(hostname);
+ if (tenant == null) return createError(hostname);
+ log.log(LogLevel.DEBUG, "tenant=" + tenant);
+ HostRegistry<ApplicationId> applicationIdHostRegistry = hostRegistries.getApplicationHostRegistry(tenant);
+ ApplicationId applicationId;
+ if (applicationIdHostRegistry == null) return createError(hostname);
+ applicationId = applicationIdHostRegistry.getKeyForHost(hostname);
+ log.log(LogLevel.DEBUG, "applicationId=" + applicationId);
+ if (applicationId == null) {
+ return createError(hostname);
+ } else {
+ log.log(LogLevel.DEBUG, "hosts in application host registry '" + applicationIdHostRegistry + "' " + applicationIdHostRegistry.getAllHosts());
+ return new HostResponse(Response.Status.OK, applicationId, zone);
+ }
+ }
+
+ private HttpErrorResponse createError(String hostname) {
+ return HttpErrorResponse.notFoundError("Could not find any application using host '" + hostname + "'");
+ }
+
+ private static BindingMatch<?> getBindingMatch(HttpRequest request) {
+ return HttpConfigRequests.getBindingMatch(request, "http://*/application/v2/host/*");
+ }
+
+ private static class HostResponse extends JSONResponse {
+ public HostResponse(int status, ApplicationId applicationId, Zone zone) {
+ super(status);
+ object.setString("tenant", applicationId.tenant().value());
+ object.setString("application", applicationId.application().value());
+ object.setString("environment", zone.environment().value());
+ object.setString("region", zone.region().value());
+ object.setString("instance", applicationId.instance().value());
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpConfigRequests.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpConfigRequests.java
new file mode 100644
index 00000000000..b63c52a26bb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpConfigRequests.java
@@ -0,0 +1,54 @@
+// 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.server.http.v2;
+
+import java.net.URI;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.jdisc.application.UriPattern;
+import com.yahoo.jdisc.application.UriPattern.Match;
+import com.yahoo.vespa.config.server.RequestHandler;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.http.NotFoundException;
+
+/**
+ * Helpers for v2 config REST API
+ *
+ * @author vegardh
+ */
+public class HttpConfigRequests {
+
+ static final String RECURSIVE_QUERY_PROPERTY = "recursive";
+
+ /**
+ * Produces the binding match for the request. If it's not available on the jDisc request, create one for
+ * testing using the long and short app id URL patterns given.
+ * @param req an {@link com.yahoo.container.jdisc.HttpRequest}
+ * @param patterns A list of patterns that should be matched if no match on binding.
+ * @return match
+ */
+ public static BindingMatch<?> getBindingMatch(HttpRequest req, String ... patterns) {
+ com.yahoo.jdisc.http.HttpRequest jDiscRequest = req.getJDiscRequest();
+ if (jDiscRequest==null) throw new IllegalArgumentException("No JDisc request for: " + req.getUri());
+ BindingMatch<?> jdBm = jDiscRequest.getBindingMatch();
+ if (jdBm!=null) return jdBm;
+
+ // If not, use provided patterns
+ for (String pattern : patterns) {
+ UriPattern fullAppIdPattern = new UriPattern(pattern);
+ URI uri = req.getUri();
+ Match match = fullAppIdPattern.match(uri);
+ if (match!=null) return new BindingMatch<>(match, new Object());
+ }
+ throw new IllegalArgumentException("Illegal url for config request: " + req.getUri());
+ }
+
+
+ static RequestHandler getRequestHandler(Tenants tenants, TenantRequest request) {
+ Tenant tenant = tenants.tenantsCopy().get(request.getApplicationId().tenant());
+ if (tenant==null) throw new NotFoundException("No such tenant: "+request.getApplicationId().tenant());
+ return tenant.getRequestHandler();
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpGetConfigHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpGetConfigHandler.java
new file mode 100644
index 00000000000..6c872578324
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpGetConfigHandler.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.server.http.v2;
+
+import com.google.inject.Inject;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.protocol.ConfigResponse;
+import com.yahoo.vespa.config.server.RequestHandler;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.http.HttpConfigRequest;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.config.server.http.HttpHandler;
+
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
+/**
+ * HTTP handler for a getConfig operation
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class HttpGetConfigHandler extends HttpHandler {
+
+ private final Tenants tenants;
+
+ @Inject
+ public HttpGetConfigHandler(Executor executor, AccessLog accesslog, Tenants tenants) {
+ super(executor, accesslog);
+ this.tenants = tenants;
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest req) {
+ HttpConfigRequest request = HttpConfigRequest.createFromRequestV2(req);
+ RequestHandler requestHandler = HttpConfigRequests.getRequestHandler(tenants, request);
+ HttpConfigRequest.validateRequestKey(request.getConfigKey(), requestHandler, request.getApplicationId());
+ return HttpConfigResponse.createFromConfig(resolveConfig(request, requestHandler));
+ }
+
+ private ConfigResponse resolveConfig(HttpConfigRequest request, RequestHandler requestHandler) {
+ log.log(LogLevel.DEBUG, "nocache=" + request.noCache());
+ ConfigResponse config = requestHandler.resolveConfig(request.getApplicationId(), request, Optional.empty());
+ if (config == null) HttpConfigRequest.throwModelNotReady();
+ return config;
+ }
+} \ No newline at end of file
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsHandler.java
new file mode 100644
index 00000000000..0358a9a7046
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsHandler.java
@@ -0,0 +1,180 @@
+// 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.server.http.v2;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.*;
+import java.util.concurrent.Executor;
+import com.google.inject.Inject;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.path.Path;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.server.RequestHandler;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.config.server.http.HttpHandler;
+import com.yahoo.vespa.config.server.http.Utils;
+import static com.yahoo.jdisc.http.HttpResponse.Status.*;
+
+/**
+ * Handler for a list configs operation. Lists all configs in model for a given application and tenant.
+ *
+ * @author vegardh
+ * @since 5.3
+ */
+public class HttpListConfigsHandler extends HttpHandler {
+ private final Tenants tenants;
+ private final Zone zone;
+
+ @Inject
+ public HttpListConfigsHandler(Executor executor, AccessLog accesslog, Tenants tenants, Zone zone) {
+ super(executor, accesslog);
+ this.tenants = tenants;
+ this.zone = zone;
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest req) {
+ HttpListConfigsRequest listReq = HttpListConfigsRequest.createFromListRequest(req);
+ RequestHandler requestHandler = HttpConfigRequests.getRequestHandler(tenants, listReq);
+ ApplicationId appId = listReq.getApplicationId();
+ Set<ConfigKey<?>> configs = requestHandler.listConfigs(appId, Optional.empty(), listReq.isRecursive());
+ String urlBase = getUrlBase(req, listReq, appId, zone);
+ Set<ConfigKey<?>> allConfigs = requestHandler.allConfigsProduced(appId, Optional.empty());
+ return new ListConfigsResponse(configs, allConfigs, urlBase, listReq.isRecursive());
+ }
+
+ static String getUrlBase(HttpRequest req, HttpListConfigsRequest listReq, ApplicationId appId, Zone zone) {
+ if (listReq.isFullAppId())
+ return Utils.getUrlBase(req,
+ Path.fromString("/config/v2/tenant/").
+ append(appId.tenant().value()).
+ append("application").
+ append(appId.application().value()).
+ append("environment").
+ append(zone.environment().value()).
+ append("region").
+ append(zone.region().value()).
+ append("instance").
+ append(appId.instance().value()).getAbsolute()+"/");
+ else
+ return Utils.getUrlBase(req,
+ Path.fromString("/config/v2/tenant/").
+ append(appId.tenant().value()).
+ append("application").
+ append(appId.application().value()).getAbsolute()+"/");
+ }
+
+ static class ListConfigsResponse extends HttpResponse {
+ private final List<ConfigKey<?>> configs;
+ private final Set<ConfigKey<?>> allConfigs;
+ private final String urlBase;
+ private final boolean recursive;
+
+ /**
+ * New list response
+ *
+ * @param configs the configs to include in the list
+ * @param urlBase for example "http://foo.com:19071/config/v1/ (configs are appended to the listed URLs based on configs list)
+ * @param recursive list recursively
+ */
+ public ListConfigsResponse(Set<ConfigKey<?>> configs, Set<ConfigKey<?>> allConfigs, String urlBase, boolean recursive) {
+ super(OK);
+ this.configs = new ArrayList<>(configs);
+ Collections.sort(this.configs);
+ this.allConfigs = allConfigs;
+ this.urlBase = urlBase;
+ this.recursive = recursive;
+ }
+
+ /**
+ * The listing URL for this config in this service
+ *
+ * @param key config key
+ * @param rec recursive
+ * @return url
+ */
+ public String toUrl(ConfigKey<?> key, boolean rec) {
+ return urlBase + key.getNamespace() + "." + key.getName() + configIdUrlPart(rec, key.getConfigId());
+ }
+
+ // Do not end with / if it's a recursive listing. Furthermore, don't do it if it's the empty config id (special handling of the root config id).
+ private String configIdUrlPart(boolean rec, String configId) {
+ if ("".equals(configId)) return "";
+ if (rec) return "/" + configId;
+ return "/" + configId + "/";
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+ Cursor array;
+ if (!recursive) {
+ array = root.setArray("children");
+ for (ConfigKey<?> key : keysThatHaveAChildWithSameName(configs, allConfigs)) {
+ array.addString(toUrl(key, false));
+ }
+ }
+ array = root.setArray("configs");
+
+ Collection<ConfigKey<?>> cfs;
+ if (recursive) {
+ cfs = configs;
+ } else {
+ cfs = withParentConfigId(configs);
+ }
+ for (ConfigKey<?> key : cfs) {
+ array.addString(toUrl(key, true));
+ }
+ new JsonFormat(true).encode(outputStream, slime);
+ }
+
+ public static Set<ConfigKey<?>> keysThatHaveAChildWithSameName(Collection<ConfigKey<?>> keys, Set<ConfigKey<?>> allConfigs) {
+ Set<ConfigKey<?>> ret = new LinkedHashSet<>();
+ for (ConfigKey<?> k : keys) {
+ if (ListConfigsResponse.hasAChild(k, allConfigs)) ret.add(k);
+ }
+ return ret;
+ }
+
+ // Do we do this already somewhere?
+ private static Set<ConfigKey<?>> withParentConfigId(Collection<ConfigKey<?>> keys) {
+ Set<ConfigKey<?>> ret = new LinkedHashSet<>();
+ for (ConfigKey<?> k : keys) {
+ ret.add(new ConfigKey<>(k.getName(), parentConfigId(k.getConfigId()), k.getNamespace()));
+ }
+ return ret;
+ }
+
+ static String parentConfigId(String id) {
+ if (id==null) return null;
+ if (!id.contains("/")) return "";
+ return id.substring(0, id.lastIndexOf('/'));
+ }
+
+ static boolean hasAChild(ConfigKey<?> key, Set<ConfigKey<?>> keys) {
+ if ("".equals(key.getConfigId())) return false;
+ for (ConfigKey<?> k : keys) {
+ if (!k.getName().equals(key.getName())) continue;
+ if ("".equals(k.getConfigId())) continue;
+ if (k.getConfigId().equals(key.getConfigId())) continue;
+ if (k.getConfigId().startsWith(key.getConfigId())) return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsRequest.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsRequest.java
new file mode 100644
index 00000000000..1140d6e769f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsRequest.java
@@ -0,0 +1,156 @@
+// 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.server.http.v2;
+
+import com.yahoo.collections.Tuple2;
+import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.http.HttpConfigRequest;
+
+/**
+ * A request to list config, bound to tenant and app id. Optionally bound to a config key, for request for named config.
+ *
+ * @author vegardh
+ *
+ */
+public class HttpListConfigsRequest implements TenantRequest {
+ private final ConfigKey<?> key; // non-null if it's a named list request
+ private final ApplicationId appId;
+ private final boolean recursive;
+ private final boolean fullAppId;
+
+ private HttpListConfigsRequest(ConfigKey<?> key, ApplicationId appId, boolean recursive, boolean fullAppId) {
+ this.key = key;
+ this.appId = appId;
+ this.recursive = recursive;
+ this.fullAppId = fullAppId;
+ }
+
+ public static HttpListConfigsRequest createFromListRequest(HttpRequest req) {
+ BindingMatch<?> bm = HttpConfigRequests.getBindingMatch(req,
+ "http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/",
+ "http://*/config/v2/tenant/*/application/*/");
+ if (bm.groupCount()>4) return createFromListRequestFullAppId(req, bm);
+ return createFromListRequestSimpleAppId(req, bm);
+ }
+
+ private static HttpListConfigsRequest createFromListRequestSimpleAppId(HttpRequest req, BindingMatch<?> bm) {
+ TenantName tenant = TenantName.from(bm.group(2));
+ ApplicationName application = ApplicationName.from(bm.group(3));
+ return new HttpListConfigsRequest(null, new ApplicationId.Builder().tenant(tenant).applicationName(application).build(),
+ req.getBooleanProperty(HttpConfigRequests.RECURSIVE_QUERY_PROPERTY), false);
+ }
+
+ private static HttpListConfigsRequest createFromListRequestFullAppId(HttpRequest req, BindingMatch<?> bm) {
+ String tenant = bm.group(2);
+ String application = bm.group(3);
+ String environment = bm.group(4);
+ String region = bm.group(5);
+ String instance = bm.group(6);
+
+ ApplicationId appId = new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName(application)
+ .instanceName(instance)
+ .build();
+ return new HttpListConfigsRequest(null, appId,
+ req.getBooleanProperty(HttpConfigRequests.RECURSIVE_QUERY_PROPERTY), true);
+ }
+
+ public static HttpListConfigsRequest createFromNamedListRequest(HttpRequest req) {
+ // http://*/config/v2/tenant/*/application/*/*/
+ // http://*/config/v2/tenant/*/application/*/*/*/
+ // http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/*/
+ // http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/*/*/
+ BindingMatch<?> bm = HttpConfigRequests.getBindingMatch(req,
+ "http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/*/*/",
+ "http://*/config/v2/tenant/*/application/*/*/*/");
+ if (bm.groupCount()>6) return createFromNamedListRequestFullAppId(req, bm);
+ return createFromNamedListRequestSimpleAppId(req, bm);
+ }
+
+ private static HttpListConfigsRequest createFromNamedListRequestSimpleAppId(HttpRequest req, BindingMatch<?> bm) {
+ TenantName tenant = TenantName.from(bm.group(2));
+ ApplicationName application = ApplicationName.from(bm.group(3));
+ String conf = bm.group(4);
+ String cId;
+ String cName;
+ String cNamespace;
+ if (bm.groupCount() >= 6) {
+ cId = bm.group(5);
+ } else {
+ cId = "";
+ }
+ Tuple2<String, String> nns = HttpConfigRequest.nameAndNamespace(conf);
+ cName = nns.first;
+ cNamespace = nns.second;
+ ConfigKey<?> key = new ConfigKey<>(cName, cId, cNamespace);
+ return new HttpListConfigsRequest(key, new ApplicationId.Builder().tenant(tenant).applicationName(application).build(),
+ req.getBooleanProperty(HttpConfigRequests.RECURSIVE_QUERY_PROPERTY), false);
+ }
+
+ private static HttpListConfigsRequest createFromNamedListRequestFullAppId(HttpRequest req, BindingMatch<?> bm) {
+ String tenant = bm.group(2);
+ String application = bm.group(3);
+ String environment = bm.group(4);
+ String region = bm.group(5);
+ String instance = bm.group(6);
+ String conf = bm.group(7);
+ String cId;
+ String cName;
+ String cNamespace;
+ if (bm.groupCount() >= 9) {
+ cId = bm.group(8);
+ } else {
+ cId = "";
+ }
+ Tuple2<String, String> nns = HttpConfigRequest.nameAndNamespace(conf);
+ cName = nns.first;
+ cNamespace = nns.second;
+ ConfigKey<?> key = new ConfigKey<>(cName, cId, cNamespace);
+ ApplicationId appId = new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName(application)
+ .instanceName(instance)
+ .build();
+ return new HttpListConfigsRequest(key, appId,
+ req.getBooleanProperty(HttpConfigRequests.RECURSIVE_QUERY_PROPERTY), true);
+ }
+
+ /**
+ * The application id of the request
+ * @return app id
+ */
+ @Override
+ public ApplicationId getApplicationId() {
+ return appId;
+ }
+
+ /**
+ * True if the request was of the recursive form
+ * @return recursive
+ */
+ public boolean isRecursive() {
+ return recursive;
+ }
+
+ /**
+ * True if this was created using a URL with tenant, application, environment, region and instance; false if only tenant and application
+ * @return true if full app id used
+ */
+ public boolean isFullAppId() {
+ return fullAppId;
+ }
+
+ /**
+ * Returns the config key of the request if it was for a named config, or null if it was just a listing request
+ * @return key or null
+ */
+ public ConfigKey<?> getKey() {
+ return key;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListNamedConfigsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListNamedConfigsHandler.java
new file mode 100644
index 00000000000..ed4accb386b
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListNamedConfigsHandler.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.server.http.v2;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+import com.google.inject.Inject;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.server.RequestHandler;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.http.HttpConfigRequest;
+import com.yahoo.vespa.config.server.http.HttpHandler;
+
+/**
+ * Handler for a list named configs operation. Lists all configs in model for a given application and tenant, config name and optionally config id.
+ *
+ * @author vegardh
+ * @since 5.3
+ */
+public class HttpListNamedConfigsHandler extends HttpHandler {
+
+ private final Tenants tenants;
+ private final Zone zone;
+
+ @Inject
+ public HttpListNamedConfigsHandler(Executor executor, AccessLog accesslog, Tenants tenants, Zone zone) {
+ super(executor, accesslog);
+ this.tenants = tenants;
+ this.zone = zone;
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest req) {
+ HttpListConfigsRequest listReq = HttpListConfigsRequest.createFromNamedListRequest(req);
+ RequestHandler requestHandler = HttpConfigRequests.getRequestHandler(tenants, listReq);
+ ApplicationId appId = listReq.getApplicationId();
+ HttpConfigRequest.validateRequestKey(listReq.getKey(), requestHandler, appId);
+ Set<ConfigKey<?>> configs = requestHandler.listNamedConfigs(appId, Optional.empty(), listReq.getKey(), listReq.isRecursive());
+ String urlBase = HttpListConfigsHandler.getUrlBase(req, listReq, appId, zone);
+ Set<ConfigKey<?>> allConfigs = requestHandler.allConfigsProduced(appId, Optional.empty());
+ return new HttpListConfigsHandler.ListConfigsResponse(configs, allConfigs, urlBase, listReq.isRecursive());
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsHandler.java
new file mode 100644
index 00000000000..e77c3928f11
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsHandler.java
@@ -0,0 +1,67 @@
+// 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.server.http.v2;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.Response;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.http.HttpHandler;
+import com.yahoo.vespa.config.server.http.Utils;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Handler for listing currently active applications for a tenant.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ListApplicationsHandler extends HttpHandler {
+ private final Tenants tenants;
+ private final Zone zone;
+ public ListApplicationsHandler(Executor executor, AccessLog accessLog, Tenants tenants, Zone zone) {
+ super(executor, accessLog);
+ this.tenants = tenants;
+ this.zone = zone;
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest request) {
+ TenantName tenantName = Utils.getTenantFromApplicationsRequest(request);
+ final String urlBase = Utils.getUrlBase(request, "/application/v2/tenant/" + tenantName + "/application/");
+
+ List<ApplicationId> applicationIds = listApplicationIds(tenantName);
+ Collection<String> applicationUrls = Collections2.transform(applicationIds, new Function<ApplicationId, String>() {
+ @Override
+ public String apply(ApplicationId id) {
+ return createUrlStringFromId(urlBase, id, zone);
+ }
+ });
+ return new ListApplicationsResponse(Response.Status.OK, applicationUrls);
+ }
+
+ private List<ApplicationId> listApplicationIds(TenantName tenantName) {
+ Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName);
+ ApplicationRepo applicationRepo = tenant.getApplicationRepo();
+ return applicationRepo.listApplications();
+ }
+
+ private static String createUrlStringFromId(String urlBase, ApplicationId id, Zone zone) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(urlBase).append(id.application().value());
+ sb.append("/environment/").append(zone.environment().value());
+ sb.append("/region/").append(zone.region().value());
+ sb.append("/instance/").append(id.instance().value());
+ return sb.toString();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsResponse.java
new file mode 100644
index 00000000000..3efd50fade6
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsResponse.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.server.http.v2;
+
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Collection;
+
+/**
+ * Response that lists applications.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ListApplicationsResponse extends HttpResponse {
+ private final Slime slime = new Slime();
+ public ListApplicationsResponse(int status, Collection<String> applications) {
+ super(status);
+ Cursor array = slime.setArray();
+ for (String url : applications) {
+ array.addString(url);
+ }
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ new JsonFormat(true).encode(outputStream, slime);
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsHandler.java
new file mode 100644
index 00000000000..0fd456b6bd0
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsHandler.java
@@ -0,0 +1,36 @@
+// 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.server.http.v2;
+
+import java.util.concurrent.Executor;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.vespa.config.server.http.HttpHandler;
+import com.yahoo.vespa.config.server.Tenants;
+
+/**
+ * Handler to list tenants in the configserver
+ *
+ * @author vegardh
+ *
+ */
+public class ListTenantsHandler extends HttpHandler {
+
+ private final Tenants tenants;
+
+
+ public ListTenantsHandler(Executor executor, AccessLog accessLog, Tenants tenants) {
+ super(executor, accessLog);
+ this.tenants = tenants;
+ }
+
+
+ @Override
+ protected HttpResponse handleGET(HttpRequest request) {
+ return new ListTenantsResponse(tenants.tenantsCopy().keySet());
+ }
+
+
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsResponse.java
new file mode 100644
index 00000000000..35ff6faa89f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsResponse.java
@@ -0,0 +1,40 @@
+// 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.server.http.v2;
+
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.slime.Cursor;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.config.server.http.SessionResponse;
+
+import java.util.Collection;
+
+/**
+ * Tenant list response
+ *
+ * @author vegardh
+ *
+ */
+public class ListTenantsResponse extends SessionResponse {
+ private final Collection<TenantName> tenantNames;
+
+ public ListTenantsResponse(final Collection<TenantName> tenants) {
+ super();
+ this.tenantNames = tenants;
+ Cursor tenantArray = this.root.setArray("tenants");
+ synchronized (tenants) {
+ for (final TenantName tenantName : tenants) {
+ tenantArray.addString(tenantName.value());
+ }
+ }
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+
+ public Collection<TenantName> getTenantNames() {
+ return tenantNames;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandler.java
new file mode 100644
index 00000000000..ee926f39cad
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandler.java
@@ -0,0 +1,59 @@
+// 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.server.http.v2;
+
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
+import com.google.inject.Inject;
+import com.yahoo.config.provision.Provisioner;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.http.SessionActiveHandlerBase;
+import com.yahoo.vespa.config.server.http.SessionHandler;
+import com.yahoo.vespa.config.server.http.Utils;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.session.LocalSession;
+
+/**
+ * Handler that activates a session given by tenant and id (PUT).
+ *
+ * @author vegardh
+ * @since 5.1
+ */
+public class SessionActiveHandler extends SessionActiveHandlerBase {
+
+ private final Tenants tenants;
+ private final Optional<Provisioner> hostProvisioner;
+ private final Zone zone;
+
+ @Inject
+ public SessionActiveHandler(Executor executor,
+ AccessLog accessLog,
+ Tenants tenants,
+ HostProvisionerProvider hostProvisionerProvider,
+ Zone zone) {
+ super(executor, accessLog);
+ this.tenants = tenants;
+ this.hostProvisioner = hostProvisionerProvider.getHostProvisioner();
+ this.zone = zone;
+ }
+
+ @Override
+ protected HttpResponse handlePUT(HttpRequest request) {
+ TimeoutBudget timeoutBudget = getTimeoutBudget(request, SessionHandler.DEFAULT_ACTIVATE_TIMEOUT);
+ TenantName tenantName = Utils.getTenantFromSessionRequest(request);
+ log.log(LogLevel.DEBUG, "Found tenant '" + tenantName + "' in request");
+ Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName);
+ LocalSession localSession = getSessionFromRequestV2(tenant.getLocalSessionRepo(), request);
+ activate(request, tenant.getLocalSessionRepo(), tenant.getActivateLock(), timeoutBudget, hostProvisioner, localSession);
+ return new SessionActiveResponse(localSession.getMetaData().getSlime(), tenantName, request, localSession, zone);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveResponse.java
new file mode 100644
index 00000000000..b5948c7e786
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveResponse.java
@@ -0,0 +1,27 @@
+// 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.server.http.v2;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.slime.Slime;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.http.SessionResponse;
+import com.yahoo.vespa.config.server.session.LocalSession;
+
+public class SessionActiveResponse extends SessionResponse {
+
+ public SessionActiveResponse(Slime metaData, TenantName tenantName, HttpRequest request, LocalSession session, Zone zone) {
+ super(metaData, metaData.get());
+ String message = "Session " + session.getSessionId() + " for tenant '" + tenantName + "' activated.";
+ root.setString("tenant", tenantName.value());
+ root.setString("message", message);
+ final ApplicationId applicationId = session.getApplicationId();
+ root.setString("url", "http://" + request.getHost() + ":" + request.getPort() +
+ "/application/v2/tenant/" + tenantName +
+ "/application/" + applicationId.application().value() +
+ "/environment/" + zone.environment().value() +
+ "/region/" + zone.region().value() +
+ "/instance/" + applicationId.instance().value());
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandler.java
new file mode 100644
index 00000000000..669ec049770
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandler.java
@@ -0,0 +1,70 @@
+// 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.server.http.v2;
+
+import com.google.inject.Inject;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.http.ContentHandler;
+import com.yahoo.vespa.config.server.http.NotFoundException;
+import com.yahoo.vespa.config.server.http.SessionHandler;
+import com.yahoo.vespa.config.server.http.Utils;
+import com.yahoo.vespa.config.server.session.LocalSession;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A handler that will return content or content status for files or directories
+ * in the session's application package
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionContentHandler extends SessionHandler {
+ private final Tenants tenants;
+ private final ContentHandler contentHandler = new ContentHandler();
+
+ @Inject
+ public SessionContentHandler(Executor executor, AccessLog accessLog, Tenants tenants) {
+ super(executor, accessLog);
+ this.tenants = tenants;
+ }
+
+ @Override
+ public HttpResponse handleGET(HttpRequest request) {
+ TenantName tenantName = Utils.getTenantFromSessionRequest(request);
+ log.log(LogLevel.DEBUG, "Found tenant '" + tenantName + "' in request");
+ Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName);
+ LocalSession session = getLocalSession(request, tenant.getLocalSessionRepo());
+ return contentHandler.get(SessionContentRequestV2.create(request, session));
+ }
+
+ private LocalSession getLocalSession(HttpRequest request, LocalSessionRepo localSessionRepo) {
+ LocalSession session = getSessionFromRequestV2(localSessionRepo, request);
+ if (session == null) {
+ throw new NotFoundException("No valid session id in request " + request.getUri().toString());
+ }
+ return session;
+ }
+
+ @Override
+ public HttpResponse handlePUT(HttpRequest request) {
+ TenantName tenantName = Utils.getTenantFromSessionRequest(request);
+ log.log(LogLevel.DEBUG, "Found tenant '" + tenantName + "' in request");
+ Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName);
+ return contentHandler.put(SessionContentRequestV2.create(request, getSessionFromRequestV2(tenant.getLocalSessionRepo(), request)));
+ }
+
+ @Override
+ public HttpResponse handleDELETE(HttpRequest request) {
+ TenantName tenantName = Utils.getTenantFromSessionRequest(request);
+ log.log(LogLevel.DEBUG, "Found tenant '" + tenantName + "' in request");
+ Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName);
+ return contentHandler.delete(SessionContentRequestV2.create(request, getSessionFromRequestV2(tenant.getLocalSessionRepo(), request)));
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentRequestV2.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentRequestV2.java
new file mode 100644
index 00000000000..16d3fd6802a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentRequestV2.java
@@ -0,0 +1,43 @@
+// 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.server.http.v2;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.vespa.config.server.http.ContentRequest;
+import com.yahoo.vespa.config.server.http.Utils;
+import com.yahoo.vespa.config.server.session.LocalSession;
+
+/**
+ * Requests for content and content status (v2)
+ * are handled by this class.
+ *
+ * @author musum
+ * @since 5.3
+ */
+class SessionContentRequestV2 extends ContentRequest {
+ private static final String uriPattern = "http://*/application/v2/tenant/*/session/*/content/*";
+ private final TenantName tenantName;
+ private final long sessionId;
+
+ private SessionContentRequestV2(HttpRequest request, LocalSession session, TenantName tenantName) {
+ super(request, session);
+ this.tenantName = tenantName;
+ this.sessionId = session.getSessionId();
+ }
+
+ static ContentRequest create(HttpRequest request, LocalSession session) {
+ return new SessionContentRequestV2(request, session, Utils.getTenantFromSessionRequest(request));
+ }
+
+ @Override
+ public String getPathPrefix() {
+ return "/application/v2/tenant/" + tenantName.value() + "/session/" + sessionId;
+ }
+
+ @Override
+ protected String getContentPath(HttpRequest request) {
+ BindingMatch<?> bm = Utils.getBindingMatch(request, uriPattern);
+ return bm.group(4);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java
new file mode 100644
index 00000000000..90d8ba63892
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java
@@ -0,0 +1,93 @@
+// 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.server.http.v2;
+
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.application.UriPattern;
+import com.yahoo.log.LogLevel;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.http.BadRequestException;
+import com.yahoo.vespa.config.server.http.SessionCreate;
+import com.yahoo.vespa.config.server.http.SessionHandler;
+import com.yahoo.vespa.config.server.http.Utils;
+import com.yahoo.vespa.config.server.session.LocalSession;
+import com.yahoo.vespa.config.server.session.LocalSessionRepo;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+/**
+ * A handler that is able to create a session from an application package,
+ * or create a new session from a previous session (with id or the "active" session).
+ * Handles /application/v2/ requests
+ *
+ * @author musum
+ * @since 5.1
+ */
+public class SessionCreateHandler extends SessionHandler {
+ private static final Logger log = Logger.getLogger(SessionCreateHandler.class.getName());
+ private final Tenants tenants;
+ private static final String fromPattern = "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*";
+ private final ConfigserverConfig configserverConfig;
+
+ @Inject
+ public SessionCreateHandler(Executor executor, AccessLog accessLog, Tenants tenants, ConfigserverConfig configserverConfig) {
+ super(executor, accessLog);
+ this.tenants = tenants;
+ this.configserverConfig = configserverConfig;
+ }
+
+ @Override
+ protected HttpResponse handlePOST(HttpRequest request) {
+ Slime deployLog = createDeployLog();
+ TenantName tenantName = Utils.getTenantFromSessionRequest(request);
+ log.log(LogLevel.DEBUG, "Found tenant '" + tenantName + "' in request");
+ Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName);
+ final SessionCreate sessionCreate = new SessionCreate(tenant.getSessionFactory(), tenant.getLocalSessionRepo(),
+ new SessionCreateResponseV2(tenant, deployLog, deployLog.get()));
+ TimeoutBudget timeoutBudget = SessionHandler.getTimeoutBudget(request, Duration.ofSeconds(configserverConfig.zookeeper().barrierTimeout()));
+ if (request.hasProperty("from")) {
+ LocalSession fromSession = getExistingSession(tenant, request);
+ return sessionCreate.createFromExisting(request, deployLog, fromSession, tenantName, timeoutBudget);
+ } else {
+ return sessionCreate.create(request, deployLog, tenantName, timeoutBudget);
+ }
+ }
+
+ private static LocalSession getExistingSession(Tenant tenant, HttpRequest request) {
+ ApplicationRepo applicationRepo = tenant.getApplicationRepo();
+ LocalSessionRepo localSessionRepo = tenant.getLocalSessionRepo();
+ ApplicationId applicationId = getFromProperty(request);
+ return SessionHandler.getSessionFromRequest(localSessionRepo, applicationRepo.getSessionIdForApplication(applicationId));
+ }
+
+ private static ApplicationId getFromProperty(HttpRequest request) {
+ String from = request.getProperty("from");
+ if (from == null || "".equals(from)) {
+ throw new BadRequestException("Parameter 'from' has illegal value '" + from + "'");
+ }
+ return getAndValidateFromParameter(URI.create(from));
+ }
+
+ private static ApplicationId getAndValidateFromParameter(URI from) {
+ UriPattern.Match match = new UriPattern(fromPattern).match(from);
+ if (match == null || match.groupCount() < 7) {
+ throw new BadRequestException("Parameter 'from' has illegal value '" + from + "'");
+ }
+ return new ApplicationId.Builder()
+ .tenant(match.group(2))
+ .applicationName(match.group(3))
+ .instanceName(match.group(6)).build();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateResponseV2.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateResponseV2.java
new file mode 100644
index 00000000000..5fab4b97407
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateResponseV2.java
@@ -0,0 +1,37 @@
+// 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.server.http.v2;
+
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.http.SessionCreateResponse;
+import com.yahoo.vespa.config.server.http.SessionResponse;
+
+/**
+ * Creates a response for SessionCreateHandler (v2).
+ *
+ * @author musum
+ * @since 5.1.27
+ */
+public class SessionCreateResponseV2 extends SessionResponse implements SessionCreateResponse {
+ private final Tenant tenant;
+
+ public SessionCreateResponseV2(Tenant tenant, Slime deployLog, Cursor root) {
+ super(deployLog, root);
+ this.tenant = tenant;
+ }
+
+ @Override
+ public HttpResponse createResponse(String hostName, int port, long sessionId) {
+ String tenantName = tenant.getName().value();
+ String path = "http://" + hostName + ":" + port + "/application/v2/tenant/" + tenantName + "/session/" + sessionId;
+
+ this.root.setString("tenant", tenantName);
+ this.root.setString("session-id", Long.toString(sessionId));
+ this.root.setString("prepared", path + "/prepared");
+ this.root.setString("content", path + "/content/");
+ this.root.setString("message", "Session " + sessionId + " for tenant '" + tenantName + "' created.");
+ return this;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandler.java
new file mode 100644
index 00000000000..9a0cc7e6d16
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandler.java
@@ -0,0 +1,114 @@
+// 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.server.http.v2;
+
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.log.LogLevel;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.server.ApplicationSet;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.configchange.ConfigChangeActions;
+import com.yahoo.vespa.config.server.configchange.RefeedActions;
+import com.yahoo.vespa.config.server.configchange.RefeedActionsFormatter;
+import com.yahoo.vespa.config.server.configchange.RestartActionsFormatter;
+import com.yahoo.vespa.config.server.http.SessionHandler;
+import com.yahoo.vespa.config.server.http.Utils;
+import com.yahoo.vespa.config.server.session.*;
+
+import java.util.Optional;
+import java.util.concurrent.Executor;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A handler that prepares a session given by an id in the request. v2 of application API
+ *
+ * @author musum
+ * @since 5.1.29
+ */
+public class SessionPrepareHandler extends SessionHandler {
+ private static final Logger log = Logger.getLogger(SessionPrepareHandler.class.getName());
+
+ private final Tenants tenants;
+ private final ConfigserverConfig configserverConfig;
+
+ @Inject
+ public SessionPrepareHandler(Executor executor,
+ AccessLog accessLog,
+ Tenants tenants,
+ ConfigserverConfig configserverConfig) {
+ super(executor, accessLog);
+ this.tenants = tenants;
+ this.configserverConfig = configserverConfig;
+ }
+
+ @Override
+ protected HttpResponse handlePUT(HttpRequest request) {
+ TenantName tenantName = Utils.getTenantFromSessionRequest(request);
+ Tenant tenantContext = Utils.checkThatTenantExists(tenants, tenantName);
+ LocalSession session = getSessionFromRequestV2(tenantContext.getLocalSessionRepo(), request);
+ if (Session.Status.ACTIVATE.equals(session.getStatus())) {
+ throw new IllegalArgumentException("Session is active: " + session.getSessionId());
+ }
+ log.log(LogLevel.DEBUG, "session=" + session);
+ boolean verbose = request.getBooleanProperty("verbose");
+ Slime rawDeployLog = createDeployLog();
+ PrepareParams prepParams = PrepareParams.fromHttpRequest(request, tenantName, configserverConfig);
+ // An app id currently using only the name
+ ApplicationId appId = prepParams.getApplicationId();
+ DeployLogger logger = createLogger(rawDeployLog, verbose, appId);
+ ConfigChangeActions actions = session.prepare(logger, prepParams, getCurrentActiveApplicationSet(tenantContext, appId), tenantContext.getPath());
+ logConfigChangeActions(actions, logger);
+ log.log(LogLevel.INFO, Tenants.logPre(appId)+"Session "+session.getSessionId()+" prepared successfully. ");
+ return new SessionPrepareResponse(rawDeployLog, tenantContext, request, session, actions);
+ }
+
+ private static void logConfigChangeActions(ConfigChangeActions actions, DeployLogger logger) {
+ if ( ! actions.getRestartActions().getEntries().isEmpty()) {
+ logger.log(Level.WARNING, "Change(s) between active and new application that require restart:\n" +
+ new RestartActionsFormatter(actions.getRestartActions()).format());
+ }
+ if ( ! actions.getRefeedActions().getEntries().isEmpty()) {
+ boolean allAllowed = actions.getRefeedActions().getEntries().stream().allMatch(RefeedActions.Entry::allowed);
+ logger.log(allAllowed ? Level.INFO : Level.WARNING,
+ "Change(s) between active and new application that may require re-feed:\n" +
+ new RefeedActionsFormatter(actions.getRefeedActions()).format());
+ }
+ }
+
+ @Override
+ protected HttpResponse handleGET(HttpRequest request) {
+ TenantName tenantName = Utils.getTenantFromSessionRequest(request);
+ log.log(LogLevel.DEBUG, "Found tenant '" + tenantName + "' in request");
+ Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName);
+ RemoteSession session = getSessionFromRequestV2(tenant.getRemoteSessionRepo(), request);
+ if (Session.Status.ACTIVATE.equals(session.getStatus()))
+ throw new IllegalArgumentException("Session is active: " + session.getSessionId());
+ if (!Session.Status.PREPARE.equals(session.getStatus()))
+ throw new IllegalArgumentException("Session not prepared: " + session.getSessionId());
+ return new SessionPrepareResponse(createDeployLog(), tenant, request, session, new ConfigChangeActions());
+ }
+
+ private static Optional<ApplicationSet> getCurrentActiveApplicationSet(Tenant tenant, ApplicationId appId) {
+ Optional<ApplicationSet> currentActiveApplicationSet = Optional.empty();
+ ApplicationRepo applicationRepo = tenant.getApplicationRepo();
+ try {
+ long currentActiveSessionId = applicationRepo.getSessionIdForApplication(appId);
+ final RemoteSession currentActiveSession = tenant.getRemoteSessionRepo().getSession(currentActiveSessionId);
+ if (currentActiveSession != null) {
+ currentActiveApplicationSet = Optional.ofNullable(currentActiveSession.ensureApplicationLoaded());
+ }
+ } catch (IllegalArgumentException e) {
+ // Do nothing if we have no currently active session
+ }
+ return currentActiveApplicationSet;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareResponse.java
new file mode 100644
index 00000000000..dbc36bbc948
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareResponse.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.server.http.v2;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.configchange.ConfigChangeActions;
+import com.yahoo.vespa.config.server.configchange.ConfigChangeActionsSlimeConverter;
+import com.yahoo.vespa.config.server.http.SessionResponse;
+import com.yahoo.vespa.config.server.session.Session;
+
+/**
+ * Creates a response for SessionPrepareHandler.
+ *
+ * @author musum
+ * @since 5.1.28
+ */
+class SessionPrepareResponse extends SessionResponse {
+
+ public SessionPrepareResponse(Slime deployLog, Tenant tenant, HttpRequest request, Session session, ConfigChangeActions actions) {
+ super(deployLog, deployLog.get());
+ String message = "Session " + session.getSessionId() + " for tenant '" + tenant.getName() + "' prepared.";
+ this.root.setString("tenant", tenant.getName().value());
+ this.root.setString("activate", "http://" + request.getHost() + ":" + request.getPort() + "/application/v2/tenant/" + tenant.getName() + "/session/" + session.getSessionId() + "/active");
+ root.setString("message", message);
+ new ConfigChangeActionsSlimeConverter(actions).toSlime(root);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantCreateResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantCreateResponse.java
new file mode 100644
index 00000000000..469736005b8
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantCreateResponse.java
@@ -0,0 +1,26 @@
+// 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.server.http.v2;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.config.server.http.SessionResponse;
+
+/**
+ * Response for tenant create
+ *
+ * @author vegardh
+ *
+ */
+public class TenantCreateResponse extends SessionResponse {
+
+ public TenantCreateResponse(TenantName tenant) {
+ super();
+ this.root.setString("message", "Tenant "+tenant+" created.");
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantDeleteResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantDeleteResponse.java
new file mode 100644
index 00000000000..dbadc77c9dd
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantDeleteResponse.java
@@ -0,0 +1,25 @@
+// 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.server.http.v2;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.config.server.http.SessionResponse;
+
+/**
+ * Response for tenant delete
+ *
+ * @author vegardh
+ *
+ */
+public class TenantDeleteResponse extends SessionResponse {
+
+ public TenantDeleteResponse(TenantName tenant) {
+ super();
+ this.root.setString("message", "Tenant "+tenant+" deleted.");
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantGetResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantGetResponse.java
new file mode 100644
index 00000000000..99393cd351a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantGetResponse.java
@@ -0,0 +1,25 @@
+// 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.server.http.v2;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.config.server.http.SessionResponse;
+
+/**
+ * Response for tenant create
+ *
+ * @author musum
+ */
+public class TenantGetResponse extends SessionResponse {
+
+ public TenantGetResponse(TenantName tenant) {
+ super();
+ this.root.setString("message", "Tenant '" + tenant + "' exists.");
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantHandler.java
new file mode 100644
index 00000000000..e373eb4478f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantHandler.java
@@ -0,0 +1,96 @@
+// 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.server.http.v2;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.yolean.Exceptions;
+import com.yahoo.vespa.config.server.Tenant;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.http.BadRequestException;
+import com.yahoo.vespa.config.server.http.HttpHandler;
+import com.yahoo.vespa.config.server.http.InternalServerException;
+import com.yahoo.vespa.config.server.http.Utils;
+import com.yahoo.vespa.config.server.Tenants;
+
+/**
+ * Handler to create, get and delete a tenant.
+ *
+ * @author vegardh
+ */
+public class TenantHandler extends HttpHandler {
+
+ private static final String TENANT_NAME_REGEXP = "[\\w-]+";
+ private final Tenants tenants;
+
+ public TenantHandler(Executor executor, AccessLog accessLog, Tenants tenants) {
+ super(executor, accessLog);
+ this.tenants = tenants;
+ }
+
+ @Override
+ protected HttpResponse handlePUT(HttpRequest request) {
+ TenantName tenant = getAndValidateTenantFromRequest(request);
+ try {
+ tenants.createTenant(tenant);
+ } catch (Exception e) {
+ throw new InternalServerException(Exceptions.toMessageString(e));
+ }
+ return new TenantCreateResponse(tenant);
+ }
+
+ /**
+ * Gets the tenant name from the request, throws if it exists already and validates its name
+ *
+ * @param request an {@link com.yahoo.container.jdisc.HttpRequest}
+ * @return tenant name
+ */
+ private TenantName getAndValidateTenantFromRequest(HttpRequest request) {
+ TenantName tenant = Utils.getTenantFromRequest(request);
+ Utils.checkThatTenantDoesNotExist(tenants, tenant);
+ validateTenantName(tenant);
+ return tenant;
+ }
+
+ private void validateTenantName(TenantName tenant) {
+ if (!tenant.value().matches(TENANT_NAME_REGEXP)) {
+ throw new BadRequestException("Illegal tenant name: " + tenant);
+ }
+ }
+
+ @Override
+ protected HttpResponse handleGET(HttpRequest request) {
+ TenantName tenant = getExistingTenant(request);
+ return new TenantGetResponse(tenant);
+ }
+
+ @Override
+ protected HttpResponse handleDELETE(HttpRequest request) {
+ TenantName tenantName = getExistingTenant(request);
+ Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName);
+ ApplicationRepo applicationRepo = tenant.getApplicationRepo();
+ final List<ApplicationId> activeApplications = applicationRepo.listApplications();
+ if (activeApplications.isEmpty()) {
+ try {
+ tenants.deleteTenant(tenantName);
+ } catch (Exception e) {
+ throw new InternalServerException(Exceptions.toMessageString(e));
+ }
+ } else {
+ throw new BadRequestException("Cannot delete tenant '" + tenantName + "', as it has active applications: " +
+ activeApplications);
+ }
+ return new TenantDeleteResponse(tenantName);
+ }
+
+ private TenantName getExistingTenant(HttpRequest request) {
+ TenantName tenant = Utils.getTenantFromRequest(request);
+ Utils.checkThatTenantExists(tenants, tenant);
+ return tenant;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantRequest.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantRequest.java
new file mode 100644
index 00000000000..5ba6d480871
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantRequest.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.server.http.v2;
+
+import com.yahoo.config.provision.ApplicationId;
+
+/**
+ * Config REST requests that have been bound to an application id
+ *
+ * @author vegardh
+ */
+public interface TenantRequest {
+
+ ApplicationId getApplicationId();
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/model/ElkProducer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/model/ElkProducer.java
new file mode 100644
index 00000000000..318a3f81d52
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/model/ElkProducer.java
@@ -0,0 +1,51 @@
+// 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.server.model;
+import com.yahoo.cloud.config.ElkConfig.Builder;
+
+import com.yahoo.cloud.config.ElkConfig;
+import com.yahoo.vespa.defaults.Defaults;
+
+/**
+ * Produces the ELK config for the SuperModel
+ *
+ * @author vegardh
+ * @since 5.38
+ *
+ */
+public class ElkProducer implements ElkConfig.Producer {
+
+ private final ElkConfig config;
+
+ public ElkProducer(ElkConfig config) {
+ this.config = config;
+ }
+
+ @Override
+ public void getConfig(Builder builder) {
+ for (ElkConfig.Elasticsearch es : config.elasticsearch()) {
+ int port = es.port() != 0 ? es.port() : Defaults.getDefaults().vespaWebServicePort();
+ builder.elasticsearch(new ElkConfig.Elasticsearch.Builder().host(es.host()).port(port));
+ }
+ ElkConfig.Logstash.Builder logstashBuilder = new ElkConfig.Logstash.Builder();
+ logstashBuilder.
+ config_file(Defaults.getDefaults().underVespaHome(config.logstash().config_file())).
+ source_field(config.logstash().source_field()).
+ spool_size(config.logstash().spool_size());
+ ElkConfig.Logstash.Network.Builder networkBuilder = new ElkConfig.Logstash.Network.Builder().
+ timeout(config.logstash().network().timeout());
+ for (ElkConfig.Logstash.Network.Servers srv : config.logstash().network().servers()) {
+ networkBuilder.
+ servers(new ElkConfig.Logstash.Network.Servers.Builder().
+ host(srv.host()).
+ port(srv.port()));
+ }
+ logstashBuilder.network(networkBuilder);
+ for (ElkConfig.Logstash.Files files : config.logstash().files()) {
+ logstashBuilder.files(new ElkConfig.Logstash.Files.Builder().
+ paths(files.paths()).
+ fields(files.fields()));
+ }
+ builder.logstash(logstashBuilder);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/model/LbServicesProducer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/model/LbServicesProducer.java
new file mode 100644
index 00000000000..d8f3c0da8d4
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/model/LbServicesProducer.java
@@ -0,0 +1,117 @@
+// 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.server.model;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import com.google.common.base.Joiner;
+import com.yahoo.config.model.api.HostInfo;
+import com.yahoo.config.model.api.ServiceInfo;
+import com.yahoo.cloud.config.LbServicesConfig;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+
+/**
+ * Produces lb-services cfg
+ *
+ * @author vegardh
+ * @since 5.9
+ */
+public class LbServicesProducer implements LbServicesConfig.Producer {
+
+ private final Map<TenantName, Map<ApplicationId, Application>> models;
+ private final Zone zone;
+
+ public LbServicesProducer(Map<TenantName, Map<ApplicationId, Application>> models, Zone zone) {
+ this.models = models;
+ this.zone = zone;
+ }
+
+ @Override
+ public void getConfig(LbServicesConfig.Builder builder) {
+ models.keySet().stream()
+ .sorted()
+ .forEach(tenant -> {
+ builder.tenants(tenant.value(), getTenantConfig(models.get(tenant)));
+ });
+ }
+
+ private LbServicesConfig.Tenants.Builder getTenantConfig(Map<ApplicationId, Application> apps) {
+ LbServicesConfig.Tenants.Builder tb = new LbServicesConfig.Tenants.Builder();
+ apps.keySet().stream()
+ .sorted()
+ .forEach(applicationId -> {
+ tb.applications(createLbAppIdKey(applicationId), getAppConfig(apps.get(applicationId)));
+ });
+ return tb;
+ }
+
+ private String createLbAppIdKey(ApplicationId applicationId) {
+ return applicationId.application().value() + ":" + zone.environment().value() + ":" + zone.region().value() + ":" + applicationId.instance().value();
+ }
+
+ private LbServicesConfig.Tenants.Applications.Builder getAppConfig(Application app) {
+ LbServicesConfig.Tenants.Applications.Builder ab = new LbServicesConfig.Tenants.Applications.Builder();
+ ab.activeRotation(getActiveRotation(app));
+ app.getModel().getHosts().stream()
+ .sorted((a, b) -> a.getHostname().compareTo(b.getHostname()))
+ .forEach(hostInfo -> {
+ ab.hosts(hostInfo.getHostname(), getHostsConfig(hostInfo));
+ });
+ return ab;
+ }
+
+ private boolean getActiveRotation(Application app) {
+ boolean activeRotation = false;
+ for (HostInfo hostInfo : app.getModel().getHosts()) {
+ final Optional<ServiceInfo> container = hostInfo.getServices().stream().filter(
+ serviceInfo -> serviceInfo.getServiceType().equals("container") ||
+ serviceInfo.getServiceType().equals("qrserver")).
+ findAny();
+ if (container.isPresent()) {
+ activeRotation |= Boolean.valueOf(container.get().getProperty("activeRotation").get());
+ }
+ }
+ return activeRotation;
+ }
+
+ private LbServicesConfig.Tenants.Applications.Hosts.Builder getHostsConfig(HostInfo hostInfo) {
+ LbServicesConfig.Tenants.Applications.Hosts.Builder hb = new LbServicesConfig.Tenants.Applications.Hosts.Builder();
+ hb.hostname(hostInfo.getHostname());
+ hostInfo.getServices().stream()
+ .forEach(serviceInfo -> {
+ hb.services(serviceInfo.getServiceName(), getServiceConfig(serviceInfo));
+ });
+ return hb;
+ }
+
+ private LbServicesConfig.Tenants.Applications.Hosts.Services.Builder getServiceConfig(ServiceInfo serviceInfo) {
+ final List<String> endpointAliases = Stream.of(serviceInfo.getProperty("endpointaliases").orElse("").split(",")).
+ filter(prop -> !"".equals(prop)).collect(Collectors.toList());
+ endpointAliases.addAll(Stream.of(serviceInfo.getProperty("rotations").orElse("").split(",")).filter(prop -> !"".equals(prop)).collect(Collectors.toList()));
+ Collections.sort(endpointAliases);
+
+ LbServicesConfig.Tenants.Applications.Hosts.Services.Builder sb = new LbServicesConfig.Tenants.Applications.Hosts.Services.Builder()
+ .type(serviceInfo.getServiceType())
+ .clustertype(serviceInfo.getProperty("clustertype").orElse(""))
+ .clustername(serviceInfo.getProperty("clustername").orElse(""))
+ .configId(serviceInfo.getConfigId())
+ .servicealiases(Stream.of(serviceInfo.getProperty("servicealiases").orElse("").split(",")).
+ filter(prop -> !"".equals(prop)).sorted((a, b) -> a.compareTo(b)).collect(Collectors.toList()))
+ .endpointaliases(endpointAliases)
+ .index(Integer.parseInt(serviceInfo.getProperty("index").orElse("999999")));
+ serviceInfo.getPorts().stream()
+ .forEach(portInfo -> {
+ LbServicesConfig.Tenants.Applications.Hosts.Services.Ports.Builder pb = new LbServicesConfig.Tenants.Applications.Hosts.Services.Ports.Builder()
+ .number(portInfo.getPort())
+ .tags(Joiner.on(" ").join(portInfo.getTags()));
+ sb.ports(pb);
+ });
+ return sb;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/model/RoutingProducer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/model/RoutingProducer.java
new file mode 100755
index 00000000000..6a63269ae6e
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/model/RoutingProducer.java
@@ -0,0 +1,36 @@
+// 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.server.model;
+
+import com.yahoo.cloud.config.RoutingConfig;
+import com.yahoo.config.model.api.HostInfo;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.application.Application;
+
+import java.util.Map;
+
+/**
+ * Create global config based on info from the zone application
+ *
+ * @author can
+ * @since 5.60
+ */
+public class RoutingProducer implements RoutingConfig.Producer {
+
+ private final Map<TenantName, Map<ApplicationId, Application>> models;
+
+ public RoutingProducer(Map<TenantName, Map<ApplicationId, Application>> models) {
+ this.models = models;
+ }
+
+ @Override
+ public void getConfig(RoutingConfig.Builder builder) {
+ for (Map<ApplicationId, Application> model : models.values()) {
+ model.values().stream().filter(application -> application.getId().isHostedVespaRoutingApplication()).forEach(application -> {
+ for (HostInfo host : application.getModel().getHosts()) {
+ builder.hosts(host.getHostname());
+ }
+ });
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/model/SuperModel.java b/configserver/src/main/java/com/yahoo/vespa/config/server/model/SuperModel.java
new file mode 100755
index 00000000000..2be7860b01f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/model/SuperModel.java
@@ -0,0 +1,93 @@
+// 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.server.model;
+
+import com.yahoo.cloud.config.ElkConfig;
+import com.yahoo.cloud.config.LbServicesConfig;
+import com.yahoo.cloud.config.RoutingConfig;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.server.application.Application;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * A config model that spans across all applications of all tenants in the config server.
+ *
+ * @author vegardh
+ * @since 5.9
+ *
+ */
+public class SuperModel implements LbServicesConfig.Producer, ElkConfig.Producer, RoutingConfig.Producer {
+
+ private final Map<TenantName, Map<ApplicationId, Application>> models;
+ private final LbServicesProducer lbProd;
+ private final ElkProducer elkProd;
+ private final RoutingProducer zoneProd;
+
+ public SuperModel(Map<TenantName, Map<ApplicationId, Application>> newModels, ElkConfig elkConfig, Zone zone) {
+ this.models = newModels;
+ this.lbProd = new LbServicesProducer(Collections.unmodifiableMap(models), zone);
+ this.elkProd = new ElkProducer(elkConfig);
+ this.zoneProd = new RoutingProducer(Collections.unmodifiableMap(models));
+ }
+
+ public ConfigPayload getConfig(ConfigKey<?> configKey) throws IOException {
+ // TODO: Override not applied, but not really necessary here
+ if (configKey.equals(new ConfigKey<>(LbServicesConfig.class, configKey.getConfigId()))) {
+ LbServicesConfig.Builder builder = new LbServicesConfig.Builder();
+ getConfig(builder);
+ return ConfigPayload.fromInstance(new LbServicesConfig(builder));
+ } else if (configKey.equals(new ConfigKey<>(ElkConfig.class, configKey.getConfigId()))) {
+ ElkConfig.Builder builder = new ElkConfig.Builder();
+ getConfig(builder);
+ return ConfigPayload.fromInstance(new ElkConfig(builder));
+ } else if (configKey.equals(new ConfigKey<>(RoutingConfig.class, configKey.getConfigId()))) {
+ RoutingConfig.Builder builder = new RoutingConfig.Builder();
+ getConfig(builder);
+ return ConfigPayload.fromInstance(new RoutingConfig(builder));
+ } else {
+ return null;
+ }
+ }
+
+ public Map<TenantName, Map<ApplicationId, Application>> getCurrentModels() {
+ return models;
+ }
+
+ @Override
+ public void getConfig(LbServicesConfig.Builder builder) {
+ lbProd.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(com.yahoo.cloud.config.ElkConfig.Builder builder) {
+ elkProd.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(RoutingConfig.Builder builder) {
+ zoneProd.getConfig(builder);
+ }
+
+ public <CONFIGTYPE extends ConfigInstance> CONFIGTYPE getConfig(Class<CONFIGTYPE> configClass, ApplicationId applicationId, String configId) throws IOException {
+ TenantName tenant = applicationId.tenant();
+ if (!models.containsKey(tenant)) {
+ throw new IllegalArgumentException("Tenant " + tenant + " not found");
+ }
+ Map<ApplicationId, Application> applications = models.get(tenant);
+ if (!applications.containsKey(applicationId)) {
+ throw new IllegalArgumentException("Application id " + applicationId + " not found");
+ }
+ Application application = applications.get(applicationId);
+ ConfigKey<CONFIGTYPE> key = new ConfigKey<>(configClass, configId);
+ ConfigPayload payload = application.getModel().getConfig(key, (ConfigDefinition)null, null);
+ return payload.toInstance(configClass, configId);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java
new file mode 100644
index 00000000000..c3046887b0e
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java
@@ -0,0 +1,126 @@
+// 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.server.modelfactory;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.model.api.Model;
+import com.yahoo.config.model.api.ModelContext;
+import com.yahoo.config.model.api.ModelFactory;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Version;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.server.GlobalComponentRegistry;
+import com.yahoo.vespa.config.server.RotationsCache;
+import com.yahoo.vespa.config.server.ServerCache;
+import com.yahoo.vespa.config.server.Tenants;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.vespa.config.server.application.PermanentApplicationPackage;
+import com.yahoo.vespa.config.server.deploy.ModelContextImpl;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.server.monitoring.Metrics;
+import com.yahoo.vespa.config.server.provision.StaticProvisioner;
+import com.yahoo.vespa.config.server.session.SessionZooKeeperClient;
+import com.yahoo.vespa.config.server.session.SilentDeployLogger;
+import com.yahoo.vespa.curator.Curator;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.logging.Logger;
+
+/**
+ * Builds activated versions of the right model versions
+ *
+ * @author bratseth
+ */
+public class ActivatedModelsBuilder extends ModelsBuilder<Application> {
+
+ private static final Logger log = Logger.getLogger(ActivatedModelsBuilder.class.getName());
+
+ private final TenantName tenant;
+ private final long appGeneration;
+ private final SessionZooKeeperClient zkClient;
+ private final Optional<PermanentApplicationPackage> permanentApplicationPackage;
+ private final Optional<com.yahoo.config.provision.Provisioner> hostProvisioner;
+ private final ConfigserverConfig configserverConfig;
+ private final ConfigDefinitionRepo configDefinitionRepo;
+ private final Metrics metrics;
+ private final Curator curator;
+ private final Zone zone;
+ private final DeployLogger logger;
+
+ public ActivatedModelsBuilder(TenantName tenant, long appGeneration, SessionZooKeeperClient zkClient, GlobalComponentRegistry globalComponentRegistry) {
+ super(globalComponentRegistry.getModelFactoryRegistry());
+ this.tenant = tenant;
+ this.appGeneration = appGeneration;
+ this.zkClient = zkClient;
+ this.permanentApplicationPackage = Optional.of(globalComponentRegistry.getPermanentApplicationPackage());
+ this.configserverConfig = globalComponentRegistry.getConfigserverConfig();
+ this.configDefinitionRepo = globalComponentRegistry.getConfigDefinitionRepo();
+ this.metrics = globalComponentRegistry.getMetrics();
+ this.hostProvisioner = globalComponentRegistry.getHostProvisioner();
+ this.curator = globalComponentRegistry.getCurator();
+ this.zone = globalComponentRegistry.getZone();
+ this.logger = new SilentDeployLogger();
+ }
+
+ @Override
+ protected Application buildModelVersion(ModelFactory modelFactory, ApplicationPackage applicationPackage,
+ ApplicationId applicationId) {
+ Version version = modelFactory.getVersion();
+ log.log(LogLevel.DEBUG, String.format("Loading model version %s for session %s application %s",
+ version, appGeneration, applicationId));
+ ModelContext modelContext = new ModelContextImpl(
+ applicationPackage,
+ Optional.<Model>empty(),
+ permanentApplicationPackage.get().applicationPackage(),
+ logger,
+ configDefinitionRepo,
+ getForVersionOrLatest(applicationPackage.getFileRegistryMap(), modelFactory.getVersion()).orElse(new MockFileRegistry()),
+ createHostProvisioner(getForVersionOrLatest(applicationPackage.getProvisionInfoMap(), modelFactory.getVersion())),
+ createModelContextProperties(applicationId),
+ Optional.empty(),
+ Optional.empty());
+ ServerCache cache = zkClient.loadServerCache();
+ MetricUpdater applicationMetricUpdater = metrics.getOrCreateMetricUpdater(Metrics.createDimensions(applicationId));
+ return new Application(modelFactory.createModel(modelContext), cache, appGeneration, version,
+ applicationMetricUpdater, applicationId);
+ }
+
+ private Optional<HostProvisioner> createHostProvisioner(Optional<ProvisionInfo> provisionInfo) {
+ if (hostProvisioner.isPresent() && provisionInfo.isPresent()) {
+ return Optional.of(createStaticProvisioner(provisionInfo.get()));
+ }
+ return Optional.empty();
+ }
+
+ private HostProvisioner createStaticProvisioner(ProvisionInfo provisionInfo) {
+ return new StaticProvisioner(provisionInfo);
+ }
+
+ private static <T> Optional<T> getForVersionOrLatest(Map<Version, T> map, Version version) {
+ if (map.isEmpty()) {
+ return Optional.empty();
+ }
+ T value = map.get(version);
+ if (value == null) {
+ value = map.get(map.keySet().stream().max((a, b) -> a.compareTo(b)).get());
+ }
+ return Optional.of(value);
+ }
+
+ private ModelContext.Properties createModelContextProperties(ApplicationId applicationId) {
+ return createModelContextProperties(
+ applicationId,
+ configserverConfig,
+ zone,
+ new RotationsCache(curator, Tenants.getTenantPath(tenant)).readRotationsFromZooKeeper(applicationId));
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelFactoryRegistry.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelFactoryRegistry.java
new file mode 100644
index 00000000000..a18a200f39e
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelFactoryRegistry.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.server.modelfactory;
+
+import com.google.inject.Inject;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.config.model.api.ModelFactory;
+import com.yahoo.config.provision.Version;
+import com.yahoo.vespa.config.server.http.UnknownVespaVersionException;
+
+import java.util.*;
+
+/**
+ * A registry of model factories. Allows querying for a specific version of a {@link ModelFactory} or
+ * simply returning all of them. Keeps track of the latest {@link com.yahoo.config.provision.Version} supported.
+ *
+ * @author lulf
+ */
+public class ModelFactoryRegistry {
+
+ private final Map<Version, ModelFactory> factories = new HashMap<>();
+
+ @Inject
+ public ModelFactoryRegistry(ComponentRegistry<ModelFactory> factories) {
+ this(factories.allComponents());
+ }
+
+ public ModelFactoryRegistry(List<ModelFactory> modelFactories) {
+ if (modelFactories.isEmpty()) {
+ throw new IllegalArgumentException("No ModelFactory instances registered, cannot build config models");
+ }
+ for (ModelFactory factory : modelFactories) {
+ factories.put(factory.getVersion(), factory);
+ }
+ }
+
+ public Set<Version> allVersions() { return factories.keySet(); }
+
+ /**
+ * Returns the factory for the given version
+ *
+ * @throws UnknownVespaVersionException if there is no factory for this version
+ */
+ public ModelFactory getFactory(Version version) {
+ if ( ! factories.containsKey(version))
+ throw new UnknownVespaVersionException("Unknown Vespa version '" + version + "', cannot build config model for this version");
+ return factories.get(version);
+ }
+
+ /**
+ * Return all factories that can build a model.
+ *
+ * @return An immutable collection of {@link com.yahoo.config.model.api.ModelFactory} instances.
+ */
+ public Collection<ModelFactory> getFactories() {
+ return Collections.unmodifiableCollection(factories.values());
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelResult.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelResult.java
new file mode 100644
index 00000000000..9c9ec160a95
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelResult.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.server.modelfactory;
+
+import com.yahoo.config.model.api.Model;
+
+/**
+ * @author bratseth
+ */
+public interface ModelResult {
+
+ Model getModel();
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelsBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelsBuilder.java
new file mode 100644
index 00000000000..99036ee0027
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelsBuilder.java
@@ -0,0 +1,132 @@
+// 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.server.modelfactory;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.api.ModelContext;
+import com.yahoo.config.model.api.ModelFactory;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Rotation;
+import com.yahoo.config.provision.Version;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.config.server.ConfigServerSpec;
+import com.yahoo.vespa.config.server.deploy.ModelContextImpl;
+import com.yahoo.vespa.config.server.http.UnknownVespaVersionException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+/**
+ * Responsible for building the right versions of application models for a given tenant and application generation.
+ * Actual model building is implemented by subclasses because it differs in the prepare and activate phases.
+ *
+ * @author bratseth
+ */
+public abstract class ModelsBuilder<MODELRESULT extends ModelResult> {
+
+ private static final Logger log = Logger.getLogger(ModelsBuilder.class.getName());
+
+ private final ModelFactoryRegistry modelFactoryRegistry;
+
+ protected ModelsBuilder(ModelFactoryRegistry modelFactoryRegistry) {
+ this.modelFactoryRegistry = modelFactoryRegistry;
+ }
+
+ public List<MODELRESULT> buildModels(ApplicationId applicationId, ApplicationPackage applicationPackage) {
+ Set<Version> versions = modelFactoryRegistry.allVersions();
+
+ // If the application specifies a major, load models only for that
+ Optional<Integer> requestedMajorVersion = applicationPackage.getMajorVersion();
+ if (requestedMajorVersion.isPresent())
+ versions = filterByMajorVersion(requestedMajorVersion.get(), versions);
+
+ // Load models by one major version at the time as new major versions are allowed to be unloadable
+ // in the case where an existing application is incompatible with a new major version
+ // (which is possible by the definition of major)
+ List<Integer> majorVersions = versions.stream()
+ .map(Version::getMajor)
+ .distinct()
+ .sorted(Comparator.reverseOrder())
+ .collect(Collectors.toList());
+ List<MODELRESULT> allApplicationModels = new ArrayList<>();
+ for (int i = 0; i < majorVersions.size(); i++) {
+ try {
+ allApplicationModels.addAll(buildModelVersion(filterByMajorVersion(majorVersions.get(i), versions),
+ applicationId, applicationPackage));
+
+ // skip old config models after we have found a major version which works
+ if (allApplicationModels.size() > 0 && allApplicationModels.get(0).getModel().skipOldConfigModels())
+ break;
+ }
+ catch (RuntimeException e) { // TODO: Make this a specialized exception
+ boolean isOldestMajor = i == majorVersions.size() - 1;
+ if (isOldestMajor) {
+ if (e instanceof NoSuchElementException && "No value present".equals(e.getMessage())) {
+ e.printStackTrace();
+ }
+ throw new IllegalArgumentException(applicationId + ": Error loading model", e);
+ } else {
+ log.log(Level.INFO, applicationId + ": Skipping major version " + majorVersions.get(i), e);
+ }
+ }
+ }
+ return allApplicationModels;
+ }
+
+ private List<MODELRESULT> buildModelVersion(Set<Version> versions, ApplicationId applicationId,
+ ApplicationPackage applicationPackage) {
+ Version latest = findLatest(versions);
+ // load latest application version
+ MODELRESULT latestApplicationVersion = buildModelVersion(modelFactoryRegistry.getFactory(latest), applicationPackage, applicationId);
+ if (latestApplicationVersion.getModel().skipOldConfigModels()) {
+ return Collections.singletonList(latestApplicationVersion);
+ }
+ else { // load old model versions
+ List<MODELRESULT> allApplicationVersions = new ArrayList<>();
+ allApplicationVersions.add(latestApplicationVersion);
+ for (Version version : versions) {
+ if (version.equals(latest)) continue; // already loaded
+ allApplicationVersions.add(buildModelVersion(modelFactoryRegistry.getFactory(version), applicationPackage, applicationId));
+ }
+ return allApplicationVersions;
+ }
+ }
+
+ private Set<Version> filterByMajorVersion(int majorVersion, Set<Version> versions) {
+ Set<Version> filteredVersions = versions.stream().filter(v -> v.getMajor() == majorVersion).collect(Collectors.toSet());
+ if (filteredVersions.isEmpty())
+ throw new UnknownVespaVersionException("No Vespa versions matching major version " + majorVersion + " are present");
+ return filteredVersions;
+ }
+
+ private Version findLatest(Set<Version> versionSet) {
+ List<Version> versionList = new ArrayList<>(versionSet);
+ Collections.sort(versionList);
+ return versionList.get(versionList.size() - 1);
+ }
+
+ protected abstract MODELRESULT buildModelVersion(ModelFactory modelFactory, ApplicationPackage applicationPackage,
+ ApplicationId applicationId);
+
+ protected ModelContext.Properties createModelContextProperties(ApplicationId applicationId,
+ ConfigserverConfig configserverConfig,
+ Zone zone,
+ Set<Rotation> rotations) {
+ return new ModelContextImpl.Properties(
+ applicationId,
+ configserverConfig.multitenant(),
+ ConfigServerSpec.fromConfig(configserverConfig),
+ configserverConfig.hostedVespa(),
+ zone,
+ rotations);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/PreparedModelsBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/PreparedModelsBuilder.java
new file mode 100644
index 00000000000..0d6170909a7
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/PreparedModelsBuilder.java
@@ -0,0 +1,224 @@
+// 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.server.modelfactory;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.model.api.Model;
+import com.yahoo.config.model.api.ModelContext;
+import com.yahoo.config.model.api.ModelCreateResult;
+import com.yahoo.config.model.api.ModelFactory;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Rotation;
+import com.yahoo.config.provision.Version;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.ApplicationSet;
+import com.yahoo.vespa.config.server.ConfigServerSpec;
+import com.yahoo.vespa.config.server.GlobalComponentRegistry;
+import com.yahoo.vespa.config.server.HostValidator;
+import com.yahoo.vespa.config.server.RotationsCache;
+import com.yahoo.vespa.config.server.application.PermanentApplicationPackage;
+import com.yahoo.vespa.config.server.deploy.ModelContextImpl;
+import com.yahoo.vespa.config.server.filedistribution.FileDistributionProvider;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.provision.ProvisionerAdapter;
+import com.yahoo.vespa.config.server.session.FileDistributionFactory;
+import com.yahoo.vespa.config.server.session.PrepareParams;
+import com.yahoo.vespa.config.server.session.SessionContext;
+import com.yahoo.vespa.curator.Curator;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+/**
+ * @author bratseth
+ */
+public class PreparedModelsBuilder extends ModelsBuilder<PreparedModelsBuilder.PreparedModelResult> {
+
+ private static final Logger log = Logger.getLogger(PreparedModelsBuilder.class.getName());
+
+ private final PermanentApplicationPackage permanentApplicationPackage;
+ private final ConfigserverConfig configserverConfig;
+ private final ConfigDefinitionRepo configDefinitionRepo;
+ private final Curator curator;
+ private final Zone zone;
+ private final SessionContext context;
+ private final DeployLogger logger;
+ private final PrepareParams params;
+ private final FileDistributionFactory fileDistributionFactory;
+ private final HostProvisionerProvider hostProvisionerProvider;
+ private final Optional<ApplicationSet> currentActiveApplicationSet;
+ private final ApplicationId applicationId;
+ private final RotationsCache rotationsCache;
+ private final Set<Rotation> rotations;
+ private final ModelContext.Properties properties;
+
+ /** Construct from global component registry */
+ public PreparedModelsBuilder(GlobalComponentRegistry globalComponentRegistry,
+ FileDistributionFactory fileDistributionFactory,
+ HostProvisionerProvider hostProvisionerProvider,
+ SessionContext context,
+ DeployLogger logger,
+ PrepareParams params,
+ Optional<ApplicationSet> currentActiveApplicationSet,
+ Path tenantPath) {
+ super(globalComponentRegistry.getModelFactoryRegistry());
+ this.permanentApplicationPackage = globalComponentRegistry.getPermanentApplicationPackage();
+ this.configserverConfig = globalComponentRegistry.getConfigserverConfig();
+ this.configDefinitionRepo = globalComponentRegistry.getConfigDefinitionRepo();
+ this.curator = globalComponentRegistry.getCurator();
+ this.zone = globalComponentRegistry.getZone();
+
+ this.fileDistributionFactory = fileDistributionFactory;
+ this.hostProvisionerProvider = hostProvisionerProvider;
+
+ this.context = context;
+ this.logger = logger;
+ this.params = params;
+ this.currentActiveApplicationSet = currentActiveApplicationSet;
+
+ this.applicationId = params.getApplicationId();
+ this.rotationsCache = new RotationsCache(curator, tenantPath);
+ this.rotations = getRotations(params.rotations());
+ this.properties = createModelContextProperties(
+ params.getApplicationId(),
+ configserverConfig,
+ zone,
+ rotations);
+ }
+
+ /** Construct with all dependencies passed separately */
+ public PreparedModelsBuilder(ModelFactoryRegistry modelFactoryRegistry,
+ PermanentApplicationPackage permanentApplicationPackage,
+ ConfigserverConfig configserverConfig,
+ ConfigDefinitionRepo configDefinitionRepo,
+ Curator curator,
+ Zone zone,
+ FileDistributionFactory fileDistributionFactory,
+ HostProvisionerProvider hostProvisionerProvider,
+ SessionContext context,
+ DeployLogger logger,
+ PrepareParams params,
+ Optional<ApplicationSet> currentActiveApplicationSet,
+ Path tenantPath) {
+ super(modelFactoryRegistry);
+ this.permanentApplicationPackage = permanentApplicationPackage;
+ this.configserverConfig = configserverConfig;
+ this.configDefinitionRepo = configDefinitionRepo;
+ this.curator = curator;
+ this.zone = zone;
+
+ this.fileDistributionFactory = fileDistributionFactory;
+ this.hostProvisionerProvider = hostProvisionerProvider;
+
+ this.context = context;
+ this.logger = logger;
+ this.params = params;
+ this.currentActiveApplicationSet = currentActiveApplicationSet;
+
+ this.applicationId = params.getApplicationId();
+ this.rotationsCache = new RotationsCache(curator, tenantPath);
+ this.rotations = getRotations(params.rotations());
+ this.properties = new ModelContextImpl.Properties(
+ params.getApplicationId(),
+ configserverConfig.multitenant(),
+ ConfigServerSpec.fromConfig(configserverConfig),
+ configserverConfig.hostedVespa(),
+ zone,
+ rotations);
+ }
+
+ @Override
+ protected PreparedModelResult buildModelVersion(ModelFactory modelFactory, ApplicationPackage applicationPackage,
+ ApplicationId applicationId) {
+ Version version = modelFactory.getVersion();
+ log.log(LogLevel.DEBUG, "Start building model for Vespa version " + version);
+ FileDistributionProvider fileDistributionProvider = fileDistributionFactory.createProvider(
+ context.getServerDBSessionDir(),
+ applicationId);
+
+ Optional<HostProvisioner> hostProvisioner = createHostProvisionerAdapter(properties);
+ Optional<Model> previousModel = currentActiveApplicationSet
+ .map(set -> set.getForVersionOrLatest(Optional.of(version)).getModel());
+ ModelContext modelContext = new ModelContextImpl(
+ applicationPackage,
+ previousModel,
+ permanentApplicationPackage.applicationPackage(),
+ logger,
+ configDefinitionRepo,
+ fileDistributionProvider.getFileRegistry(),
+ hostProvisioner,
+ properties,
+ getAppDir(applicationPackage),
+ Optional.of(version));
+
+ log.log(LogLevel.DEBUG, "Running createAndValidateModel for Vespa version " + version);
+ ModelCreateResult result = modelFactory.createAndValidateModel(modelContext, params.ignoreValidationErrors());
+ validateModelHosts(context.getHostValidator(), applicationId, result.getModel());
+ log.log(LogLevel.DEBUG, "Done building model for Vespa version " + version);
+ return new PreparedModelsBuilder.PreparedModelResult(version, result.getModel(), fileDistributionProvider, result.getConfigChangeActions());
+ }
+
+ private Optional<File> getAppDir(ApplicationPackage applicationPackage) {
+ try {
+ return applicationPackage instanceof FilesApplicationPackage ?
+ Optional.of(((FilesApplicationPackage) applicationPackage).getAppDir()) :
+ Optional.empty();
+ } catch (IOException e) {
+ throw new RuntimeException("Could not find app dir", e);
+ }
+ }
+
+ private void validateModelHosts(HostValidator<ApplicationId> hostValidator, ApplicationId applicationId, Model model) {
+ hostValidator.verifyHosts(applicationId, model.getHosts().stream().map(hostInfo -> hostInfo.getHostname())
+ .collect(Collectors.toList()));
+ }
+
+ private Set<Rotation> getRotations(Set<Rotation> rotations) {
+ if (rotations == null || rotations.isEmpty()) {
+ rotations = rotationsCache.readRotationsFromZooKeeper(applicationId);
+ }
+ return rotations;
+ }
+
+ private Optional<HostProvisioner> createHostProvisionerAdapter(ModelContext.Properties properties) {
+ return hostProvisionerProvider.getHostProvisioner().map(
+ provisioner -> new ProvisionerAdapter(provisioner, properties.applicationId()));
+ }
+
+
+ /** The result of preparing a single model version */
+ public static class PreparedModelResult implements ModelResult {
+
+ public final Version version;
+ public final Model model;
+ public final FileDistributionProvider fileDistributionProvider;
+ public final List<ConfigChangeAction> actions;
+
+ public PreparedModelResult(Version version, Model model,
+ FileDistributionProvider fileDistributionProvider, List<ConfigChangeAction> actions) {
+ this.version = version;
+ this.model = model;
+ this.fileDistributionProvider = fileDistributionProvider;
+ this.actions = actions;
+ }
+
+ @Override
+ public Model getModel() {
+ return model;
+ }
+
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdater.java b/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdater.java
new file mode 100644
index 00000000000..8eae14b9aac
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdater.java
@@ -0,0 +1,222 @@
+// 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.server.monitoring;
+
+import com.yahoo.jdisc.Metric;
+import com.yahoo.vespa.config.server.ServerCache;
+import com.yahoo.vespa.config.server.RequestHandler;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static com.yahoo.vespa.config.server.monitoring.Metrics.getMetricName;
+/**
+ * @author musum
+ */
+// TODO javadoc, thread non-safeness maybe
+public class MetricUpdater {
+ private static final String METRIC_UNKNOWN_HOSTS = getMetricName("unknownHostRequests");
+ private static final String METRIC_SESSION_CHANGE_ERRORS = getMetricName("sessionChangeErrors");
+ private static final String METRIC_NEW_SESSIONS = getMetricName("newSessions");
+ private static final String METRIC_PREPARED_SESSIONS = getMetricName("preparedSessions");
+ private static final String METRIC_ACTIVATED_SESSIONS = getMetricName("activeSessions");
+ private static final String METRIC_DEACTIVATED_SESSIONS = getMetricName("inactiveSessions");
+ private static final String METRIC_ADDED_SESSIONS = getMetricName("addedSessions");
+ private static final String METRIC_REMOVED_SESSIONS = getMetricName("removedSessions");
+ private static final String METRIC_ZK_CONNECTION_LOST = getMetricName("zkConnectionLost");
+ private static final String METRIC_ZK_RECONNECTED = getMetricName("zkReconnected");
+ private static final String METRIC_ZK_CONNECTED = getMetricName("zkConnected");
+ private static final String METRIC_ZK_SUSPENDED = getMetricName("zkSuspended");
+ private static final String METRIC_TENANTS = getMetricName("tenants");
+ private static final String METRIC_HOSTS = getMetricName("hosts");
+ private static final String METRIC_APPLICATIONS = getMetricName("applications");
+ private static final String METRIC_CACHE_CONFIG_ELEMENTS = getMetricName("cacheConfigElems");
+ private static final String METRIC_CACHE_CONFIG_CHECKSUMS = getMetricName("cacheChecksumElems");
+ private static final String METRIC_DELAYED_RESPONSES = getMetricName("delayedResponses");
+ private static final String METRIC_RPCSERVER_WORK_QUEUE_SIZE = getMetricName("rpcServerWorkQueueSize");
+
+
+ private final Metrics metrics;
+ private final Map<String, String> dimensions;
+ private final Metric.Context metricContext;
+ private final Map<String, Number> staticMetrics = new ConcurrentHashMap<>();
+
+ public MetricUpdater(Metrics metrics, Map<String, String> dimensions) {
+ this.metrics = metrics;
+ this.dimensions = dimensions;
+ metricContext = createContext(metrics, dimensions);
+ }
+
+ public void incrementRequests() {
+ metrics.incrementRequests(metricContext);
+ }
+
+ public void incrementFailedRequests() {
+ metrics.incrementFailedRequests(metricContext);
+ }
+
+ public void incrementProcTime(long increment) {
+ metrics.incrementProcTime(increment, metricContext);
+ }
+
+ /**
+ * Sets the count for number of config elements in the {@link ServerCache}
+ *
+ * @param elems number of elements
+ */
+ public void setCacheConfigElems(long elems) {
+ staticMetrics.put(METRIC_CACHE_CONFIG_ELEMENTS, elems);
+ }
+
+ /**
+ * Sets the count for number of checksum elements in the {@link ServerCache}
+ *
+ * @param elems number of elements
+ */
+ public void setCacheChecksumElems(long elems) {
+ staticMetrics.put(METRIC_CACHE_CONFIG_CHECKSUMS, elems);
+ }
+
+ /**
+ * Sets the number of outstanding responses (unchanged config in long poll)
+ *
+ * @param elems number of elements
+ */
+ public void setDelayedResponses(long elems) {
+ staticMetrics.put(METRIC_DELAYED_RESPONSES, elems);
+ }
+
+ private void setStaticMetric(String name, int size) {
+ staticMetrics.put(name, size);
+ }
+
+ /**
+ * Increment the number of requests where we were unable to map host to a {@link RequestHandler}.
+ */
+ public void incUnknownHostRequests() {
+ metrics.increment(METRIC_UNKNOWN_HOSTS, metricContext);
+ }
+
+ private Metric.Context createContext(Metrics metrics, Map<String, String> dimensions) {
+ if (metrics == null) return null;
+
+ return metrics.getMetric().createContext(dimensions);
+ }
+
+ public Map<String, Number> getStaticMetrics() {
+ return staticMetrics;
+ }
+
+ public Metric.Context getMetricContext() {
+ return metricContext;
+ }
+
+ public Map<String, String> getDimensions() {
+ return dimensions;
+ }
+
+ /**
+ * Increment the number of errors from changed sessions.
+ */
+ public void incSessionChangeErrors() {
+ metrics.increment(METRIC_SESSION_CHANGE_ERRORS, metricContext);
+ }
+
+ /**
+ * Set the number of new sessions.
+ */
+ public void setNewSessions(int numNew) {
+ setStaticMetric(METRIC_NEW_SESSIONS, numNew);
+ }
+
+ /**
+ * Set the number of prepared sessions.
+ */
+ public void setPreparedSessions(int numPrepared) {
+ setStaticMetric(METRIC_PREPARED_SESSIONS, numPrepared);
+ }
+
+ /**
+ * Set the number of activated sessions.
+ */
+ public void setActivatedSessions(int numActivated) {
+ setStaticMetric(METRIC_ACTIVATED_SESSIONS, numActivated);
+ }
+
+ /**
+ * Set the number of deactivated sessions.
+ */
+ public void setDeactivatedSessions(int numDeactivated) {
+ setStaticMetric(METRIC_DEACTIVATED_SESSIONS, numDeactivated);
+ }
+
+ /**
+ * Increment the number of removed sessions.
+ */
+ public void incRemovedSessions() {
+ metrics.increment(METRIC_REMOVED_SESSIONS, metricContext);
+ }
+
+ /**
+ * Increment the number of added sessions.
+ */
+ public void incAddedSessions() {
+ metrics.increment(METRIC_ADDED_SESSIONS, metricContext);
+ }
+
+ public static MetricUpdater createTestUpdater() {
+ return new MetricUpdater(Metrics.createTestMetrics(), null);
+ }
+
+ /**
+ * Increment the number of ZK connection losses.
+ */
+ public void incZKConnectionLost() {
+ metrics.increment(METRIC_ZK_CONNECTION_LOST, metricContext);
+ }
+
+ /**
+ * Increment the number of ZK connection establishments.
+ */
+ public void incZKConnected() {
+ metrics.increment(METRIC_ZK_CONNECTED, metricContext);
+ }
+
+ /**
+ * Increment the number of ZK connection suspended.
+ */
+ public void incZKSuspended() {
+ metrics.increment(METRIC_ZK_SUSPENDED, metricContext);
+ }
+
+ /**
+ * Increment the number of ZK reconnections.
+ */
+ public void incZKReconnected() {
+ metrics.increment(METRIC_ZK_RECONNECTED, metricContext);
+ }
+
+ /**
+ * Set the number of tenants.
+ */
+ public void setTenants(int numTenants) {
+ setStaticMetric(METRIC_TENANTS, numTenants);
+ }
+
+ /**
+ * Set the number of hosts.
+ */
+ public void setHosts(int numHosts) {
+ setStaticMetric(METRIC_HOSTS, numHosts);
+ }
+
+ /**
+ * Set the number of applications.
+ */
+ public void setApplications(int numApplications) {
+ setStaticMetric(METRIC_APPLICATIONS, numApplications);
+ }
+
+ public void setRpcServerQueueSize(int numQueued) {
+ metrics.set(METRIC_RPCSERVER_WORK_QUEUE_SIZE, numQueued, metricContext);
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdaterFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdaterFactory.java
new file mode 100644
index 00000000000..7f40414c147
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdaterFactory.java
@@ -0,0 +1,14 @@
+// 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.server.monitoring;
+
+import java.util.Map;
+
+/**
+ * A factory for creating metric updates with a given context.
+ *
+ * @author lulf
+ * @since 5.15
+ */
+public interface MetricUpdaterFactory {
+ MetricUpdater getOrCreateMetricUpdater(Map<String, String> dimensions);
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/Metrics.java b/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/Metrics.java
new file mode 100644
index 00000000000..d0984baefc2
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/Metrics.java
@@ -0,0 +1,140 @@
+// 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.server.monitoring;
+
+import com.google.inject.Inject;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.config.HealthMonitorConfig;
+import com.yahoo.docproc.jdisc.metric.NullMetric;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.log.LogLevel;
+import com.yahoo.statistics.Statistics;
+import com.yahoo.statistics.Counter;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Statistics for server. The statistics framework takes care of logging.
+ *
+ * @author Harald Musum
+ * @since 4.2
+ */
+public class Metrics extends TimerTask implements MetricUpdaterFactory {
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(Metrics.class.getName());
+ private static final String METRIC_REQUESTS = getMetricName("requests");
+ private static final String METRIC_FAILED_REQUESTS = getMetricName("failedRequests");
+ private static final String METRIC_FREE_MEMORY = getMetricName("freeMemory");
+ private static final String METRIC_LATENCY = getMetricName("latency");
+
+ private final Counter requests;
+ private final Counter failedRequests;
+ private final Counter procTimeCounter;
+ private final Metric metric;
+
+ // TODO The map is the key for now
+ private final Map<Map<String, String>, MetricUpdater> metricUpdaters = new ConcurrentHashMap<>();
+ private final Timer timer = new Timer();
+
+ @Inject
+ public Metrics(Metric metric, Statistics statistics, HealthMonitorConfig healthMonitorConfig) {
+ this.metric = metric;
+ requests = createCounter(METRIC_REQUESTS, statistics);
+ failedRequests = createCounter(METRIC_FAILED_REQUESTS, statistics);
+ procTimeCounter = createCounter("procTime", statistics);
+ timer.scheduleAtFixedRate(this, 5000, (long) (healthMonitorConfig.snapshot_interval() * 1000));
+ log.log(LogLevel.DEBUG, "Metric update interval is " + healthMonitorConfig.snapshot_interval() + " seconds");
+ }
+
+ public static Metrics createTestMetrics() {
+ NullMetric metric = new NullMetric();
+ Statistics.NullImplementation statistics = new Statistics.NullImplementation();
+ HealthMonitorConfig.Builder builder = new HealthMonitorConfig.Builder();
+ builder.snapshot_interval(60.0);
+ return new Metrics(metric, statistics, new HealthMonitorConfig(builder));
+ }
+
+ private Counter createCounter(String name, Statistics statistics) {
+ return new Counter(name, statistics, false);
+ }
+
+
+ void incrementRequests(Metric.Context metricContext) {
+ requests.increment(1);
+ metric.add(METRIC_REQUESTS, 1, metricContext);
+ }
+
+ void incrementFailedRequests(Metric.Context metricContext) {
+ failedRequests.increment(1);
+ metric.add(METRIC_FAILED_REQUESTS, 1, metricContext);
+ }
+
+ void incrementProcTime(long increment, Metric.Context metricContext) {
+ procTimeCounter.increment(increment);
+ metric.set(METRIC_LATENCY, increment, metricContext);
+ }
+
+ public long getRequests() {
+ return requests.get();
+ }
+
+ public Metric getMetric() {
+ return metric;
+ }
+
+ public MetricUpdater removeMetricUpdater(Map<String, String> dimensions) {
+ return metricUpdaters.remove(dimensions);
+ }
+
+ public static Map<String, String> createDimensions(ApplicationId applicationId) {
+ final Map<String, String> properties = new LinkedHashMap<>();
+ properties.put("tenantName", applicationId.tenant().value());
+ properties.put("applicationName", applicationId.application().value());
+ properties.put("applicationInstance", applicationId.instance().value());
+ return properties;
+ }
+
+ public static Map<String, String> createDimensions(TenantName tenant) {
+ final Map<String, String> properties = new LinkedHashMap<>();
+ properties.put("tenantName", tenant.value());
+ return properties;
+ }
+
+ public synchronized MetricUpdater getOrCreateMetricUpdater(Map<String, String> dimensions) {
+ if (metricUpdaters.containsKey(dimensions)) {
+ return metricUpdaters.get(dimensions);
+ }
+ MetricUpdater metricUpdater = new MetricUpdater(this, dimensions);
+ metricUpdaters.put(dimensions, metricUpdater);
+ return metricUpdater;
+ }
+
+ @Override
+ public void run() {
+ for (MetricUpdater metricUpdater : metricUpdaters.values()) {
+ log.log(LogLevel.DEBUG, "Running metric updater for static values for " + metricUpdater.getDimensions());
+ for (Map.Entry<String, Number> fixedMetric : metricUpdater.getStaticMetrics().entrySet()) {
+ log.log(LogLevel.DEBUG, "Setting " + fixedMetric.getKey());
+ metric.set(fixedMetric.getKey(), fixedMetric.getValue(), metricUpdater.getMetricContext());
+ }
+ }
+ setRegularMetrics();
+ timer.purge();
+ }
+
+ private void setRegularMetrics() {
+ metric.set(METRIC_FREE_MEMORY, Runtime.getRuntime().freeMemory(), null);
+ }
+
+ void increment(String metricName, Metric.Context context) {
+ metric.add(metricName, 1, context);
+ }
+
+ void set(String metricName, Number value, Metric.Context context) {
+ metric.set(metricName, value, context);
+ }
+
+ static String getMetricName(String name) {
+ return "configserver." + name;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/provision/HostProvisionerProvider.java b/configserver/src/main/java/com/yahoo/vespa/config/server/provision/HostProvisionerProvider.java
new file mode 100644
index 00000000000..91429eaefc0
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/provision/HostProvisionerProvider.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.server.provision;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.config.provision.Provisioner;
+import com.yahoo.log.LogLevel;
+
+import java.util.Optional;
+import java.util.logging.Logger;
+
+/**
+ * This class is necessary to support both having and not having a host provisioner. We inject
+ * a component registry here, which then enables us to check whether or not we have a provisioner available.
+ *
+ * @author lulf
+ * @since 5.15
+ */
+public class HostProvisionerProvider {
+
+ private static final Logger log = Logger.getLogger(HostProvisionerProvider.class.getName());
+ private final Optional<Provisioner> hostProvisioner;
+
+ public HostProvisionerProvider(ComponentRegistry<Provisioner> hostProvisionerRegistry, ConfigserverConfig configserverConfig) {
+ if (hostProvisionerRegistry.allComponents().isEmpty() || !configserverConfig.hostedVespa()) {
+ hostProvisioner = Optional.empty();
+ } else {
+ log.log(LogLevel.DEBUG, "Host provisioner injected. Will be used for all deployments");
+ hostProvisioner = Optional.of(hostProvisionerRegistry.allComponents().get(0));
+ }
+ }
+
+ private HostProvisionerProvider(ComponentRegistry<Provisioner> componentRegistry) {
+ this(componentRegistry, new ConfigserverConfig(new ConfigserverConfig.Builder()));
+ }
+
+ public Optional<Provisioner> getHostProvisioner() {
+ return hostProvisioner;
+ }
+
+ // for testing
+ public static HostProvisionerProvider empty() {
+ return new HostProvisionerProvider(new ComponentRegistry<>());
+ }
+
+ // for testing
+ public static HostProvisionerProvider withProvisioner(Provisioner provisioner) {
+ ComponentRegistry<Provisioner> registry = new ComponentRegistry<>();
+ registry.register(ComponentId.createAnonymousComponentId("foobar"), provisioner);
+ return new HostProvisionerProvider(registry, new ConfigserverConfig(new ConfigserverConfig.Builder().hostedVespa(true)));
+ }
+
+ /** Creates either an empty provider or a provider having the given provisioner */
+ public static HostProvisionerProvider from(Optional<Provisioner> provisioner) {
+ if (provisioner.isPresent())
+ return withProvisioner(provisioner.get());
+ else
+ return empty();
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/provision/ProvisionerAdapter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/provision/ProvisionerAdapter.java
new file mode 100644
index 00000000000..c1884278cd4
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/provision/ProvisionerAdapter.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.server.provision;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.provision.*;
+import com.yahoo.config.provision.Provisioner;
+
+import java.util.*;
+
+/**
+ * A wrapper for {@link Provisioner} to avoid having to expose multitenant
+ * behavior to the config model. Adapts interface from a {@link HostProvisioner} to a
+ * {@link Provisioner}.
+ *
+ * @author lulf
+ * @since 5.11
+ */
+public class ProvisionerAdapter implements HostProvisioner {
+
+ private final Provisioner provisioner;
+ private final ApplicationId applicationId;
+
+ public ProvisionerAdapter(Provisioner provisioner, ApplicationId applicationId) {
+ this.provisioner = provisioner;
+ this.applicationId = applicationId;
+ }
+
+ @Override
+ public HostSpec allocateHost(String alias) {
+ throw new UnsupportedOperationException("Allocating a single host in a hosted environment is not possible");
+ }
+
+ @Override
+ public List<HostSpec> prepare(ClusterSpec cluster, Capacity capacity, int groups, ProvisionLogger logger) {
+ return provisioner.prepare(applicationId, cluster, capacity, groups, logger);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/provision/StaticProvisioner.java b/configserver/src/main/java/com/yahoo/vespa/config/server/provision/StaticProvisioner.java
new file mode 100644
index 00000000000..0c9575fd834
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/provision/StaticProvisioner.java
@@ -0,0 +1,44 @@
+// 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.server.provision;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.provision.*;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Host provisioning from an existing {@link ProvisionInfo} instance.
+ *
+ * @author bratseth
+ */
+public class StaticProvisioner implements HostProvisioner {
+
+ private final ProvisionInfo provisionInfo;
+
+ public StaticProvisioner(ProvisionInfo provisionInfo) {
+ this.provisionInfo = provisionInfo;
+ }
+
+ @Override
+ public HostSpec allocateHost(String alias) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<HostSpec> prepare(ClusterSpec cluster, Capacity capacity, int groups, ProvisionLogger logger) {
+ List<HostSpec> l = provisionInfo.getHosts().stream()
+ .filter(host -> host.membership().isPresent() && matches(host.membership().get().cluster(), cluster))
+ .collect(Collectors.toList());
+ return l;
+ }
+
+ private boolean matches(ClusterSpec nodeCluster, ClusterSpec requestedCluster) {
+ if (requestedCluster.group().isPresent()) // we are requesting a specific group
+ return nodeCluster.equals(requestedCluster);
+ else // we are requesting nodes of all groups in this cluster
+ return nodeCluster.equalsIgnoringGroup(requestedCluster);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/StatusResource.java b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/StatusResource.java
new file mode 100644
index 00000000000..1e7114957e9
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/StatusResource.java
@@ -0,0 +1,57 @@
+// 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.server.restapi.impl;
+
+import com.google.common.annotations.Beta;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.model.api.ModelFactory;
+import com.yahoo.config.provision.Version;
+import com.yahoo.container.jaxrs.annotation.Component;
+import com.yahoo.vespa.config.server.GlobalComponentRegistry;
+import com.yahoo.vespa.config.server.http.v2.HttpGetConfigHandler;
+import com.yahoo.vespa.config.server.http.v2.HttpListConfigsHandler;
+import com.yahoo.vespa.config.server.http.v2.HttpListNamedConfigsHandler;
+import com.yahoo.vespa.config.server.http.v2.SessionActiveHandler;
+import com.yahoo.vespa.config.server.http.v2.SessionContentHandler;
+import com.yahoo.vespa.config.server.http.v2.SessionCreateHandler;
+import com.yahoo.vespa.config.server.http.v2.SessionPrepareHandler;
+import com.yahoo.vespa.config.server.restapi.resources.StatusInformation;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * A simple status handler that can provide the status of the config server.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+@Beta
+@Path("/")
+@Produces(MediaType.APPLICATION_JSON)
+public class StatusResource {
+ private final ConfigserverConfig configserverConfig;
+ private final List<String> modelVersions;
+
+ @SuppressWarnings("UnusedParameters")
+ public StatusResource(@Component SessionCreateHandler create,
+ @Component SessionContentHandler content,
+ @Component SessionPrepareHandler prepare,
+ @Component SessionActiveHandler active,
+ @Component HttpGetConfigHandler getHandler,
+ @Component HttpListConfigsHandler listHandler,
+ @Component HttpListNamedConfigsHandler listNamedHandler,
+ @Component GlobalComponentRegistry componentRegistry) {
+ this.configserverConfig = componentRegistry.getConfigserverConfig();
+ this.modelVersions = componentRegistry.getModelFactoryRegistry().getFactories().stream()
+ .map(ModelFactory::getVersion).map(Version::toString).collect(Collectors.toList());
+ }
+
+ @GET
+ public StatusInformation getStatus() {
+ return new StatusInformation(configserverConfig, modelVersions);
+ }
+} \ No newline at end of file
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/package-info.java b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/package-info.java
new file mode 100644
index 00000000000..ade3b6b26bc
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.config.server.restapi.impl;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/StatusInformation.java b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/StatusInformation.java
new file mode 100644
index 00000000000..71b819edc8a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/StatusInformation.java
@@ -0,0 +1,82 @@
+// 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.server.restapi.resources;
+
+import com.yahoo.config.provision.Version;
+import com.yahoo.vespa.defaults.Defaults;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Status information of config server. Currently needs to convert generated configserver config to a POJO that can
+ * be serialized to JSON.
+ *
+ * @author lulf
+ * @since 5.21
+ */
+public class StatusInformation {
+
+ public ConfigserverConfig configserverConfig;
+ public List<String> modelVersions;
+
+ public StatusInformation(com.yahoo.cloud.config.ConfigserverConfig configserverConfig, List<String> modelVersions) {
+ this.configserverConfig = new ConfigserverConfig(configserverConfig);
+ this.modelVersions = modelVersions;
+ }
+
+ public static class ConfigserverConfig {
+ public final int rpcport;
+ public final int numthreads;
+ public final String zookeepercfg;
+ public final Collection<ZooKeeperServer> zookeeeperserver;
+ public final long zookeeperBarrierTimeout;
+ public final Collection<String> configModelPluginDir;
+ public final String configServerDBDir;
+ public final int maxgetconfigclients;
+ public final long sessionLifetime;
+ public final String applicationDirectory;
+ public final long masterGeneration;
+ public final boolean multitenant;
+ public final int numDelayedResponseThreads;
+ public final com.yahoo.cloud.config.ConfigserverConfig.PayloadCompressionType.Enum payloadCompressionType;
+ public final boolean useVespaVersionInRequest;
+ public final String serverId;
+ public final String region;
+ public final String environment;
+
+
+ public ConfigserverConfig(com.yahoo.cloud.config.ConfigserverConfig configserverConfig) {
+ this.rpcport = configserverConfig.rpcport();
+ this.numthreads = configserverConfig.numthreads();
+ this.zookeepercfg = Defaults.getDefaults().underVespaHome(configserverConfig.zookeepercfg());
+ this.zookeeeperserver = configserverConfig.zookeeperserver().stream()
+ .map(zks -> new ZooKeeperServer(zks.hostname(), zks.port()))
+ .collect(Collectors.toList());
+ this.zookeeperBarrierTimeout = configserverConfig.zookeeper().barrierTimeout();
+ this.configModelPluginDir = configserverConfig.configModelPluginDir();
+ this.configServerDBDir = Defaults.getDefaults().underVespaHome(configserverConfig.configServerDBDir());
+ this.maxgetconfigclients = configserverConfig.maxgetconfigclients();
+ this.sessionLifetime = configserverConfig.sessionLifetime();
+ this.applicationDirectory = Defaults.getDefaults().underVespaHome(configserverConfig.applicationDirectory());
+ this.masterGeneration = configserverConfig.masterGeneration();
+ this.multitenant = configserverConfig.multitenant();
+ this.numDelayedResponseThreads = configserverConfig.numDelayedResponseThreads();
+ this.payloadCompressionType = configserverConfig.payloadCompressionType();
+ this.useVespaVersionInRequest = configserverConfig.useVespaVersionInRequest();
+ this.serverId = configserverConfig.serverId();
+ this.region = configserverConfig.region();
+ this.environment = configserverConfig.environment();
+ }
+ }
+
+ public static class ZooKeeperServer {
+ public final String hostname;
+ public final int port;
+
+ public ZooKeeperServer(String hostname, int port) {
+ this.hostname = hostname;
+ this.port = port;
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/package-info.java b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/package-info.java
new file mode 100644
index 00000000000..1794c47a6c6
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.config.server.restapi.resources;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/FileDistributionFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/FileDistributionFactory.java
new file mode 100644
index 00000000000..9575907ab67
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/FileDistributionFactory.java
@@ -0,0 +1,40 @@
+// 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.server.session;
+
+import com.google.inject.Inject;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.filedistribution.FileDistributionLock;
+import com.yahoo.vespa.config.server.filedistribution.FileDistributionProvider;
+import com.yahoo.vespa.curator.Curator;
+
+import java.io.File;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * Factory for creating providers that are used to interact with file distribution.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+@SuppressWarnings("WeakerAccess")
+public class FileDistributionFactory {
+
+ private static final String lockPath = "/vespa/filedistribution/lock";
+ private final String zkSpec;
+ private final Lock lock;
+
+ @Inject
+ public FileDistributionFactory(Curator curator) {
+ this(curator, curator.connectionSpec());
+ }
+
+ public FileDistributionFactory(Curator curator, String zkSpec) {
+ this.lock = new FileDistributionLock(curator, lockPath);
+ this.zkSpec = zkSpec;
+ }
+
+ public FileDistributionProvider createProvider(File applicationPackage, ApplicationId applicationId) {
+ return new FileDistributionProvider(applicationPackage, zkSpec, applicationId.serializedForm(), lock);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSession.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSession.java
new file mode 100644
index 00000000000..d2857068885
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSession.java
@@ -0,0 +1,170 @@
+// 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.server.session;
+
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.ApplicationMetaData;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.transaction.Transaction;
+import com.yahoo.io.IOUtils;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.*;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.configchange.ConfigChangeActions;
+import com.yahoo.vespa.curator.Curator;
+
+import java.io.File;
+import java.util.Optional;
+
+/**
+ * A LocalSession is a session that has been created locally on this configserver. A local session can be edited and
+ * prepared. Deleting a local session will ensure that the local filesystem state and global zookeeper state is
+ * cleaned for this session.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class LocalSession extends Session implements Comparable<LocalSession> {
+
+ private final ApplicationPackage applicationPackage;
+ private final ApplicationRepo applicationRepo;
+ private final SessionZooKeeperClient zooKeeperClient;
+ private final SessionPreparer sessionPreparer;
+ private final SessionContext sessionContext;
+ private final File serverDB;
+ private final SuperModelGenerationCounter superModelGenerationCounter;
+
+ /**
+ * Create a session. This involves loading the application, validating it and distributing it.
+ *
+ * @param sessionId The session id for this session.
+ */
+ // TODO tenant in SessionContext?
+ public LocalSession(TenantName tenant, long sessionId, SessionPreparer sessionPreparer, SessionContext sessionContext) {
+ super(tenant, sessionId);
+ this.serverDB = sessionContext.getServerDBSessionDir();
+ this.applicationPackage = sessionContext.getApplicationPackage();
+ this.zooKeeperClient = sessionContext.getSessionZooKeeperClient();
+ this.applicationRepo = sessionContext.getApplicationRepo();
+ this.sessionPreparer = sessionPreparer;
+ this.sessionContext = sessionContext;
+ this.superModelGenerationCounter = sessionContext.getSuperModelGenerationCounter();
+ }
+
+ public ConfigChangeActions prepare(DeployLogger logger, PrepareParams params, Optional<ApplicationSet> currentActiveApplicationSet, Path tenantPath) {
+ Curator.CompletionWaiter waiter = zooKeeperClient.createPrepareWaiter();
+ ConfigChangeActions actions = sessionPreparer.prepare(sessionContext, logger, params, currentActiveApplicationSet, tenantPath);
+ setPrepared();
+ waiter.awaitCompletion(params.getTimeoutBudget().timeLeft());
+ return actions;
+ }
+
+ public ApplicationFile getApplicationFile(Path relativePath, Mode mode) {
+ if (mode.equals(Mode.WRITE)) {
+ markSessionEdited();
+ }
+ return applicationPackage.getFile(relativePath);
+ }
+
+ private void setPrepared() {
+ setStatus(Session.Status.PREPARE);
+ }
+
+ private Transaction setActive() {
+ Transaction transaction = createSetStatusTransaction(Status.ACTIVATE);
+ transaction.add(applicationRepo.createPutApplicationTransaction(zooKeeperClient.readApplicationId(getTenant()), getSessionId()).operations());
+ return transaction;
+ }
+
+ private Transaction createSetStatusTransaction(Status status) {
+ return zooKeeperClient.createWriteStatusTransaction(status);
+ }
+
+ public Session.Status getStatus() {
+ return zooKeeperClient.readStatus();
+ }
+
+ private void setStatus(Session.Status newStatus) {
+ zooKeeperClient.writeStatus(newStatus);
+ }
+
+ public Transaction createActivateTransaction() {
+ zooKeeperClient.createActiveWaiter();
+ superModelGenerationCounter.increment();
+ return setActive();
+ }
+
+ public Transaction createDeactivateTransaction() {
+ return createSetStatusTransaction(Status.DEACTIVATE);
+ }
+
+ private void markSessionEdited() {
+ setStatus(Session.Status.NEW);
+ }
+
+ public long getActiveSessionAtCreate() {
+ return applicationPackage.getMetaData().getPreviousActiveGeneration();
+ }
+
+ // Note: Assumes monotonically increasing session ids
+ public boolean isNewerThan(long sessionId) {
+ return getSessionId() > sessionId;
+ }
+
+ /**
+ * Deletes this session from ZooKeeper and filesystem, as well as making sure the supermodel generation counter is incremented.
+ */
+ public void delete() {
+ superModelGenerationCounter.increment();
+ IOUtils.recursiveDeleteDir(serverDB);
+ zooKeeperClient.delete();
+ }
+
+ @Override
+ public int compareTo(LocalSession rhs) {
+ Long lhsId = getSessionId();
+ Long rhsId = rhs.getSessionId();
+ return lhsId.compareTo(rhsId);
+ }
+
+ // in seconds
+ public long getCreateTime() {
+ return zooKeeperClient.readCreateTime();
+ }
+
+ public void waitUntilActivated(TimeoutBudget timeoutBudget) {
+ zooKeeperClient.getActiveWaiter().awaitCompletion(timeoutBudget.timeLeft());
+ }
+
+ public void setApplicationId(ApplicationId applicationId) {
+ zooKeeperClient.writeApplicationId(applicationId);
+ }
+
+ public enum Mode {
+ READ, WRITE
+ }
+
+ public ApplicationMetaData getMetaData() {
+ return applicationPackage.getMetaData();
+ }
+
+ public ApplicationId getApplicationId() {
+ return zooKeeperClient.readApplicationId(getTenant());
+ }
+
+ public ProvisionInfo getProvisionInfo() {
+ return zooKeeperClient.getProvisionInfo();
+ }
+
+ @Override
+ public String logPre() {
+ if (getApplicationId().equals(ApplicationId.defaultId())) {
+ return Tenants.logPre(getTenant());
+ } else {
+ return Tenants.logPre(getApplicationId());
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionLoader.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionLoader.java
new file mode 100644
index 00000000000..3aa8155a7c6
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionLoader.java
@@ -0,0 +1,14 @@
+// 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.server.session;
+
+/**
+ * Interface of a component that is able to load a session given a session id.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public interface LocalSessionLoader {
+
+ LocalSession loadSession(long sessionId);
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionRepo.java
new file mode 100644
index 00000000000..8f39d0b96d1
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionRepo.java
@@ -0,0 +1,121 @@
+// 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.server.session;
+
+import com.yahoo.log.LogLevel;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.time.Clock;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+/**
+ * File-based session repository for LocalSessions. Contains state for the local instance of the configserver.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class LocalSessionRepo extends SessionRepo<LocalSession> {
+
+ private static final Logger log = Logger.getLogger(LocalSessionRepo.class.getName());
+
+ private final static FilenameFilter sessionApplicationsFilter = new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.matches("\\d+");
+ }
+ };
+
+ private final long sessionLifetime; // in seconds
+ private final ApplicationRepo applicationRepo;
+ private final Clock clock;
+
+ public LocalSessionRepo(TenantFileSystemDirs tenantFileSystemDirs, LocalSessionLoader loader, ApplicationRepo applicationRepo, Clock clock, long sessionLifeTime) {
+ this(applicationRepo, clock, sessionLifeTime);
+ loadSessions(tenantFileSystemDirs.path(), loader);
+ }
+
+ private void loadSessions(File applicationsDir, LocalSessionLoader loader) {
+ File[] applications = applicationsDir.listFiles(sessionApplicationsFilter);
+ if (applications == null) {
+ return;
+ }
+ for (File application : applications) {
+ try {
+ addSession(loader.loadSession(Long.parseLong(application.getName())));
+ } catch (IllegalArgumentException e) {
+ log.log(LogLevel.WARNING, "Could not load application '" + application.getAbsolutePath() + "':" + e.getMessage() + ", skipping it.");
+ }
+ }
+ }
+
+ /**
+ * Gets the active Session for the given application id.
+ *
+ * @return the active session, or null if there is no active session for the given application id.
+ */
+ public LocalSession getActiveSession(ApplicationId applicationId) {
+ List<ApplicationId> applicationIds = applicationRepo.listApplications();
+ if (applicationIds.contains(applicationId)) {
+ return getSession(applicationRepo.getSessionIdForApplication(applicationId));
+ }
+ return null;
+ }
+
+ // Constructor only for testing
+ public LocalSessionRepo(ApplicationRepo applicationRepo, Clock clock, long sessionLifetime) {
+ this.applicationRepo = applicationRepo;
+ this.sessionLifetime = sessionLifetime;
+ this.clock = clock;
+ }
+
+ public LocalSessionRepo(ApplicationRepo applicationRepo) {
+ this(applicationRepo, Clock.systemUTC(), TimeUnit.DAYS.toMillis(1));
+ }
+
+ @Override
+ public synchronized void addSession(LocalSession session) {
+ purgeOldSessions();
+ super.addSession(session);
+ }
+
+ private void purgeOldSessions() {
+ final List<ApplicationId> applicationIds = applicationRepo.listApplications();
+ List<LocalSession> sessions = new ArrayList<>(listSessions());
+ for (LocalSession candidate : sessions) {
+ if (hasExpired(candidate) && !isActiveSession(candidate, applicationIds)) {
+ deleteSession(candidate);
+ }
+ }
+ }
+
+ private boolean hasExpired(LocalSession candidate) {
+ return (candidate.getCreateTime() + sessionLifetime) <= TimeUnit.MILLISECONDS.toSeconds(clock.millis());
+ }
+
+ private boolean isActiveSession(LocalSession candidate, List<ApplicationId> activeIds) {
+ if (candidate.getStatus() == Session.Status.ACTIVATE && activeIds.contains(candidate.getApplicationId())) {
+ long sessionId = applicationRepo.getSessionIdForApplication(candidate.getApplicationId());
+ return (candidate.getSessionId() == sessionId);
+ } else {
+ return false;
+ }
+ }
+
+ private void deleteSession(LocalSession candidate) {
+ removeSession(candidate.getSessionId());
+ candidate.delete();
+ }
+
+ public void deleteAllSessions() {
+ List<LocalSession> sessions = new ArrayList<>(listSessions());
+ for (LocalSession session : sessions) {
+ deleteSession(session);
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java
new file mode 100644
index 00000000000..6c9224db0fe
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java
@@ -0,0 +1,165 @@
+// 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.server.session;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Rotation;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Version;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.http.SessionHandler;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.util.LinkedHashSet;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Parameters for prepare.
+ *
+ * @author lulf
+ * @since 5.1.24
+ */
+public final class PrepareParams {
+
+ static final String APPLICATION_NAME_PARAM_NAME = "applicationName";
+ static final String INSTANCE_PARAM_NAME = "instance";
+ static final String IGNORE_VALIDATION_PARAM_NAME = "ignoreValidationErrors";
+ static final String DRY_RUN_PARAM_NAME = "dryRun";
+ static final String VESPA_VERSION_PARAM_NAME = "vespaVersion";
+ static final String ROTATIONS_PARAM_NAME = "rotations";
+ static final String DOCKER_VESPA_IMAGE_VERSION_PARAM_NAME = "dockerVespaImageVersion";
+
+ private boolean ignoreValidationErrors = false;
+ private boolean dryRun = false;
+ private ApplicationId applicationId = ApplicationId.defaultId();
+ private TimeoutBudget timeoutBudget;
+ private Optional<Version> vespaVersion = Optional.empty();
+ private Set<Rotation> rotations;
+ private Optional<Version> dockerVespaImageVersion = Optional.empty();
+
+ PrepareParams() {
+ this(new ConfigserverConfig(new ConfigserverConfig.Builder()));
+ }
+
+ public PrepareParams(ConfigserverConfig configserverConfig) {
+ timeoutBudget = new TimeoutBudget(Clock.systemUTC(), getBarrierTimeout(configserverConfig));
+ }
+
+ public PrepareParams applicationId(ApplicationId applicationId) {
+ this.applicationId = applicationId;
+ return this;
+ }
+
+ public PrepareParams ignoreValidationErrors(boolean ignoreValidationErrors) {
+ this.ignoreValidationErrors = ignoreValidationErrors;
+ return this;
+ }
+
+ public PrepareParams dryRun(boolean dryRun) {
+ this.dryRun = dryRun;
+ return this;
+ }
+
+ public PrepareParams timeoutBudget(TimeoutBudget timeoutBudget) {
+ this.timeoutBudget = timeoutBudget;
+ return this;
+ }
+
+ public PrepareParams vespaVersion(String vespaVersion) {
+ Optional<Version> version = Optional.empty();
+ if (vespaVersion != null && !vespaVersion.isEmpty()) {
+ version = Optional.of(Version.fromString(vespaVersion));
+ }
+ this.vespaVersion = version;
+ return this;
+ }
+
+ public PrepareParams rotations(String rotationsString) {
+ this.rotations = new LinkedHashSet<>();
+ if (rotationsString != null && !rotationsString.isEmpty()) {
+ String[] rotations = rotationsString.split(",");
+ for (String s : rotations) {
+ this.rotations.add(new Rotation(s));
+ }
+ }
+ return this;
+ }
+
+ public PrepareParams dockerVespaImageVersion(String dockerVespaImageVersion) {
+ Optional<Version> version = Optional.empty();
+ if (dockerVespaImageVersion != null && !dockerVespaImageVersion.isEmpty()) {
+ version = Optional.of(Version.fromString(dockerVespaImageVersion));
+ }
+ this.dockerVespaImageVersion = version;
+ return this;
+ }
+
+ public static PrepareParams fromHttpRequest(HttpRequest request, TenantName tenant, ConfigserverConfig configserverConfig) {
+ return new PrepareParams(configserverConfig).ignoreValidationErrors(request.getBooleanProperty(IGNORE_VALIDATION_PARAM_NAME))
+ .dryRun(request.getBooleanProperty(DRY_RUN_PARAM_NAME))
+ .timeoutBudget(SessionHandler.getTimeoutBudget(request, getBarrierTimeout(configserverConfig)))
+ .applicationId(createApplicationId(request, tenant))
+ .vespaVersion(request.getProperty(VESPA_VERSION_PARAM_NAME))
+ .rotations(request.getProperty(ROTATIONS_PARAM_NAME))
+ .dockerVespaImageVersion(request.getProperty(DOCKER_VESPA_IMAGE_VERSION_PARAM_NAME));
+ }
+
+ private static Duration getBarrierTimeout(ConfigserverConfig configserverConfig) {
+ return Duration.ofSeconds(configserverConfig.zookeeper().barrierTimeout());
+ }
+
+ private static ApplicationId createApplicationId(HttpRequest request, TenantName tenant) {
+ return new ApplicationId.Builder()
+ .tenant(tenant)
+ .applicationName(getPropertyWithDefault(request, APPLICATION_NAME_PARAM_NAME, "default"))
+ .instanceName(getPropertyWithDefault(request, INSTANCE_PARAM_NAME, "default"))
+ .build();
+ }
+
+ private static String getPropertyWithDefault(HttpRequest request, String propertyName, String defaultProperty) {
+ return getProperty(request, propertyName).orElse(defaultProperty);
+ }
+
+ private static Optional<String> getProperty(HttpRequest request, String propertyName) {
+ return Optional.ofNullable(request.getProperty(propertyName));
+ }
+
+ public String getApplicationName() {
+ return applicationId.application().value();
+ }
+
+ public ApplicationId getApplicationId() {
+ return applicationId;
+ }
+
+ public Optional<Version> vespaVersion() { return vespaVersion; }
+
+ public Set<Rotation> rotations() { return rotations; }
+
+ public boolean ignoreValidationErrors() {
+ return ignoreValidationErrors;
+ }
+
+ public boolean isDryRun() {
+ return dryRun;
+ }
+
+ public TimeoutBudget getTimeoutBudget() {
+ return timeoutBudget;
+ }
+
+ public Optional<Version> getVespaVersion() {
+ return vespaVersion;
+ }
+
+ public Set<Rotation> getRotations() {
+ return rotations;
+ }
+
+ public Optional<Version> getDockerVespaImageVersion() {
+ return dockerVespaImageVersion;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSession.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSession.java
new file mode 100644
index 00000000000..2ce378d0464
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSession.java
@@ -0,0 +1,97 @@
+// 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.server.session;
+
+import com.yahoo.config.provision.*;
+import com.yahoo.vespa.config.server.*;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.server.modelfactory.ActivatedModelsBuilder;
+import com.yahoo.vespa.curator.Curator;
+
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * A RemoteSession represents a session created on another config server. This session can
+ * be regarded as read only, and this interface only allows reading information about a session.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class RemoteSession extends Session {
+
+ private static final Logger log = Logger.getLogger(RemoteSession.class.getName());
+ private volatile ApplicationSet applicationSet = null;
+ private final SessionZooKeeperClient zooKeeperClient;
+ private final ActivatedModelsBuilder applicationLoader;
+
+ /**
+ * Creates a session. This involves loading the application, validating it and distributing it.
+ *
+ * @param tenant The name of the tenant creating session
+ * @param sessionId The session id for this session.
+ * @param globalComponentRegistry a registry of global components
+ * @param zooKeeperClient a SessionZooKeeperClient instance
+ */
+ public RemoteSession(TenantName tenant,
+ long sessionId,
+ GlobalComponentRegistry globalComponentRegistry,
+ SessionZooKeeperClient zooKeeperClient) {
+ super(tenant, sessionId);
+ this.zooKeeperClient = zooKeeperClient;
+ this.applicationLoader = new ActivatedModelsBuilder(tenant, sessionId, zooKeeperClient, globalComponentRegistry);
+ }
+
+ public void loadPrepared() {
+ Curator.CompletionWaiter waiter = zooKeeperClient.getPrepareWaiter();
+ ensureApplicationLoaded();
+ waiter.notifyCompletion();
+ }
+
+ private ApplicationSet loadApplication() {
+ return ApplicationSet.fromList(applicationLoader.buildModels(zooKeeperClient.readApplicationId(getTenant()),
+ zooKeeperClient.loadApplicationPackage()));
+ }
+
+ public ApplicationSet ensureApplicationLoaded() {
+ if (applicationSet == null) {
+ applicationSet = loadApplication();
+ }
+ return applicationSet;
+ }
+
+ public Session.Status getStatus() {
+ return zooKeeperClient.readStatus();
+ }
+
+ public void deactivate() {
+ applicationSet = null;
+ }
+
+ public void makeActive(ReloadHandler reloadHandler) {
+ Curator.CompletionWaiter waiter = zooKeeperClient.getActiveWaiter();
+ log.log(LogLevel.DEBUG, logPre()+"Getting session from repo: " + getSessionId());
+ ApplicationSet app = ensureApplicationLoaded();
+ log.log(LogLevel.DEBUG, logPre() + "Reloading config for " + app);
+ reloadHandler.reloadConfig(app);
+ log.log(LogLevel.DEBUG, logPre() + "Notifying " + waiter);
+ waiter.notifyCompletion();
+ log.log(LogLevel.DEBUG, logPre() + "Session activated: " + app);
+ }
+
+ @Override
+ public String logPre() {
+ if (applicationSet != null) {
+ return Tenants.logPre(applicationSet.getForVersionOrLatest(Optional.empty()).getId());
+ }
+
+ return Tenants.logPre(getTenant());
+ }
+
+ public void confirmUpload() {
+ Curator.CompletionWaiter waiter = zooKeeperClient.getUploadWaiter();
+ log.log(LogLevel.DEBUG, "Notifying upload waiter for session " + getSessionId());
+ waiter.notifyCompletion();
+ log.log(LogLevel.DEBUG, "Done notifying for session " + getSessionId());
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionFactory.java
new file mode 100644
index 00000000000..c44436740be
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionFactory.java
@@ -0,0 +1,44 @@
+// 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.server.session;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.path.Path;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.vespa.config.server.GlobalComponentRegistry;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+import com.yahoo.vespa.curator.Curator;
+
+/**
+ * @author lulf
+ * @since 5.1.24
+ */
+public class RemoteSessionFactory {
+
+ private final GlobalComponentRegistry componentRegistry;
+ private final Curator curator;
+ private final ConfigCurator configCurator;
+ private final Path sessionDirPath;
+ private final ConfigDefinitionRepo defRepo;
+ private final TenantName tenant;
+ private final ConfigserverConfig configserverConfig;
+
+ public RemoteSessionFactory(GlobalComponentRegistry componentRegistry,
+ Path sessionsPath,
+ TenantName tenant) {
+ this.componentRegistry = componentRegistry;
+ this.curator = componentRegistry.getCurator();
+ this.configCurator = componentRegistry.getConfigCurator();
+ this.sessionDirPath = sessionsPath;
+ this.tenant = tenant;
+ this.defRepo = componentRegistry.getConfigDefinitionRepo();
+ this.configserverConfig = componentRegistry.getConfigserverConfig();
+ }
+
+ public RemoteSession createSession(long sessionId) {
+ Path sessionPath = sessionDirPath.append(String.valueOf(sessionId));
+ SessionZooKeeperClient sessionZKClient = new SessionZooKeeperClient(curator, configCurator, sessionPath, defRepo, configserverConfig.serverId());
+ return new RemoteSession(tenant, sessionId, componentRegistry, sessionZKClient);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionRepo.java
new file mode 100644
index 00000000000..78d7704506f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionRepo.java
@@ -0,0 +1,226 @@
+// 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.server.session;
+
+import java.util.*;
+import java.util.concurrent.ExecutorService;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Multiset;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.ApplicationSet;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.yolean.Exceptions;
+import com.yahoo.vespa.config.server.ReloadHandler;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.recipes.cache.*;
+
+/**
+ * Will watch/prepare sessions (applications) based on watched nodes in ZooKeeper, set for example
+ * by the prepare HTTP handler on another configserver. The zookeeper state watched in this class is shared
+ * between all configservers, so it should not modify any global state, because the operation will be performed
+ * on all servers. The repo can be regarded as read only from the POV of the configserver.
+ *
+ * @author vegardh
+ * @author lulf
+ * @since 5.1
+ */
+public class RemoteSessionRepo extends SessionRepo<RemoteSession> implements NodeCacheListener, PathChildrenCacheListener {
+
+ private static final Logger log = Logger.getLogger(RemoteSessionRepo.class.getName());
+ private final Curator curator;
+ private final Path sessionsPath;
+ private final RemoteSessionFactory remoteSessionFactory;
+ private final Map<Long, SessionStateWatcher> sessionStateWatchers = new HashMap<>();
+ private final ReloadHandler reloadHandler;
+ private final MetricUpdater metrics;
+ private final Curator.DirectoryCache directoryCache;
+ private final ApplicationRepo applicationRepo;
+
+ public static RemoteSessionRepo create(Curator curator,
+ RemoteSessionFactory remoteSessionFactory,
+ ReloadHandler reloadHandler,
+ Path sessionsPath,
+ ApplicationRepo applicationRepo,
+ MetricUpdater metrics,
+ ExecutorService executorService) throws Exception {
+ return new RemoteSessionRepo(curator, remoteSessionFactory, reloadHandler, sessionsPath, applicationRepo, metrics, executorService);
+ }
+
+ /**
+ * Used when the RemoteSessionRepo is set up programmatically from a Tenant, i.e. config v2
+ * @param curator a {@link Curator} instance.
+ * @param remoteSessionFactory a {@link com.yahoo.vespa.config.server.session.RemoteSessionFactory}
+ * @param reloadHandler a {@link com.yahoo.vespa.config.server.ReloadHandler}
+ * @param sessionsPath a {@link com.yahoo.path.Path} to the sessions dir.
+ * @param applicationRepo an {@link com.yahoo.vespa.config.server.application.ApplicationRepo} object.
+ * @param executorService an {@link ExecutorService} to run callbacks from ZooKeeper.
+ * @throws java.lang.Exception if creating the repo fails
+ */
+ private RemoteSessionRepo(Curator curator,
+ RemoteSessionFactory remoteSessionFactory,
+ ReloadHandler reloadHandler,
+ Path sessionsPath,
+ ApplicationRepo applicationRepo,
+ MetricUpdater metricUpdater,
+ ExecutorService executorService) throws Exception {
+ this.curator = curator;
+ this.sessionsPath = sessionsPath;
+ this.applicationRepo = applicationRepo;
+ this.remoteSessionFactory = remoteSessionFactory;
+ this.reloadHandler = reloadHandler;
+ this.metrics = metricUpdater;
+ this.directoryCache = curator.createDirectoryCache(sessionsPath.getAbsolute(), false, false, executorService);
+ this.directoryCache.start();
+ this.directoryCache.addListener(this);
+ sessionsChanged(getSessionList(directoryCache.getCurrentData()));
+ }
+
+ private void loadActiveSession(RemoteSession session) {
+ tryReload(session.ensureApplicationLoaded(), session.logPre());
+ }
+
+ private void tryReload(ApplicationSet applicationSet, String logPre) {
+ try {
+ reloadHandler.reloadConfig(applicationSet);
+ log.log(LogLevel.INFO, logPre+"Application activated successfully: " + applicationSet.getId());
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, logPre+"Skipping loading of application '" + applicationSet.getId() + "': " + Exceptions.toMessageString(e));
+ }
+ }
+
+ // For testing only
+ public RemoteSessionRepo() {
+ this.curator = null;
+ this.remoteSessionFactory = null;
+ this.reloadHandler = null;
+ this.sessionsPath = Path.createRoot();
+ this.metrics = null;
+ this.directoryCache = null;
+ this.applicationRepo = null;
+ }
+
+ private List<Long> getSessionList(List<ChildData> children) {
+ List<Long> sessions = new ArrayList<>();
+ for (ChildData data : children) {
+ sessions.add(Long.parseLong(Path.fromString(data.getPath()).getName()));
+ }
+ return sessions;
+ }
+
+ synchronized void sessionsChanged(List<Long> sessions) throws NumberFormatException {
+ checkForRemovedSessions(sessions);
+ checkForAddedSessions(sessions);
+ }
+
+ private void checkForRemovedSessions(List<Long> sessions) {
+ for (RemoteSession session : listSessions()) {
+ if (!sessions.contains(session.getSessionId())) {
+ SessionStateWatcher watcher = sessionStateWatchers.remove(session.getSessionId());
+ watcher.close();
+ removeSession(session.getSessionId());
+ metrics.incRemovedSessions();
+ }
+ }
+ }
+
+ private void checkForAddedSessions(List<Long> sessions) {
+ for (Long sessionId : sessions) {
+ if (getSession(sessionId) == null) {
+ log.log(LogLevel.DEBUG, "Loading session id " + sessionId);
+ newSession(sessionId);
+ metrics.incAddedSessions();
+ }
+ }
+ }
+
+ /**
+ * A session for which we don't have a watcher, i.e. hitherto unknown to us.
+ *
+ * @param sessionId session id for the new session
+ */
+ private void newSession(long sessionId) {
+ try {
+ log.log(LogLevel.DEBUG, "Adding session to RemoteSessionRepo: " + sessionId);
+ RemoteSession session = remoteSessionFactory.createSession(sessionId);
+ Path sessionPath = sessionsPath.append(String.valueOf(sessionId));
+ Curator.FileCache fileCache = curator.createFileCache(sessionPath.append(ConfigCurator.SESSIONSTATE_ZK_SUBPATH).getAbsolute(), false);
+ fileCache.addListener(this);
+ loadSessionIfActive(session);
+ sessionStateWatchers.put(sessionId, new SessionStateWatcher(fileCache, reloadHandler, session, metrics));
+ addSession(session);
+ } catch (Exception e) {
+ log.log(Level.WARNING, "Failed loading session " + sessionId + " (no config for this session can be served) : " + Exceptions.toMessageString(e));
+ }
+ }
+
+ private void loadSessionIfActive(RemoteSession session) {
+ for (ApplicationId applicationId : applicationRepo.listApplications()) {
+ try {
+ if (applicationRepo.getSessionIdForApplication(applicationId) == session.getSessionId()) {
+ log.log(LogLevel.DEBUG, "Found active application for session " + session.getSessionId() + " , loading it");
+ loadActiveSession(session);
+ break;
+ }
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, session.logPre() + " error reading session id for " + applicationId);
+ }
+ }
+ }
+
+ public synchronized void close() {
+ try {
+ if (directoryCache != null) {
+ directoryCache.close();
+ }
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, "Exception when closing path cache", e);
+ } finally {
+ checkForRemovedSessions(new ArrayList<>());
+ }
+ }
+
+ @Override
+ public void nodeChanged() throws Exception {
+ Multiset<Session.Status> sessionMetrics = HashMultiset.create();
+ for (RemoteSession session : listSessions()) {
+ sessionMetrics.add(session.getStatus());
+ }
+ metrics.setNewSessions(sessionMetrics.count(Session.Status.NEW));
+ metrics.setPreparedSessions(sessionMetrics.count(Session.Status.PREPARE));
+ metrics.setActivatedSessions(sessionMetrics.count(Session.Status.ACTIVATE));
+ metrics.setDeactivatedSessions(sessionMetrics.count(Session.Status.DEACTIVATE));
+ }
+
+ @Override
+ public void childEvent(CuratorFramework framework, PathChildrenCacheEvent event) throws Exception {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Got child event: " + event);
+ }
+ switch (event.getType()) {
+ case CHILD_ADDED:
+ sessionsChanged(getSessionList(directoryCache.getCurrentData()));
+ synchronizeOnNew(getSessionList(Collections.singletonList(event.getData())));
+ break;
+ case CHILD_REMOVED:
+ sessionsChanged(getSessionList(directoryCache.getCurrentData()));
+ break;
+ }
+ }
+
+ private void synchronizeOnNew(List<Long> sessionList) {
+ for (long sessionId : sessionList) {
+ RemoteSession session = getSession(sessionId);
+ log.log(LogLevel.DEBUG, session.logPre() + "Confirming upload for session " + sessionId);
+ session.confirmUpload();
+
+ }
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/ServerCacheLoader.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/ServerCacheLoader.java
new file mode 100644
index 00000000000..329eab89458
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/ServerCacheLoader.java
@@ -0,0 +1,82 @@
+// 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.server.session;
+
+import com.google.common.base.Splitter;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.subscription.CfgConfigPayloadBuilder;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+import com.yahoo.vespa.config.server.ServerCache;
+import com.yahoo.vespa.config.util.ConfigUtils;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * This class is tasked with reading config definitions and legacy configs/ from zookeeper, and create
+ * a {@link ServerCache} instance containing these in memory.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ServerCacheLoader {
+
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(ServerCacheLoader.class.getName());
+ private final ConfigDefinitionRepo repo;
+ private final ConfigCurator configCurator;
+ private final Path path;
+ public ServerCacheLoader(ConfigCurator configCurator, Path rootPath, ConfigDefinitionRepo repo) {
+ this.configCurator = configCurator;
+ this.path = rootPath;
+ this.repo = repo;
+ }
+
+ public ServerCache loadCache() {
+ return loadConfigDefinitions();
+ }
+
+ /**
+ * Reads config definitions from zookeeper, parses them and puts both ConfigDefinition instances
+ * and payload (raw config definition) into cache.
+ *
+ * @return the populated cache.
+ */
+ public ServerCache loadConfigDefinitions() {
+ ServerCache cache = new ServerCache();
+ try {
+ log.log(LogLevel.DEBUG, "Getting config definitions");
+ loadGlobalConfigDefinitions(cache);
+ loadConfigDefinitionsFromPath(cache, path.append(ConfigCurator.USER_DEFCONFIGS_ZK_SUBPATH).getAbsolute());
+ log.log(LogLevel.DEBUG, "Done getting config definitions");
+ } catch (Exception e) {
+ throw new IllegalStateException("Could not load config definitions for " + path, e);
+ }
+ return cache;
+ }
+
+ private void loadGlobalConfigDefinitions(ServerCache cache) {
+ for (Map.Entry<ConfigDefinitionKey, ConfigDefinition> entry : repo.getConfigDefinitions().entrySet()) {
+ cache.addDef(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Loads config definitions from a specified path into server cache and returns it.
+ *
+ * @param appPath the path to load config definitions from
+ */
+ private void loadConfigDefinitionsFromPath(ServerCache cache, String appPath) throws InterruptedException {
+ if ( ! configCurator.exists(appPath)) return;
+ for (String nodeName : configCurator.getChildren(appPath)) {
+ String payload = configCurator.getData(appPath, nodeName);
+ ConfigDefinitionKey dKey = ConfigUtils.createConfigDefinitionKeyFromZKString(nodeName);
+ cache.addDef(dKey, new ConfigDefinition(dKey.getName(), Splitter.on("\n").splitToList(payload).toArray(new String[0])));
+ }
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java
new file mode 100644
index 00000000000..961f9d10a60
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.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.server.session;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.Tenants;
+
+/**
+ * A session represents an instance of an application that can be edited, prepared and activated. This
+ * class represents the common stuff between sessions working on the local file
+ * system ({@link LocalSession}s) and sessions working on zookeeper {@link RemoteSession}s.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public abstract class Session {
+
+ private final long sessionId;
+ protected final TenantName tenant;
+
+ protected Session(TenantName tenant, long sessionId) {
+ this.tenant = tenant;
+ this.sessionId = sessionId;
+ }
+ /**
+ * Retrieve the session id for this session.
+ * @return the session id.
+ */
+ public final long getSessionId() {
+ return sessionId;
+ }
+
+ @Override
+ public String toString() {
+ return "Session,id=" + sessionId;
+ }
+
+ /**
+ * Represents the status of this session.
+ */
+ public enum Status {
+ NEW, PREPARE, ACTIVATE, DEACTIVATE, NONE;
+
+ public static Status parse(String data) {
+ for (Status status : Status.values()) {
+ if (status.name().equals(data)) {
+ return status;
+ }
+ }
+ return Status.NEW;
+ }
+ }
+
+ public TenantName getTenant() {
+ return tenant;
+ }
+
+ /**
+ * Helper to provide a log message preamble for code dealing with sessions
+ * @return log preamble
+ */
+ public String logPre() {
+ return Tenants.logPre(getTenant());
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionContext.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionContext.java
new file mode 100644
index 00000000000..dd908eaa559
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionContext.java
@@ -0,0 +1,60 @@
+// 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.server.session;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.vespa.config.server.HostValidator;
+import com.yahoo.vespa.config.server.SuperModelGenerationCounter;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+
+import java.io.File;
+
+/**
+ * The dependencies needed for a local session to be edited and prepared.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionContext {
+
+ private final ApplicationPackage applicationPackage;
+ private final SessionZooKeeperClient sessionZooKeeperClient;
+ private final File serverDBSessionDir;
+ private final ApplicationRepo applicationRepo;
+ private final HostValidator<ApplicationId> hostRegistry;
+ private final SuperModelGenerationCounter superModelGenerationCounter;
+
+ public SessionContext(ApplicationPackage applicationPackage, SessionZooKeeperClient sessionZooKeeperClient,
+ File serverDBSessionDir, ApplicationRepo applicationRepo,
+ HostValidator<ApplicationId> hostRegistry, SuperModelGenerationCounter superModelGenerationCounter) {
+ this.applicationPackage = applicationPackage;
+ this.sessionZooKeeperClient = sessionZooKeeperClient;
+ this.serverDBSessionDir = serverDBSessionDir;
+ this.applicationRepo = applicationRepo;
+ this.hostRegistry = hostRegistry;
+ this.superModelGenerationCounter = superModelGenerationCounter;
+ }
+
+ public ApplicationPackage getApplicationPackage() {
+ return applicationPackage;
+ }
+
+ public SessionZooKeeperClient getSessionZooKeeperClient() {
+ return sessionZooKeeperClient;
+ }
+
+ public File getServerDBSessionDir() {
+ return serverDBSessionDir;
+ }
+
+ public ApplicationRepo getApplicationRepo() {
+ return applicationRepo;
+ }
+
+ public HostValidator<ApplicationId> getHostValidator() { return hostRegistry; }
+
+ public SuperModelGenerationCounter getSuperModelGenerationCounter() {
+ return superModelGenerationCounter;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactory.java
new file mode 100644
index 00000000000..87af5351186
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactory.java
@@ -0,0 +1,38 @@
+// 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.server.session;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.vespa.config.server.TimeoutBudget;
+
+import java.io.File;
+
+/**
+ * A session factory responsible for creating deploy sessions.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public interface SessionFactory {
+ /**
+ * Creates a new deployment session from an application package.
+ *
+ *
+ *
+ * @param applicationDirectory a File pointing to an application.
+ * @param applicationName name of the application for this new session.
+ * @param logger a deploy logger where the deploy log will be written.
+ * @param timeoutBudget Timeout for creating session and waiting for other servers.
+ * @return a new session
+ */
+ LocalSession createSession(File applicationDirectory, String applicationName, DeployLogger logger, TimeoutBudget timeoutBudget);
+
+ /**
+ * Creates a new deployment session from an already existing session.
+ *
+ * @param existingSession The session to use as base
+ * @param logger a deploy logger where the deploy log will be written.
+ * @param timeoutBudget Timeout for creating session and waiting for other servers.
+ * @return a new session
+ */
+ LocalSession createSessionFromExisting(LocalSession existingSession, DeployLogger logger, TimeoutBudget timeoutBudget);
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactoryImpl.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactoryImpl.java
new file mode 100644
index 00000000000..3ef6f23b84e
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactoryImpl.java
@@ -0,0 +1,170 @@
+// 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.server.session;
+
+import com.yahoo.config.application.api.ApplicationMetaData;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.application.provider.*;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.io.IOUtils;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.*;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.config.server.application.ApplicationRepo;
+import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs;
+import com.yahoo.vespa.config.server.zookeeper.SessionCounter;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+import com.yahoo.vespa.curator.Curator;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+/**
+ * Serves as the factory of sessions. Takes care of copying files to the correct folder and initializing the
+ * session state.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionFactoryImpl implements SessionFactory, LocalSessionLoader {
+
+ private static final Logger log = Logger.getLogger(SessionFactoryImpl.class.getName());
+ private static final long nonExistingActiveSession = 0;
+
+ private final SessionPreparer sessionPreparer;
+ private final Curator curator;
+ private final ConfigCurator configCurator;
+ private final SessionCounter sessionCounter;
+ private final ApplicationRepo applicationRepo;
+ private final Path sessionsPath;
+ private final TenantFileSystemDirs tenantFileSystemDirs;
+ private final HostValidator<ApplicationId> hostRegistry;
+ private final SuperModelGenerationCounter superModelGenerationCounter;
+ private final ConfigDefinitionRepo defRepo;
+ private final TenantName tenant;
+ private final String serverId;
+
+ public SessionFactoryImpl(GlobalComponentRegistry globalComponentRegistry,
+ SessionCounter sessionCounter,
+ Path sessionsPath,
+ ApplicationRepo applicationRepo,
+ TenantFileSystemDirs tenantFileSystemDirs, HostValidator<ApplicationId> hostRegistry, TenantName tenant) {
+ this.hostRegistry = hostRegistry;
+ this.tenant = tenant;
+ this.sessionPreparer = globalComponentRegistry.getSessionPreparer();
+ this.curator = globalComponentRegistry.getCurator();
+ this.configCurator = globalComponentRegistry.getConfigCurator();
+ this.sessionCounter = sessionCounter;
+ this.sessionsPath = sessionsPath;
+ this.applicationRepo = applicationRepo;
+ this.tenantFileSystemDirs = tenantFileSystemDirs;
+ this.superModelGenerationCounter = globalComponentRegistry.getSuperModelGenerationCounter();
+ this.defRepo = globalComponentRegistry.getConfigDefinitionRepo();
+ this.serverId = globalComponentRegistry.getConfigserverConfig().serverId();
+ }
+
+ @Override
+ public LocalSession createSession(File applicationFile, String applicationName, DeployLogger logger, TimeoutBudget timeoutBudget) {
+ return create(applicationFile, applicationName, logger, nonExistingActiveSession, timeoutBudget);
+ }
+
+ private void ensureZKPathDoesNotExist(Path sessionPath) {
+ if (configCurator.exists(sessionPath.getAbsolute())) {
+ throw new IllegalArgumentException("Path " + sessionPath.getAbsolute() + " already exists in ZooKeeper");
+ }
+ }
+
+ private ApplicationPackage createApplication(File userDir,
+ File configApplicationDir,
+ String applicationName,
+ long sessionId,
+ long currentlyActiveSession) {
+ long deployTimestamp = System.currentTimeMillis();
+ String user = System.getenv("USER");
+ if (user == null) {
+ user = "unknown";
+ }
+ DeployData deployData = new DeployData(user, userDir.getAbsolutePath(), applicationName, deployTimestamp, sessionId, currentlyActiveSession);
+ return FilesApplicationPackage.fromFileWithDeployData(configApplicationDir, deployData);
+ }
+
+ private LocalSession createSessionFromApplication(ApplicationPackage applicationPackage,
+ long sessionId,
+ SessionZooKeeperClient sessionZKClient, TimeoutBudget timeoutBudget) throws Exception {
+ log.log(LogLevel.DEBUG, Tenants.logPre(tenant) + "Creating session " + sessionId + " in ZooKeeper");
+ sessionZKClient.createNewSession(System.currentTimeMillis(), TimeUnit.MILLISECONDS);
+ log.log(LogLevel.DEBUG, Tenants.logPre(tenant) + "Creating upload waiter for session " + sessionId);
+ Curator.CompletionWaiter waiter = sessionZKClient.getUploadWaiter();
+ log.log(LogLevel.DEBUG, Tenants.logPre(tenant) + "Done creating upload waiter for session " + sessionId);
+ LocalSession session = new LocalSession(tenant, sessionId, sessionPreparer, new SessionContext(applicationPackage, sessionZKClient, getSessionAppDir(sessionId), applicationRepo, hostRegistry, superModelGenerationCounter));
+ log.log(LogLevel.DEBUG, Tenants.logPre(tenant) + "Waiting on upload waiter for session " + sessionId);
+ waiter.awaitCompletion(timeoutBudget.timeLeft());
+ log.log(LogLevel.DEBUG, Tenants.logPre(tenant) + "Done waiting on upload waiter for session " + sessionId);
+ return session;
+ }
+
+ @Override
+ public LocalSession createSessionFromExisting(LocalSession existingSession,
+ DeployLogger logger,
+ TimeoutBudget timeoutBudget) {
+ File existingApp = getSessionAppDir(existingSession.getSessionId());
+ ApplicationMetaData metaData = FilesApplicationPackage.readMetaData(existingApp);
+ final ApplicationId existingSessionId = existingSession.getApplicationId();
+
+
+ final long liveApp = getLiveApp(existingSessionId);
+ logger.log(LogLevel.DEBUG, "Create from existing application id " + existingSessionId + ", live app for it is " + liveApp);
+ LocalSession session = create(existingApp, metaData.getApplicationName(), logger, liveApp, timeoutBudget);
+ session.setApplicationId(existingSessionId);
+ return session;
+ }
+
+ private LocalSession create(File applicationFile, String applicationName, DeployLogger logger, long currentlyActiveSession, TimeoutBudget timeoutBudget) {
+ long sessionId = sessionCounter.nextSessionId();
+ Path sessionIdPath = sessionsPath.append(String.valueOf(sessionId));
+ log.log(LogLevel.DEBUG, Tenants.logPre(tenant) + "Next session id is " + sessionId + " , sessionIdPath=" + sessionIdPath.getAbsolute());
+ try {
+ ensureZKPathDoesNotExist(sessionIdPath);
+ SessionZooKeeperClient sessionZooKeeperClient = new SessionZooKeeperClient(curator, configCurator, sessionIdPath, defRepo, serverId);
+ File userApplicationDir = tenantFileSystemDirs.getUserApplicationDir(sessionId);
+ IOUtils.copyDirectory(applicationFile, userApplicationDir);
+ ApplicationPackage applicationPackage = createApplication(applicationFile, userApplicationDir, applicationName, sessionId, currentlyActiveSession);
+ applicationPackage.writeMetaData();
+ logger.log(LogLevel.SPAM, "Application package is written to disk");
+ return createSessionFromApplication(applicationPackage, sessionId, sessionZooKeeperClient, timeoutBudget);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException("Error creating session: " + e.getMessage(), e);
+ }
+ }
+
+ private File getSessionAppDir(long sessionId) {
+ File appDir = tenantFileSystemDirs.getUserApplicationDir(sessionId);
+ if (!appDir.exists() || !appDir.isDirectory()) {
+ throw new IllegalArgumentException("Unable to find correct application directory for session " + sessionId);
+ }
+ return appDir;
+ }
+
+ @Override
+ public LocalSession loadSession(long sessionId) {
+ File sessionDir = getSessionAppDir(sessionId);
+ ApplicationPackage applicationPackage = FilesApplicationPackage.fromFile(sessionDir);
+ Path sessionIdPath = sessionsPath.append(String.valueOf(sessionId));
+ SessionZooKeeperClient sessionZKClient = new SessionZooKeeperClient(curator, configCurator, sessionIdPath, defRepo, serverId);
+ SessionContext context = new SessionContext(applicationPackage, sessionZKClient, sessionDir, applicationRepo, hostRegistry, superModelGenerationCounter);
+ return new LocalSession(tenant, sessionId, sessionPreparer, context);
+ }
+
+ private long getLiveApp(ApplicationId applicationId) {
+ List<ApplicationId> applicationIds = applicationRepo.listApplications();
+ if (applicationIds.contains(applicationId)) {
+ return applicationRepo.getSessionIdForApplication(applicationId);
+ }
+ return nonExistingActiveSession;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java
new file mode 100644
index 00000000000..4057d010b15
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java
@@ -0,0 +1,283 @@
+// 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.server.session;
+
+import com.google.common.collect.ImmutableList;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.model.api.ModelContext;
+import com.yahoo.config.provision.*;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.config.server.ApplicationSet;
+import com.yahoo.vespa.config.server.ConfigServerSpec;
+import com.yahoo.vespa.config.server.application.PermanentApplicationPackage;
+import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry;
+import com.yahoo.vespa.config.server.RotationsCache;
+import com.yahoo.vespa.config.server.configchange.ConfigChangeActions;
+import com.yahoo.vespa.config.server.deploy.ModelContextImpl;
+import com.yahoo.vespa.config.server.deploy.ZooKeeperDeployer;
+import com.yahoo.vespa.config.server.http.InvalidApplicationException;
+import com.yahoo.vespa.config.server.modelfactory.PreparedModelsBuilder;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+
+import com.yahoo.vespa.curator.Curator;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerException;
+
+/**
+ * A SessionPreparer is responsible for preparing a session given an application package.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionPreparer {
+
+ private static final Logger log = Logger.getLogger(SessionPreparer.class.getName());
+
+ private final ModelFactoryRegistry modelFactoryRegistry;
+ private final FileDistributionFactory fileDistributionFactory;
+ private final HostProvisionerProvider hostProvisionerProvider;
+ private final PermanentApplicationPackage permanentApplicationPackage;
+ private final ConfigserverConfig configserverConfig;
+ private final ConfigDefinitionRepo configDefinitionRepo;
+ private final Curator curator;
+ private final Zone zone;
+
+ public SessionPreparer(ModelFactoryRegistry modelFactoryRegistry,
+ FileDistributionFactory fileDistributionFactory,
+ HostProvisionerProvider hostProvisionerProvider,
+ PermanentApplicationPackage permanentApplicationPackage,
+ ConfigserverConfig configserverConfig,
+ ConfigDefinitionRepo configDefinitionRepo,
+ Curator curator,
+ Zone zone) {
+ this.modelFactoryRegistry = modelFactoryRegistry;
+ this.fileDistributionFactory = fileDistributionFactory;
+ this.hostProvisionerProvider = hostProvisionerProvider;
+ this.permanentApplicationPackage = permanentApplicationPackage;
+ this.configserverConfig = configserverConfig;
+ this.configDefinitionRepo = configDefinitionRepo;
+ this.curator = curator;
+ this.zone = zone;
+ }
+
+ /**
+ * Prepares a session (validates, builds model, writes to zookeeper and distributes files)
+ *
+ * @param context Contains classes needed to read/write session data.
+ * @param logger For storing logs returned in response to client.
+ * @param params parameters controlling behaviour of prepare.
+ * @param currentActiveApplicationSet Set of currently active applications.
+ * @param tenantPath Zookeeper path for the tenant for this session
+ * @return The config change actions that must be done to handle the activation of the models prepared.
+ */
+ public ConfigChangeActions prepare(SessionContext context, DeployLogger logger, PrepareParams params,
+ Optional<ApplicationSet> currentActiveApplicationSet, Path tenantPath)
+ {
+ Preparation prep = new Preparation(context, logger, params, currentActiveApplicationSet, tenantPath);
+ prep.preprocess();
+ try {
+ prep.buildModels();
+ prep.makeResult();
+ if (!params.isDryRun()) {
+ prep.writeStateZK();
+ prep.writeRotZK();
+ prep.distribute();
+ prep.reloadDeployFileDistributor();
+ }
+ return prep.result();
+ } catch (IllegalArgumentException e) {
+ throw new InvalidApplicationException("Invalid application package", e);
+ }
+ }
+
+ private class Preparation {
+
+ final SessionContext context;
+ final DeployLogger logger;
+ final PrepareParams params;
+
+ final Optional<ApplicationSet> currentActiveApplicationSet;
+ final Path tenantPath;
+ final ApplicationId applicationId;
+ final RotationsCache rotationsCache;
+ final Set<Rotation> rotations;
+ final ModelContext.Properties properties;
+
+ private ApplicationPackage applicationPackage;
+ private List<PreparedModelsBuilder.PreparedModelResult> modelResultList;
+ private PrepareResult prepareResult;
+
+ private final PreparedModelsBuilder preparedModelsBuilder;
+
+ Preparation(SessionContext context, DeployLogger logger, PrepareParams params,
+ Optional<ApplicationSet> currentActiveApplicationSet, Path tenantPath) {
+ this.context = context;
+ this.logger = logger;
+ this.params = params;
+ this.currentActiveApplicationSet = currentActiveApplicationSet;
+ this.tenantPath = tenantPath;
+
+ this.applicationId = params.getApplicationId();
+ this.rotationsCache = new RotationsCache(curator, tenantPath);
+ this.rotations = getRotations(params.rotations());
+ this.properties = new ModelContextImpl.Properties(params.getApplicationId(),
+ configserverConfig.multitenant(),
+ ConfigServerSpec.fromConfig(configserverConfig),
+ configserverConfig.hostedVespa(),
+ zone,
+ rotations);
+ this.preparedModelsBuilder = new PreparedModelsBuilder(modelFactoryRegistry,
+ permanentApplicationPackage,
+ configserverConfig,
+ configDefinitionRepo,
+ curator,
+ zone,
+ fileDistributionFactory,
+ hostProvisionerProvider,
+ context,
+ logger,
+ params,
+ currentActiveApplicationSet,
+ tenantPath);
+ }
+
+ void checkTimeout(String step) {
+ if (! params.getTimeoutBudget().hasTimeLeft()) {
+ String used = params.getTimeoutBudget().timesUsed();
+ throw new RuntimeException("prepare timed out "+used+" after "+step+" step: " + applicationId);
+ }
+ }
+
+ void preprocess() {
+ try {
+ this.applicationPackage = context.getApplicationPackage().preprocess(
+ properties.zone(),
+ null,
+ logger);
+ } catch (IOException | TransformerException | ParserConfigurationException | SAXException e) {
+ throw new RuntimeException("Error deploying application package", e);
+ }
+ checkTimeout("preprocess");
+ }
+
+ void buildModels() {
+ this.modelResultList = preparedModelsBuilder.buildModels(applicationId, applicationPackage);
+ checkTimeout("build models");
+ }
+
+ void makeResult() {
+ this.prepareResult = new PrepareResult(modelResultList);
+ checkTimeout("making result from models");
+ }
+
+ void writeStateZK() {
+ log.log(LogLevel.DEBUG, "Writing application package state to zookeeper");
+ writeStateToZooKeeper(context.getSessionZooKeeperClient(), applicationPackage, params, logger,
+ prepareResult.getFileRegistries(), prepareResult.getProvisionInfos());
+ checkTimeout("write state to zookeeper");
+ }
+
+ void writeRotZK() {
+ rotationsCache.writeRotationsToZooKeeper(applicationId, rotations);
+ checkTimeout("write rotations to zookeeper");
+ }
+
+ void distribute() {
+ prepareResult.asList().forEach(modelResult -> modelResult.model
+ .distributeFiles(modelResult.fileDistributionProvider.getFileDistribution()));
+ checkTimeout("distribute files");
+ }
+
+ void reloadDeployFileDistributor() {
+ if (prepareResult.asList().isEmpty()) return;
+ PreparedModelsBuilder.PreparedModelResult aModelResult = prepareResult.asList().get(0);
+ aModelResult.model.reloadDeployFileDistributor(aModelResult.fileDistributionProvider.getFileDistribution());
+ checkTimeout("reload all deployed files in file distributor");
+ }
+
+ ConfigChangeActions result() {
+ return prepareResult.getConfigChangeActions();
+ }
+
+ private Set<Rotation> getRotations(Set<Rotation> rotations) {
+ if (rotations == null || rotations.isEmpty()) {
+ rotations = rotationsCache.readRotationsFromZooKeeper(applicationId);
+ }
+ return rotations;
+ }
+
+ }
+
+ private void writeStateToZooKeeper(SessionZooKeeperClient zooKeeperClient,
+ ApplicationPackage applicationPackage,
+ PrepareParams prepareParams,
+ DeployLogger deployLogger,
+ Map<Version, FileRegistry> fileRegistryMap,
+ Map<Version, ProvisionInfo> provisionInfoMap) {
+ ZooKeeperDeployer zkDeployer = zooKeeperClient.createDeployer(deployLogger);
+ try {
+ zkDeployer.deploy(applicationPackage, fileRegistryMap, provisionInfoMap);
+ zooKeeperClient.writeApplicationId(prepareParams.getApplicationId());
+ } catch (RuntimeException | IOException e) {
+ zkDeployer.cleanup();
+ throw new RuntimeException("Error preparing session", e);
+ }
+ }
+
+ /** The result of preparation over all model versions */
+ private static class PrepareResult {
+
+ private final ImmutableList<PreparedModelsBuilder.PreparedModelResult> results;
+
+ public PrepareResult(List<PreparedModelsBuilder.PreparedModelResult> results) {
+ this.results = ImmutableList.copyOf(results);
+ }
+
+ /** Returns the results for each model as an immutable list */
+ public List<PreparedModelsBuilder.PreparedModelResult> asList() { return results; }
+
+ public Map<Version, ProvisionInfo> getProvisionInfos() {
+ return results.stream()
+ .filter(result -> result.model.getProvisionInfo().isPresent())
+ .collect(Collectors.toMap((prepareResult -> prepareResult.version),
+ (prepareResult -> prepareResult.model.getProvisionInfo().get())));
+ }
+
+ public Map<Version, FileRegistry> getFileRegistries() {
+ return results.stream()
+ .collect(Collectors.toMap((prepareResult -> prepareResult.version),
+ (prepareResult -> prepareResult.fileDistributionProvider.getFileRegistry())));
+ }
+
+ /**
+ * Collects the config change actions from all model factory creations and returns the aggregated union of these actions.
+ * A system in the process of upgrading Vespa will have hosts running both version X and Y, and this will change
+ * during the upgrade process. Trying to be smart about which actions to perform on which hosts depending
+ * on the version running will be a nightmare to maintain. A pragmatic approach is therefore to just use the
+ * union of all actions as this will give the correct end result at the cost of perhaps restarting nodes twice
+ * (once for the upgrading case and once for a potential restart action).
+ */
+ public ConfigChangeActions getConfigChangeActions() {
+ return new ConfigChangeActions(results.stream().
+ map(result -> result.actions).
+ flatMap(actions -> actions.stream()).
+ collect(Collectors.toList()));
+ }
+
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepo.java
new file mode 100644
index 00000000000..872e6117637
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepo.java
@@ -0,0 +1,78 @@
+// 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.server.session;
+
+import com.yahoo.vespa.config.server.TimeoutBudget;
+import com.yahoo.vespa.config.server.http.NotFoundException;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+
+/**
+ * A generic session repository that can store any type of session that extends the abstract interface.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionRepo<SESSIONTYPE extends Session> {
+
+ private final HashMap<Long, SESSIONTYPE> sessions = new HashMap<>();
+
+ public synchronized void addSession(SESSIONTYPE session) {
+ final long sessionId = session.getSessionId();
+ if (sessions.containsKey(sessionId)) {
+ throw new IllegalArgumentException("There already exists a session with id '" + sessionId + "'");
+ }
+ sessions.put(sessionId, session);
+ }
+
+ public synchronized void removeSession(long id) {
+ if ( ! sessions.containsKey(id)) {
+ throw new IllegalArgumentException("No such session exists '" + id + "'");
+ }
+ sessions.remove(id);
+ }
+
+ /**
+ * Gets a Session
+ *
+ * @param id session id
+ * @return a session belonging to the id supplied, or null if no session with the id was found
+ */
+ public synchronized SESSIONTYPE getSession(long id) {
+ return sessions.get(id);
+ }
+
+ /**
+ * Gets a Session with a timeout
+ *
+ * @param id session id
+ * @param timeoutInMillis timeout for getting session (loops and wait for session to show up if not found)
+ * @return a session belonging to the id supplied, or null if no session with the id was found
+ */
+ public synchronized SESSIONTYPE getSession(long id, long timeoutInMillis) {
+ try {
+ return internalGetSession(id, timeoutInMillis);
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while retrieving session with id " + id);
+ }
+ }
+
+ private synchronized SESSIONTYPE internalGetSession(long id, long timeoutInMillis) throws InterruptedException {
+ TimeoutBudget timeoutBudget = new TimeoutBudget(Clock.systemUTC(), Duration.ofMillis(timeoutInMillis));
+ do {
+ SESSIONTYPE session = getSession(id);
+ if (session != null) {
+ return session;
+ }
+ wait(100);
+ } while (timeoutBudget.hasTimeLeft());
+ throw new NotFoundException("Unable to retrieve session with id " + id + " before timeout was reached");
+ }
+
+ public synchronized Collection<SESSIONTYPE> listSessions() {
+ return new ArrayList<>(sessions.values());
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionStateWatcher.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionStateWatcher.java
new file mode 100644
index 00000000000..37dff639a35
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionStateWatcher.java
@@ -0,0 +1,82 @@
+// 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.server.session;
+
+import com.yahoo.concurrent.ThreadFactoryFactory;
+import com.yahoo.log.LogLevel;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.config.server.ReloadHandler;
+import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.curator.Curator;
+import org.apache.curator.framework.recipes.cache.ChildData;
+import org.apache.curator.framework.recipes.cache.NodeCacheListener;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.logging.Logger;
+
+/**
+ * Watches one particular session (/vespa/config/apps/n/sessionState in ZK)
+ * The session must be in the session repo.
+ *
+ * @author vegardh
+ */
+public class SessionStateWatcher implements NodeCacheListener {
+
+ private static final Logger log = Logger.getLogger(SessionStateWatcher.class.getName());
+ private final Curator.FileCache fileCache;
+ private final ReloadHandler reloadHandler;
+ private final RemoteSession session;
+ private final MetricUpdater metrics;
+ private final Executor executor;
+
+ public SessionStateWatcher(Curator.FileCache fileCache, ReloadHandler reloadHandler, RemoteSession session, MetricUpdater metrics) throws Exception {
+ executor = Executors.newSingleThreadExecutor(ThreadFactoryFactory.getThreadFactory(SessionStateWatcher.class.getName() + "-" + session));
+ this.fileCache = fileCache;
+ this.reloadHandler = reloadHandler;
+ this.session = session;
+ this.metrics = metrics;
+ this.fileCache.start();
+ this.fileCache.addListener(this);
+ }
+
+ private void sessionChanged(Session.Status status) {
+ log.log(LogLevel.DEBUG, session.logPre()+"Session change: Session " + session.getSessionId() + " changed status to " + status);
+
+ // valid for NEW -> PREPARE transitions, not ACTIVATE -> PREPARE.
+ if (status.equals(Session.Status.PREPARE)) {
+ log.log(LogLevel.DEBUG, session.logPre() + "Loading prepared session: " + session.getSessionId());
+ session.loadPrepared();
+ } else if (status.equals(Session.Status.ACTIVATE)) {
+ session.makeActive(reloadHandler);
+ } else if (status.equals(Session.Status.DEACTIVATE)) {
+ session.deactivate();
+ }
+ }
+
+ public long getSessionId() {
+ return session.getSessionId();
+ }
+
+ public void close() {
+ try {
+ fileCache.close();
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, "Exception when closing watcher", e);
+ }
+ }
+
+ @Override
+ public void nodeChanged() throws Exception {
+ executor.execute(() -> {
+ try {
+ ChildData data = fileCache.getCurrentData();
+ if (data != null) {
+ sessionChanged(Session.Status.parse(Utf8.toString(fileCache.getCurrentData().getData())));
+ }
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, session.logPre() + "Error handling session changed for session " + getSessionId(), e);
+ metrics.incSessionChangeErrors();
+ }
+ });
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java
new file mode 100644
index 00000000000..e17667c2a5a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java
@@ -0,0 +1,213 @@
+// 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.server.session;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.transaction.Transaction;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.config.server.StaticConfigDefinitionRepo;
+import com.yahoo.vespa.config.server.ServerCache;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.config.server.deploy.ZooKeeperClient;
+import com.yahoo.vespa.config.server.deploy.ZooKeeperDeployer;
+import com.yahoo.vespa.config.server.zookeeper.ZKApplicationPackage;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.curator.transaction.CuratorOperations;
+import com.yahoo.vespa.curator.transaction.CuratorTransaction;
+import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Zookeeper client for a specific session. Can be used to read and write session status
+ * and create and get prepare and active barrier.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionZooKeeperClient {
+
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(SessionZooKeeperClient.class.getName());
+ static final String APPLICATION_ID_PATH = "applicationId";
+ static final String CREATE_TIME_PATH = "createTime";
+ private final Curator curator;
+ private final ConfigCurator configCurator;
+ private final Path rootPath;
+ private final Path sessionStatusPath;
+ private final String serverId;
+ private final ServerCacheLoader cacheLoader;
+
+ // Only for testing when cache loader does not need cache entries.
+ public SessionZooKeeperClient(Curator curator, Path rootPath) {
+ this(curator, ConfigCurator.create(curator), rootPath, new StaticConfigDefinitionRepo(), "");
+ }
+
+ public SessionZooKeeperClient(Curator curator, ConfigCurator configCurator, Path rootPath, ConfigDefinitionRepo definitionRepo, String serverId) {
+ this.curator = curator;
+ this.configCurator = configCurator;
+ this.rootPath = rootPath;
+ this.serverId = serverId;
+ this.sessionStatusPath = rootPath.append(ConfigCurator.SESSIONSTATE_ZK_SUBPATH);
+ this.cacheLoader = new ServerCacheLoader(configCurator, rootPath, definitionRepo);
+ }
+
+ public void writeStatus(Session.Status sessionStatus) {
+ try {
+ createWriteStatusTransaction(sessionStatus).commit();
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to write session status", e);
+ }
+ }
+
+ public Session.Status readStatus() {
+ try {
+ String data = configCurator.getData(sessionStatusPath.getAbsolute());
+ return Session.Status.parse(data);
+ } catch (Exception e) {
+ log.log(LogLevel.INFO, "Unable to read session status, assuming it was deleted");
+ return Session.Status.NONE;
+ }
+ }
+
+ Curator.CompletionWaiter createPrepareWaiter() {
+ return createCompletionWaiter(PREPARE_BARRIER);
+ }
+
+ Curator.CompletionWaiter createActiveWaiter() {
+ return createCompletionWaiter(ACTIVE_BARRIER);
+ }
+
+ Curator.CompletionWaiter getPrepareWaiter() {
+ return getCompletionWaiter(getWaiterPath(PREPARE_BARRIER));
+ }
+
+ Curator.CompletionWaiter getActiveWaiter() {
+ return getCompletionWaiter(getWaiterPath(ACTIVE_BARRIER));
+ }
+
+ Curator.CompletionWaiter getUploadWaiter() {
+ return getCompletionWaiter(getWaiterPath(UPLOAD_BARRIER));
+ }
+
+ private static final String PREPARE_BARRIER = "prepareBarrier";
+ private static final String ACTIVE_BARRIER = "activeBarrier";
+ private static final String UPLOAD_BARRIER = "uploadBarrier";
+
+ private Path getWaiterPath(String barrierName) {
+ return rootPath.append(barrierName);
+ }
+
+ private int getNumberOfMembers() {
+ /*
+ * The number of members required in a barrier is the majority of servers.
+ */
+ int numServers = curator.serverCount();
+ return (numServers / 2) + 1;
+ }
+
+ private Curator.CompletionWaiter createCompletionWaiter(String waiterNode) {
+ return curator.createCompletionWaiter(rootPath, waiterNode, getNumberOfMembers(), serverId);
+ }
+
+ private Curator.CompletionWaiter getCompletionWaiter(Path path) {
+ return curator.getCompletionWaiter(path, getNumberOfMembers(), serverId);
+ }
+
+ public void delete() {
+ try {
+ log.log(LogLevel.DEBUG, "Deleting " + rootPath.getAbsolute());
+ configCurator.deleteRecurse(rootPath.getAbsolute());
+ } catch (RuntimeException e) {
+ log.log(LogLevel.INFO, "Error deleting session (" + rootPath.getAbsolute() + ") from zookeeper");
+ }
+ }
+
+ public ApplicationPackage loadApplicationPackage() {
+ return new ZKApplicationPackage(configCurator, rootPath);
+ }
+
+ public ServerCache loadServerCache() {
+ return cacheLoader.loadCache();
+ }
+
+ public void writeApplicationId(ApplicationId id) {
+ String path = getApplicationIdPath();
+ try {
+ configCurator.putData(path, id.serializedForm());
+ } catch (RuntimeException e) {
+ throw new RuntimeException("Unable to write application id '" + id + "' to '" + path + "'", e);
+ }
+ }
+
+ private String getApplicationIdPath() {
+ return rootPath.append(APPLICATION_ID_PATH).getAbsolute();
+ }
+
+ public ApplicationId readApplicationId(TenantName tenant) {
+ String path = getApplicationIdPath();
+ try {
+ // Fallback for cases where id never existed.
+ if ( ! configCurator.exists(path)) {
+ // TODO: DEBUG LOG
+ log.log(LogLevel.INFO, "Unable to locate application id at '" + path + "', returning default");
+ return ApplicationId.defaultId();
+ }
+ return ApplicationId.fromSerializedForm(tenant, configCurator.getData(path));
+ } catch (RuntimeException e) {
+ throw new RuntimeException("Unable to read application id from '" + path + "'", e);
+ }
+ }
+
+ // in seconds
+ public long readCreateTime() {
+ String path = getCreateTimePath();
+ if (!configCurator.exists(path)) return 0l;
+ return Long.parseLong(configCurator.getData(path));
+ }
+
+ private String getCreateTimePath() {
+ return rootPath.append(CREATE_TIME_PATH).getAbsolute();
+ }
+
+ ProvisionInfo getProvisionInfo() {
+ return loadApplicationPackage().getProvisionInfoMap().values().stream()
+ .reduce((infoA, infoB) -> infoA.merge(infoB))
+ .orElseThrow(() -> new IllegalStateException("Trying to read provision info, but no provision info exists"));
+ }
+
+ public ZooKeeperDeployer createDeployer(DeployLogger logger) {
+ ZooKeeperClient zkClient = new ZooKeeperClient(configCurator, logger, true, rootPath);
+ return new ZooKeeperDeployer(zkClient);
+ }
+
+ public Transaction createWriteStatusTransaction(Session.Status status) {
+ String path = sessionStatusPath.getAbsolute();
+ CuratorTransaction transaction = new CuratorTransaction(curator);
+ if (configCurator.exists(path)) {
+ transaction.add(CuratorOperations.setData(sessionStatusPath.getAbsolute(), Utf8.toBytes(status.name())));
+ } else {
+ transaction.add(CuratorOperations.create(sessionStatusPath.getAbsolute(), Utf8.toBytes(status.name())));
+ }
+ return transaction;
+ }
+
+ /**
+ * Create necessary paths atomically for a new session.
+ * @param createTime Time of session creation.
+ * @param timeUnit Time unit of createTime.
+ */
+ public void createNewSession(long createTime, TimeUnit timeUnit) {
+ CuratorTransaction transaction = new CuratorTransaction(curator);
+ transaction.add(CuratorOperations.create(rootPath.getAbsolute()));
+ transaction.add(CuratorOperations.create(rootPath.append(UPLOAD_BARRIER).getAbsolute()));
+ transaction.add(createWriteStatusTransaction(Session.Status.NEW).operations());
+ transaction.add(CuratorOperations.create(getCreateTimePath(), Utf8.toBytes(String.valueOf(timeUnit.toSeconds(createTime)))));
+ transaction.commit();
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SilentDeployLogger.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SilentDeployLogger.java
new file mode 100644
index 00000000000..9121eab8d2f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SilentDeployLogger.java
@@ -0,0 +1,25 @@
+// 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.server.session;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.yahoo.config.application.api.DeployLogger;
+
+/**
+ * The purpose of this is to mute the log messages from model and application building in {@link RemoteSession} that is triggered by {@link SessionStateWatcher}, since those messages already
+ * have been emitted by the prepare handler, for the same prepare operation.
+ *
+ * @author vegardh
+ *
+ */
+public class SilentDeployLogger implements DeployLogger {
+
+ private static final Logger log = Logger.getLogger(SilentDeployLogger.class.getName());
+
+ @Override
+ public void log(Level level, String message) {
+ log.log(Level.FINE, message);
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/version/VersionState.java b/configserver/src/main/java/com/yahoo/vespa/config/server/version/VersionState.java
new file mode 100644
index 00000000000..2a09e46d821
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/version/VersionState.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.server.version;
+
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.provision.Version;
+import com.yahoo.io.IOUtils;
+import com.yahoo.vespa.defaults.Defaults;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+
+/**
+ * Contains version information for this configserver.
+ *
+ * @author lulf
+ */
+public class VersionState {
+
+ private final File versionFile;
+
+ @Inject
+ public VersionState(ConfigserverConfig config) {
+ this(new File(Defaults.getDefaults().underVespaHome(config.configServerDBDir()), "vespa_version"));
+ }
+
+ public VersionState(File versionFile) {
+ this.versionFile = versionFile;
+ }
+
+ public boolean isUpgraded() {
+ return currentVersion().compareTo(storedVersion()) > 0;
+ }
+
+ public void saveNewVersion() {
+ try (FileWriter writer = new FileWriter(versionFile)) {
+ writer.write(currentVersion().toSerializedForm());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Version storedVersion() {
+ try (FileReader reader = new FileReader(versionFile)) {
+ return Version.fromString(IOUtils.readAll(reader));
+ } catch (Exception e) {
+ return Version.fromIntValues(0, 0, 0); // Use an old value to signal we don't know
+ }
+ }
+
+ public Version currentVersion() {
+ return Version.fromIntValues(VespaVersion.major, VespaVersion.minor, VespaVersion.micro);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Current version:%s, stored version:%s", currentVersion(), storedVersion());
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ConfigCurator.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ConfigCurator.java
new file mode 100644
index 00000000000..af8d2fed95c
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ConfigCurator.java
@@ -0,0 +1,429 @@
+// 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.server.zookeeper;
+
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ZookeeperServerConfig;
+import com.yahoo.io.IOUtils;
+import com.yahoo.log.LogLevel;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.zookeeper.ZooKeeperServer;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A (stateful) curator wrapper for the config server. This simplifies Curator method calls used by the config server
+ * and knows about how config content is mapped to node names and stored.
+ * <p>
+ * Usage details:
+ * Config ids are stored as foo#bar#c0 instead of foo/bar/c0, for simplicity.
+ * Keep the amount of domain-specific logic here to a minimum.
+ * Data for one application x is stored on this form:
+ * /vespa/config/apps/x/defconfigs
+ * /vespa/config/apps/x/userapp
+ * The different types of configs are stored on this form (ie. names of the ZK nodes under their respective
+ * paths):
+ * <p>
+ * Def configs are stored on the form name,version
+ * The user application structure is exactly the same as in the user's app dir during deploy.
+ * The current live app id (for example x) is stored in the node /vespa/config/liveapp
+ * It is updated outside this class, typically in config server during reload-config.
+ * Some methods have retries and/or reconnect. This is necessary because ZK will throw on certain scenarios,
+ * even though it will recover from it itself, @see http://wiki.apache.org/hadoop/ZooKeeper/ErrorHandling
+ *
+ * @author vegardh
+ * @author bratseth
+ * @since 5.0
+ */
+public class ConfigCurator {
+
+ /** Path for def files, under one app */
+ public static final String DEFCONFIGS_ZK_SUBPATH = "/defconfigs";
+
+ /** Path for def files, under one app */
+ public static final String USER_DEFCONFIGS_ZK_SUBPATH = "/userdefconfigs";
+
+ /** Path for metadata about an application */
+ public static final String META_ZK_PATH = "/meta";
+
+ /** Path for the app package's dir structure, under one app */
+ public static final String USERAPP_ZK_SUBPATH = "/userapp";
+
+ /** Path for session state */
+ public static final String SESSIONSTATE_ZK_SUBPATH = "/sessionState";
+
+ protected static final FilenameFilter acceptsAllFileNameFilter = (dir, name) -> true;
+
+ private final Curator curator;
+
+ public static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(ConfigCurator.class.getName());
+
+ /** The number of zookeeper operations done with this ZKFacade instance */
+ private final AtomicInteger operations = new AtomicInteger();
+
+ /** The number of zookeeper read operations done with this ZKFacade instance */
+ private final AtomicInteger readOperations = new AtomicInteger();
+
+ /** The number of zookeeper write operations done with this ZKFacade instance */
+ private final AtomicInteger writeOperations = new AtomicInteger();
+
+ /** The maximum size of a ZooKeeper node */
+ private final int maxNodeSize;
+
+ /**
+ * Sets up thread local zk access if not done before and returns a facade object
+ *
+ * @return a ZKFacade object
+ */
+ public static ConfigCurator create(Curator curator, int juteMaxBuffer) {
+ return new ConfigCurator(curator, juteMaxBuffer);
+ }
+
+ public static ConfigCurator create(Curator curator) {
+ return new ConfigCurator(curator, 1024*1024*10);
+ }
+
+ @Inject
+ public ConfigCurator(Curator curator, ZooKeeperServer server) {
+ this(curator, server.getConfig().juteMaxBuffer());
+ }
+
+ private ConfigCurator(Curator curator, int maxNodeSize) {
+ this.curator = curator;
+ this.maxNodeSize = maxNodeSize;
+ log.log(LogLevel.CONFIG, "Using jute max buffer size " + this.maxNodeSize);
+ testZkConnection();
+ }
+
+ /** Returns the curator instance this wraps */
+ public Curator curator() { return curator; }
+
+ /** Cleans and creates a zookeeper completely */
+ public void initAndClear(String path) {
+ try {
+ operations.incrementAndGet();
+ readOperations.incrementAndGet();
+ if (exists(path))
+ deleteRecurse(path);
+ createRecurse(path);
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Exception clearing path " + path + " in ZooKeeper", e);
+ }
+ }
+
+ /** Creates a path. If the path already exists this does nothing. */
+ private void createRecurse(String path) {
+ try {
+ if (exists(path)) return;
+ curator.framework().create().creatingParentsIfNeeded().forPath(path);
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Exception creating path " + path + " in ZooKeeper", e);
+ }
+ }
+
+ /** Returns the data at a path and node. Replaces / by # in node names. Returns null if the path doesn't exist. */
+ public String getData(String path, String node) {
+ return getData(createFullPath(path, node));
+ }
+
+ /** Returns the data at a path. Returns null if the path doesn't exist. */
+ public String getData(String path) {
+ byte[] data = getBytes(path);
+ return (data == null) ? null : Utf8.toString(data);
+ }
+
+ /** Returns the data at a path and node. Replaces / by # in node names. Returns null if the path doesn't exist. */
+ public byte[] getBytes(String path, String node) {
+ return getBytes(createFullPath(path, node));
+ }
+
+ /**
+ * Returns the data at a path, or null if the path does not exist.
+ *
+ * @param path a String with a pathname.
+ * @return a byte array with data.
+ */
+ public byte[] getBytes(String path) {
+ try {
+ if ( ! exists(path)) return null; // TODO: Ugh
+ operations.incrementAndGet();
+ readOperations.incrementAndGet();
+ return curator.framework().getData().forPath(path);
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Exception reading from path " + path + " in ZooKeeper", e);
+ }
+ }
+
+ /** Returns whether a path exists in zookeeper */
+ public boolean exists(String path, String node) {
+ return exists(createFullPath(path, node));
+ }
+
+ /** Returns whether a path exists in zookeeper */
+ public boolean exists(String path) {
+ try {
+ return curator.framework().checkExists().forPath(path) != null;
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Exception checking existence of path " + path + " in ZooKeeper", e);
+ }
+ }
+
+ /** Creates a Zookeeper node. If the node already exists this does nothing. */
+ public void createNode(String path) {
+ if ( ! exists(path))
+ createRecurse(path);
+ operations.incrementAndGet();
+ writeOperations.incrementAndGet();
+ }
+
+ /** Creates a Zookeeper node synchronously. Replaces / by # in node names. */
+ public void createNode(String path, String node) {
+ createNode(createFullPath(path, node));
+ }
+
+ private String createFullPath(String path, String node) {
+ return path + "/" + toConfigserverName(node);
+ }
+
+ /** Sets data at a given path and name. Replaces / by # in node names. Creates the node if it doesn't exist */
+ public void putData(String path, String node, String data) {
+ putData(path, node, Utf8.toBytes(data));
+ }
+
+ /** Sets data at a given path. Creates the node if it doesn't exist */
+ public void putData(String path, String data) {
+ putData(path, Utf8.toBytes(data));
+ }
+
+ private void ensureDataIsNotTooLarge(byte[] toPut, String path) {
+ if (toPut.length >= maxNodeSize) {
+ throw new IllegalArgumentException("Error: too much zookeeper data in node: "
+ + "[" + toPut.length + " bytes] (path " + path + ")");
+ }
+ }
+
+ /** Sets data at a given path and name. Replaces / by # in node names. Creates the node if it doesn't exist */
+ public void putData(String path, String node, byte[] data) {
+ putData(createFullPath(path, node), data);
+ }
+
+ /** Sets data at a given path. Creates the path if it doesn't exist */
+ public void putData(String path, byte[] data) {
+ try {
+ ensureDataIsNotTooLarge(data, path);
+ operations.incrementAndGet();
+ writeOperations.incrementAndGet();
+ if (exists(path))
+ curator.framework().setData().forPath(path, data);
+ else
+ curator.framework().create().creatingParentsIfNeeded().forPath(path, data);
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Exception writing to path " + path + " in ZooKeeper", e);
+ }
+ }
+
+ /** Sets data at an existing node. Replaces / by # in node names. */
+ public void setData(String path, String node, String data) {
+ setData(path, node, Utf8.toBytes(data));
+ }
+
+ /** Sets data at an existing node. Replaces / by # in node names. */
+ public void setData(String path, String node, byte[] data) {
+ setData(createFullPath(path, node), data);
+ }
+
+ /** Sets data at an existing node. Replaces / by # in node names. */
+ public void setData(String path, byte[] data) {
+ try {
+ ensureDataIsNotTooLarge(data, path);
+ operations.incrementAndGet();
+ writeOperations.incrementAndGet();
+ curator.framework().setData().forPath(path, data);
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Exception writing to path " + path + " in ZooKeeper", e);
+ }
+ }
+
+ /**
+ * Replaces / with # in the given node.
+ *
+ * @param node a zookeeper node name
+ * @return a config server node name
+ */
+ protected String toConfigserverName(String node) {
+ if (node.startsWith("/")) node = node.substring(1);
+ return node.replaceAll("/", "#");
+ }
+
+ /**
+ * Lists thh children at the given path.
+ *
+ * @return the local names of the children at this path, or an empty list (never null) if none.
+ */
+ public List<String> getChildren(String path) {
+ try {
+ operations.incrementAndGet();
+ readOperations.incrementAndGet();
+ return curator.framework().getChildren().forPath(path);
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Exception getting children of path " + path + " in ZooKeeper", e);
+ }
+ }
+
+ /**
+ * Puts config definition data and metadata into ZK.
+ *
+ * @param name The config definition name (including namespace)
+ * @param version The config definition version
+ * @param path /zoopath
+ * @param data The contents to write to ZK (as a byte array)
+ */
+ public void putDefData(String name, String version, String path, byte[] data) {
+ if (version == null) {
+ putData(path, name, data);
+ } else {
+ String fullPath = createFullPath(path, name + "," + version);
+ if (exists(fullPath)) {
+ // TODO This should not happen when all the compatibility hacks in 5.1 have been removed
+ log.log(LogLevel.INFO, "There already exists a config definition '" + name + "', skipping feeding this one to ZooKeeper");
+ }
+ else {
+ putData(fullPath, data);
+ }
+ }
+ }
+
+ /**
+ * Takes for instance the dir /app and puts the contents into the given ZK path. Ignores files starting with dot,
+ * and dirs called CVS.
+ *
+ * @param dir directory which holds the summary class part files
+ * @param path zookeeper path
+ * @param filenameFilter A FilenameFilter which decides which files in dir are fed to zookeeper
+ * @param recurse recurse subdirectories
+ */
+ public void feedZooKeeper(File dir, String path, FilenameFilter filenameFilter, boolean recurse) {
+ try {
+ if (filenameFilter == null) {
+ filenameFilter = acceptsAllFileNameFilter;
+ }
+ if (!dir.isDirectory()) {
+ log.fine(dir.getCanonicalPath() + " is not a directory. Not feeding the files into ZooKeeper.");
+ return;
+ }
+ for (File file : listFiles(dir, filenameFilter)) {
+ if (file.getName().startsWith(".")) continue; //.svn , .git ...
+ if ("CVS".equals(file.getName())) continue;
+ if (file.isFile()) {
+ byte[] contents = IOUtils.readFileBytes(file);
+ putData(path, file.getName(), contents);
+ } else if (recurse && file.isDirectory()) {
+ createNode(path, file.getName());
+ feedZooKeeper(file, path + '/' + file.getName(), filenameFilter, recurse);
+ }
+ }
+ }
+ catch (IOException e) {
+ throw new RuntimeException("Exception feeding ZooKeeper at path " + path, e);
+ }
+ }
+
+ /**
+ * Same as normal listFiles, but use the filter only for normal files
+ *
+ * @param dir directory to list files in
+ * @param filter A FilenameFilter which decides which files in dir are listed
+ * @return an array of Files
+ */
+ protected File[] listFiles(File dir, FilenameFilter filter) {
+ File[] rawList = dir.listFiles();
+ List<File> ret = new ArrayList<>();
+ if (rawList != null) {
+ for (File f : rawList) {
+ if (f.isDirectory()) {
+ ret.add(f);
+ } else {
+ if (filter.accept(dir, f.getName())) {
+ ret.add(f);
+ }
+ }
+ }
+ }
+ return ret.toArray(new File[ret.size()]);
+ }
+
+ /**
+ * The node string for a given config and path. Ignores id and/or version if the path is that of a
+ * data set that doesn't use id or version.
+ *
+ * @param path a zookeeper path
+ * @param name config definition name
+ * @param version config definition version
+ * @param id config id
+ * @return a String with path to a zookeeper node for a given config and path
+ */
+ public static String getZkNodePath(String path, String name, String version, String id) {
+ if (path.endsWith(DEFCONFIGS_ZK_SUBPATH) || path.endsWith(USER_DEFCONFIGS_ZK_SUBPATH))
+ return getConfigNodeName(name, version);
+ throw new IllegalArgumentException("Don't know how data in " + path + " is organised in ZK");
+ }
+
+ public static String getConfigNodeName(String name, String version) {
+ return getConfigNodeName(name, version, null);
+ }
+
+ public static String getConfigNodeName(String name, String version, String configId) {
+ if (configId == null || "".equals(configId))
+ return name + "," + version;
+ return name + "," + version + "," + configId;
+ }
+
+ /** Deletes the node at the given path, and any children it may have. If the node does not exist this does nothing */
+ public void deleteRecurse(String path) {
+ try {
+ if ( ! exists(path)) return;
+ curator.framework().delete().deletingChildrenIfNeeded().forPath(path);
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Exception deleting path " + path, e);
+ }
+ }
+
+ public Integer getNumberOfOperations() { return operations.intValue(); }
+
+ public Integer getNumberOfReadOperations() {
+ return readOperations.intValue();
+ }
+
+ public Integer getNumberOfWriteOperations() {
+ return writeOperations.intValue();
+ }
+
+
+ private void testZkConnection() { // This is not necessary, but allows us to give a useful error message
+ if (curator.connectionSpec().isEmpty()) return;
+ try {
+ curator.framework().checkExists().forPath("/dummy");
+ }
+ catch (Exception e) {
+ log.log(LogLevel.ERROR, "Unable to contact ZooKeeper on " + curator.connectionSpec() +
+ ". Please verify for all configserver nodes that " +
+ "services.addr_configserver points to the correct configserver(s), " +
+ "the same configserver(s) as in services.xml, and that they are started. " +
+ "Check the log(s) for configserver errors. Aborting.", e);
+ }
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/InitializedCounter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/InitializedCounter.java
new file mode 100644
index 00000000000..20572b938ba
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/InitializedCounter.java
@@ -0,0 +1,88 @@
+// 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.server.zookeeper;
+
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.curator.recipes.CuratorCounter;
+import com.yahoo.vespa.curator.Curator;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A counter that sets its initial value to the number of apps in zookeeper if no counter value is set. Subclass
+ * this to get that behavior.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class InitializedCounter {
+
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(InitializedCounter.class.getName());
+ protected final CuratorCounter counter;
+ private final String sessionsDirPath;
+
+ public InitializedCounter(Curator curator, String counterPath, String sessionsDirPath) {
+ this.sessionsDirPath = sessionsDirPath;
+ this.counter = new CuratorCounter(curator, counterPath);
+ initializeCounterValue(getLatestSessionId(ConfigCurator.create(curator), sessionsDirPath));
+ }
+
+ private void initializeCounterValue(Long latestSessionId) {
+ log.log(LogLevel.DEBUG, "path=" + sessionsDirPath + ", current=" + latestSessionId);
+ if (latestSessionId != null) {
+ counter.initialize(latestSessionId);
+ } else {
+ counter.initialize(1);
+ }
+ }
+
+ /**
+ * Checks if an application exists in Zookeeper.
+ *
+ * @return true, if an application exists, false otherwise
+ */
+ private static boolean applicationExists(ConfigCurator configCurator, String appsPath) {
+ // TODO Need to try and catch now since interface should not expose Zookeeper exceptions
+ try {
+ return configCurator.exists(appsPath);
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Returns the application generation for the most recently deployed application from ZK,
+ * or null if no application has been deployed yet
+ *
+ * @return generation of the latest deployed application
+ */
+ private static Long getLatestSessionId(ConfigCurator configCurator, String appsPath) {
+ if (!applicationExists(configCurator, appsPath)) return null;
+ Long newestGeneration = null;
+ try {
+ if (!getDeployedApplicationGenerations(configCurator, appsPath).isEmpty()) {
+ newestGeneration = Collections.max(getDeployedApplicationGenerations(configCurator, appsPath));
+ }
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, "Could not get newest application generation from Zookeeper");
+ }
+ return newestGeneration;
+ }
+
+ private static List<Long> getDeployedApplicationGenerations(ConfigCurator configCurator, String appsPath) {
+ ArrayList<Long> generations = new ArrayList<>();
+ try {
+ List<String> stringGenerations = configCurator.getChildren(appsPath);
+ if (stringGenerations != null && !(stringGenerations.isEmpty())) {
+ for (String s : stringGenerations) {
+ generations.add(Long.parseLong(s));
+ }
+ }
+ } catch (RuntimeException e) {
+ log.log(LogLevel.WARNING, "Could not get application generations from Zookeeper");
+ }
+ return generations;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/SessionCounter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/SessionCounter.java
new file mode 100644
index 00000000000..cad164f8614
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/SessionCounter.java
@@ -0,0 +1,27 @@
+// 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.server.zookeeper;
+
+import com.yahoo.path.Path;
+import com.yahoo.vespa.curator.Curator;
+
+/**
+ * A counter keeping track of session ids in an atomic fashion across multiple config servers.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class SessionCounter extends InitializedCounter {
+
+ public SessionCounter(Curator curator, Path rootPath, Path sessionsDir) {
+ super(curator, rootPath.append("sessionCounter").getAbsolute(), sessionsDir.getAbsolute());
+ }
+
+ /**
+ * Atomically increment and return next session id.
+ *
+ * @return a new session id.
+ */
+ public long nextSessionId() {
+ return counter.next();
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationFile.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationFile.java
new file mode 100644
index 00000000000..4aedb487352
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationFile.java
@@ -0,0 +1,175 @@
+// 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.server.zookeeper;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.path.Path;
+import com.yahoo.io.IOUtils;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.util.ConfigUtils;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * @author lulf
+ * @author vegardh
+ * @since 5.1
+ */
+class ZKApplicationFile extends ApplicationFile {
+ private static final Logger log = Logger.getLogger("ZKApplicationFile");
+ private final ZKLiveApp zkApp;
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ public ZKApplicationFile(Path path, ZKLiveApp app) {
+ super(path);
+ this.zkApp = app;
+ }
+
+ @Override
+ public boolean isDirectory() {
+ String zkPath = getZKPath(path);
+ if (zkApp.exists(zkPath)) {
+ String data = zkApp.getData(zkPath);
+ if (data == null || data.isEmpty() || !zkApp.getChildren(zkPath).isEmpty()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean exists() {
+ try {
+ String zkPath = getZKPath(path);
+ return zkApp.exists(zkPath);
+ } catch (RuntimeException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public ApplicationFile delete() {
+ if (!listFiles().isEmpty()) {
+ throw new RuntimeException("Can't delete, directory not empty: " + this);
+ }
+ zkApp.deleteRecurse(getZKPath(path));
+ writeMetaFile(null, ContentStatusDeleted);
+ return this;
+ }
+
+ @Override
+ public Reader createReader() throws FileNotFoundException {
+ String zkPath = getZKPath(path);
+ String data = zkApp.getData(zkPath);
+ if (data == null) {
+ throw new FileNotFoundException("No such path: " + path);
+ }
+ return new StringReader(data);
+ }
+
+ @Override
+ public InputStream createInputStream() throws FileNotFoundException {
+ String zkPath = getZKPath(path);
+ byte[] data = zkApp.getBytes(zkPath);
+ if (data == null) {
+ throw new FileNotFoundException("No such path: " + path);
+ }
+ return new ByteArrayInputStream(data);
+ }
+
+ @Override
+ public ApplicationFile createDirectory() {
+ String zkPath = getZKPath(path);
+ if (isDirectory()) return this;
+ if (exists()) {
+ throw new IllegalArgumentException("Unable to create directory, file exists: " + path);
+ }
+ zkApp.create(zkPath);
+ writeMetaFile(null, ContentStatusNew);
+ return this;
+ }
+
+ @Override
+ public ApplicationFile writeFile(Reader input) {
+ // foo/bar/baz.txt
+ String zkPath = getZKPath(path);
+ try {
+ String data = IOUtils.readAll(input);
+ String status = ContentStatusNew;
+ if (zkApp.exists(zkPath)) {
+ status = ContentStatusChanged;
+ }
+ zkApp.putData(zkPath, data);
+ writeMetaFile(data, status);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public List<ApplicationFile> listFiles(PathFilter filter) {
+ String userPath = getZKPath(path);
+ List<ApplicationFile> ret = new ArrayList<>();
+ for (String zkChild : zkApp.getChildren(userPath)) {
+ Path childPath = path.append(zkChild);
+ // Ignore dot-files.
+ if (!childPath.getName().startsWith(".") && filter.accept(childPath)) {
+ ret.add(new ZKApplicationFile(childPath, zkApp));
+ }
+ }
+ return ret;
+ }
+
+ private static String getZKPath(Path path) {
+ if (path.isRoot()) {
+ return ConfigCurator.USERAPP_ZK_SUBPATH;
+ }
+ return ConfigCurator.USERAPP_ZK_SUBPATH + "/" + path.getRelative();
+ }
+
+ private void writeMetaFile(String input, String status) {
+ String metaPath = getZKPath(getMetaPath());
+ StringWriter writer = new StringWriter();
+ try {
+ mapper.writeValue(writer, new MetaData(status, input == null ? "" : ConfigUtils.getMd5(input)));
+ log.log(LogLevel.DEBUG, "Writing meta file to " + metaPath);
+ zkApp.putData(metaPath, writer.toString());
+ } catch (IOException e) {
+ throw new RuntimeException("Error writing meta file to " + metaPath, e);
+ }
+ }
+
+ public MetaData getMetaData() {
+ String metaPath = getZKPath(getMetaPath());
+ log.log(LogLevel.DEBUG, "Getting metadata for " + metaPath);
+ if (!zkApp.exists(getZKPath(path))) {
+ if (zkApp.exists(metaPath)) {
+ return getMetaDataFromZk(metaPath);
+ } else {
+ return null;
+ }
+ }
+ if (zkApp.exists(metaPath)) {
+ return getMetaDataFromZk(metaPath);
+ }
+ return new MetaData(ContentStatusNew, isDirectory() ? "" : ConfigUtils.getMd5(zkApp.getData(getZKPath(path))));
+ }
+
+ private MetaData getMetaDataFromZk(String metaPath) {
+ try {
+ return mapper.readValue(zkApp.getBytes(metaPath), MetaData.class);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public int compareTo(ApplicationFile other) {
+ if (other == this) return 0;
+ return this.getPath().getName().compareTo((other).getPath().getName());
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationPackage.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationPackage.java
new file mode 100644
index 00000000000..9061e47d134
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationPackage.java
@@ -0,0 +1,281 @@
+// 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.server.zookeeper;
+
+import com.google.common.base.Joiner;
+import com.yahoo.config.application.api.ApplicationMetaData;
+import com.yahoo.config.application.api.ComponentInfo;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.config.application.api.UnparsedConfigDefinition;
+import com.yahoo.config.codegen.DefParser;
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.application.provider.*;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.config.provision.Version;
+import com.yahoo.io.IOUtils;
+import com.yahoo.path.Path;
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.vespa.config.ConfigDefinition;
+import com.yahoo.vespa.config.ConfigDefinitionBuilder;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.util.ConfigUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.*;
+
+/**
+ * Represents an application residing in zookeeper.
+ *
+ * @author tonytv
+ */
+public class ZKApplicationPackage implements ApplicationPackage {
+
+ private ZKLiveApp liveApp;
+
+ private final Map<Version, PreGeneratedFileRegistry> fileRegistryMap = new HashMap<>();
+ private final Map<Version, ProvisionInfo> provisionInfoMap = new HashMap<>();
+ private static final Version legacyVersion = Version.fromIntValues(0, 0, 0);
+
+ public static final String fileRegistryNode = "fileregistry";
+ public static final String allocatedHostsNode = "allocatedHosts";
+ private final ApplicationMetaData metaData;
+
+ public ZKApplicationPackage(ConfigCurator zk, Path appPath) {
+ verifyAppPath(zk, appPath);
+ liveApp = new ZKLiveApp(zk, appPath);
+ metaData = readMetaDataFromLiveApp(liveApp);
+ importFileRegistries(fileRegistryNode);
+ importProvisionInfos(allocatedHostsNode);
+ }
+
+ private void importProvisionInfos(String allocatedHostsNode) {
+ List<String> provisionInfoNodes = liveApp.getChildren(allocatedHostsNode);
+ if (provisionInfoNodes.isEmpty()) {
+ Optional<ProvisionInfo> provisionInfo = importProvisionInfo(allocatedHostsNode);
+ provisionInfo.ifPresent(info -> provisionInfoMap.put(legacyVersion, info));
+ } else {
+ provisionInfoNodes.stream()
+ .forEach(versionStr -> {
+ Version version = Version.fromString(versionStr);
+ Optional<ProvisionInfo> provisionInfo = importProvisionInfo(Joiner.on("/").join(allocatedHostsNode, versionStr));
+ provisionInfo.ifPresent(info -> provisionInfoMap.put(version, info));
+ });
+ }
+ }
+
+ private Optional<ProvisionInfo> importProvisionInfo(String provisionInfoNode) {
+ try {
+ if (liveApp.exists(provisionInfoNode)) {
+ return Optional.of(ProvisionInfo.fromJson(liveApp.getBytes(provisionInfoNode)));
+ } else {
+ return Optional.empty();
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to read provision info", e);
+ }
+ }
+
+ private void importFileRegistries(String fileRegistryNode) {
+ List<String> fileRegistryNodes = liveApp.getChildren(fileRegistryNode);
+ if (fileRegistryNodes.isEmpty()) {
+ fileRegistryMap.put(legacyVersion, importFileRegistry(fileRegistryNode));
+ } else {
+ fileRegistryNodes.stream()
+ .forEach(versionStr -> {
+ Version version = Version.fromString(versionStr);
+ fileRegistryMap.put(version, importFileRegistry(Joiner.on("/").join(fileRegistryNode, versionStr)));
+ });
+ }
+ }
+
+ private PreGeneratedFileRegistry importFileRegistry(String fileRegistryNode) {
+ try {
+ return PreGeneratedFileRegistry.importRegistry(liveApp.getDataReader(fileRegistryNode));
+ } catch (Exception e) {
+ throw new RuntimeException("Could not determine which files to distribute. " +
+ "Please try redeploying the application", e);
+ }
+ }
+
+ private ApplicationMetaData readMetaDataFromLiveApp(ZKLiveApp liveApp) {
+ String metaDataString = liveApp.getData(ConfigCurator.META_ZK_PATH);
+ if (metaDataString == null || metaDataString.isEmpty()) {
+ return null;
+ }
+ return ApplicationMetaData.fromJsonString(liveApp.getData(ConfigCurator.META_ZK_PATH));
+ }
+
+ @Override
+ public ApplicationMetaData getMetaData() {
+ return metaData;
+ }
+
+ private static void verifyAppPath(ConfigCurator zk, Path appPath) {
+ if (!zk.exists(appPath.getAbsolute()))
+ throw new RuntimeException("App with path " + appPath + " does not exist");
+ }
+
+ @Override
+ public String getApplicationName() {
+ return metaData.getApplicationName();
+ }
+
+ @Override
+ public Reader getServices() {
+ return getUserAppData(SERVICES);
+ }
+
+ @Override
+ public Reader getHosts() {
+ if (liveApp.exists(ConfigCurator.USERAPP_ZK_SUBPATH,HOSTS))
+ return getUserAppData(HOSTS);
+ return null;
+ }
+
+ @Override
+ public List<NamedReader> searchDefinitionContents() {
+ List<NamedReader> ret = new ArrayList<>();
+ for (String sd : liveApp.getChildren(ConfigCurator.USERAPP_ZK_SUBPATH+"/"+SEARCH_DEFINITIONS_DIR)) {
+ if (sd.endsWith(ApplicationPackage.SD_NAME_SUFFIX)) {
+ ret.add(new NamedReader(sd, new StringReader(liveApp.getData(ConfigCurator.USERAPP_ZK_SUBPATH+"/"+SEARCH_DEFINITIONS_DIR, sd))));
+ }
+ }
+ return ret;
+ }
+
+ public Map<Version, ProvisionInfo> getProvisionInfoMap() {
+ return Collections.unmodifiableMap(provisionInfoMap);
+ }
+
+ @Override
+ public Map<Version, FileRegistry> getFileRegistryMap() {
+ return Collections.unmodifiableMap(fileRegistryMap);
+ }
+
+ private Optional<PreGeneratedFileRegistry> getPreGeneratedFileRegistry(Version vespaVersion) {
+ // Assumes at least one file registry, which we always have.
+ Optional<PreGeneratedFileRegistry> fileRegistry = Optional.ofNullable(fileRegistryMap.get(vespaVersion));
+ if (!fileRegistry.isPresent()) {
+ fileRegistry = Optional.of(fileRegistryMap.values().iterator().next());
+ }
+ return fileRegistry;
+ }
+
+ @Override
+ public List<NamedReader> getSearchDefinitions() {
+ return searchDefinitionContents();
+ }
+
+ private Reader retrieveConfigDefReader(String def) {
+ try {
+ return liveApp.getDataReader(ConfigCurator.DEFCONFIGS_ZK_SUBPATH, def);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Could not retrieve config definition " + def + ".", e);
+ }
+ }
+
+ @Override
+ public Map<ConfigDefinitionKey, UnparsedConfigDefinition> getAllExistingConfigDefs() {
+ Map<ConfigDefinitionKey, UnparsedConfigDefinition> ret = new LinkedHashMap<>();
+
+ List<String> allDefs = liveApp.getChildren(ConfigCurator.DEFCONFIGS_ZK_SUBPATH);
+
+ for (final String nodeName : allDefs) {
+ final ConfigDefinitionKey key = ConfigUtils.createConfigDefinitionKeyFromZKString(nodeName);
+ ret.put(key, new UnparsedConfigDefinition() {
+ @Override
+ public ConfigDefinition parse() {
+ DefParser parser = new DefParser(key.getName(), retrieveConfigDefReader(nodeName));
+ return ConfigDefinitionBuilder.createConfigDefinition(parser.getTree());
+ }
+
+ @Override
+ public String getUnparsedContent() {
+ try {
+ return IOUtils.readAll(retrieveConfigDefReader(nodeName));
+ } catch (Exception e) {
+ throw new RuntimeException("Error retriving def file", e);
+ }
+ }
+ });
+ }
+ return ret;
+ }
+
+ //Returns readers for all the children of a node.
+ //The node is looked up relative to the location of the active application package
+ //in zookeeper.
+ @Override
+ public List<NamedReader> getFiles(Path relativePath,String suffix,boolean recurse) {
+ return liveApp.getAllDataFromDirectory(ConfigCurator.USERAPP_ZK_SUBPATH + '/' + relativePath.getRelative(), suffix, recurse);
+ }
+
+ @Override
+ public ApplicationFile getFile(Path file) { // foo/bar/baz.json
+ return new ZKApplicationFile(file, liveApp);
+ }
+
+ @Override
+ public String getHostSource() {
+ return "zookeeper hosts file";
+ }
+
+ @Override
+ public String getServicesSource() {
+ return "zookeeper services file";
+ }
+
+ @Override
+ public Optional<Reader> getDeployment() { return optionalFile(DEPLOYMENT_FILE.getName()); }
+
+ @Override
+ public Optional<Reader> getValidationOverrides() { return optionalFile(VALIDATION_OVERRIDES.getName()); }
+
+ private Optional<Reader> optionalFile(String file) {
+ if (liveApp.exists(ConfigCurator.USERAPP_ZK_SUBPATH, file))
+ return Optional.of(getUserAppData(file));
+ else
+ return Optional.empty();
+ }
+
+ @Override
+ public List<ComponentInfo> getComponentsInfo(Version vespaVersion) {
+ List<ComponentInfo> components = new ArrayList<>();
+ PreGeneratedFileRegistry fileRegistry = getPreGeneratedFileRegistry(vespaVersion).get();
+ for (String path : fileRegistry.getPaths()) {
+ if (path.startsWith(FilesApplicationPackage.COMPONENT_DIR + File.separator) && path.endsWith(".jar")) {
+ ComponentInfo component = new ComponentInfo(path);
+ components.add(component);
+ }
+ }
+ return components;
+ }
+
+ private Reader getUserAppData(String node) {
+ return liveApp.getDataReader(ConfigCurator.USERAPP_ZK_SUBPATH, node);
+ }
+
+ @Override
+ public Reader getRankingExpression(String name) {
+ return liveApp.getDataReader(ConfigCurator.USERAPP_ZK_SUBPATH+"/"+SEARCH_DEFINITIONS_DIR, name);
+ }
+
+ @Override
+ public File getFileReference(Path pathRelativeToAppDir) {
+ String fileName = liveApp.getData(ConfigCurator.USERAPP_ZK_SUBPATH + "/" + pathRelativeToAppDir.getRelative());
+ return new File(fileName);
+ }
+
+ @Override
+ public void validateIncludeDir(String dirName) {
+ String fullPath = ConfigCurator.USERAPP_ZK_SUBPATH + "/" + dirName;
+ if (!liveApp.exists(fullPath)) {
+ throw new IllegalArgumentException("Cannot include directory '" + dirName +
+ "', as it does not exist in ZooKeeper!");
+ }
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKLiveApp.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKLiveApp.java
new file mode 100644
index 00000000000..254203118f2
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKLiveApp.java
@@ -0,0 +1,208 @@
+// 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.server.zookeeper;
+
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.path.Path;
+
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Responsible for providing data from the currently live application subtree in zookeeper.
+ * (i.e. /vespa/config/apps/&lt;id of currently active app&gt;/)
+ *
+ * @author tonytv
+ */
+public class ZKLiveApp {
+
+ private static final Logger log = Logger.getLogger(ZKLiveApp.class.getName());
+
+ private final ConfigCurator zk;
+ private final Path appPath;
+
+ public ZKLiveApp(ConfigCurator zk, Path appPath) {
+ this.zk = zk;
+ this.appPath = appPath;
+ }
+
+ /**
+ * Returns a list of the files (as readers) in the given path. The readers <b>must</b>
+ * be closed by the caller.
+ *
+ * @param path a path relative to the currently active application (i.e. /vespa/config/apps/&lt;id of currently active app&gt;/).
+ * @param fileNameSuffix the suffix of files to return, or null to return all
+ * @param recursive if true, all files from all subdirectories of this will also be returned
+ * @return the files in the given path, or an empty list (never null) if the directory does not exist or is empty.
+ * The list gets owned by the caller and can be modified freely.
+ */
+ public List<NamedReader> getAllDataFromDirectory(String path, String fileNameSuffix, boolean recursive) {
+ return getAllDataFromDirectory(path, "", fileNameSuffix, recursive);
+ }
+
+ /**
+ * As above, except
+ *
+ * @param namePrefix the prefix to prepend to the returned reader names
+ */
+ private List<NamedReader> getAllDataFromDirectory(String path, String namePrefix, String fileNameSuffix, boolean recursive) {
+ String fullPath = getFullPath(path);
+ List<NamedReader> result = new ArrayList<>();
+ List<String> children = getChildren(path);
+
+ try {
+ for (String child : children) {
+ if (fileNameSuffix == null || child.endsWith(fileNameSuffix)) {
+ result.add(new NamedReader(namePrefix + child, reader(zk.getData(fullPath, child))));
+ if (log.isLoggable(Level.FINER))
+ log.finer("ZKApplicationPackage: Added '" + child + "' (matched suffix " + fileNameSuffix + ")");
+ } else {
+ if (log.isLoggable(Level.FINER))
+ log.finer("ZKApplicationPackage: Skipped '" + child + "' (did not match suffix " + fileNameSuffix + ")");
+ }
+ if (recursive)
+ result.addAll(getAllDataFromDirectory(path + "/" + child, namePrefix + child + "/", fileNameSuffix, recursive));
+ }
+ if (log.isLoggable(Level.FINE))
+ log.fine("ZKApplicationPackage: Found '" + result.size() + "' files in " + fullPath);
+ return result;
+ } catch (Exception e) {
+ throw new RuntimeException("Could not retrieve all data from '" + fullPath + "' in zookeeper", e);
+ }
+ }
+
+ /**
+ * Retrieves a node relative to the node of the live application, e.g. /vespa/config/apps/$lt;app_id&gt;/&lt;path&gt;/&lt;node&gt;
+ *
+ * @param path a path relative to the currently active application
+ * @param node a path relative to the path above
+ * @return a Reader that can be used to get the data
+ */
+ public Reader getDataReader(String path, String node) {
+ final String data = getData(path, node);
+ if (data == null) {
+ throw new IllegalArgumentException("No node for " + getFullPath(path) + "/" + node + " exists");
+ }
+ return reader(data);
+ }
+
+ public String getData(String path, String node) {
+ try {
+ return zk.getData(getFullPath(path), node);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Could not retrieve node '" + getFullPath(path) + "/" + node + "' in zookeeper", e);
+ }
+ }
+
+ public String getData(String path) {
+ try {
+ return zk.getData(getFullPath(path));
+ } catch (RuntimeException e) {
+ throw new IllegalArgumentException("Could not retrieve path '" + getFullPath(path) + "' in zookeeper", e);
+ }
+ }
+
+ public byte[] getBytes(String path) {
+ try {
+ return zk.getBytes(getFullPath(path));
+ } catch (RuntimeException e) {
+ throw new IllegalArgumentException("Could not retrieve path '" + getFullPath(path) + "' in zookeeper", e);
+ }
+ }
+
+ public void putData(String path, String data) {
+ try {
+ zk.putData(getFullPath(path), data);
+ } catch (RuntimeException e) {
+ throw new IllegalArgumentException("Could not put data to node '" + getFullPath(path) + "' in zookeeper", e);
+ }
+ }
+
+ public void create(String path, String node) {
+ if (path != null && !path.startsWith("/")) path = "/" + path;
+ try {
+ zk.createNode(getFullPath(path), node);
+ } catch (RuntimeException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Checks if the given node exists under path under this live app
+ *
+ * @param path a zookeeper path
+ * @param node a zookeeper node
+ * @return true if the node exists in the path, false otherwise
+ */
+ public boolean exists(String path, String node) {
+ return zk.exists(getFullPath(path), node);
+ }
+
+ /**
+ * Checks if the given node exists under path under this live app
+ *
+ * @param path a zookeeper path
+ * @return true if the node exists in the path, false otherwise
+ */
+ public boolean exists(String path) {
+ return zk.exists(getFullPath(path));
+ }
+
+ private String getFullPath(String path) {
+ Path fullPath = appPath;
+ if (path != null) {
+ fullPath = appPath.append(path);
+ }
+ return fullPath.getAbsolute();
+ }
+
+ /**
+ * Recursively delete given path
+ *
+ * @param path path to delete
+ */
+ public void deleteRecurse(String path) {
+ zk.deleteRecurse(getFullPath(path));
+ }
+
+ /**
+ * Returns the full list of children (file names) in the given path.
+ *
+ * @param path a path relative to the currently active application
+ * @return a list of file names
+ */
+ public List<String> getChildren(String path) {
+ String fullPath = getFullPath(path);
+ if (! zk.exists(fullPath)) {
+ log.fine("ZKApplicationPackage: " + fullPath + " is not a valid dir");
+ return Collections.emptyList();
+ }
+ return zk.getChildren(fullPath);
+ }
+
+ private static Reader reader(String string) {
+ return new StringReader(string);
+ }
+
+ public void create(String path) {
+ if (path != null && !path.startsWith("/")) path = "/" + path;
+ try {
+ zk.createNode(getFullPath(path));
+ } catch (RuntimeException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public Reader getDataReader(String path) {
+ final String data = getData(path);
+ if (data == null) {
+ throw new IllegalArgumentException("No node for " + getFullPath(path) + " exists");
+ }
+ return reader(data);
+ }
+}
+
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/package-info.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/package-info.java
new file mode 100644
index 00000000000..a548df4d493
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/package-info.java
@@ -0,0 +1,8 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author tonytv
+ */
+@ExportPackage
+package com.yahoo.vespa.config.server.zookeeper;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/Cluster.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/Cluster.java
new file mode 100644
index 00000000000..879dae419d2
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/Cluster.java
@@ -0,0 +1,85 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.serviceview;
+
+import java.util.Arrays;
+import java.util.List;
+
+import com.google.common.collect.ImmutableList;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+
+/**
+ * Model a single cluster of services in the Vespa model.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public final class Cluster implements Comparable<Cluster> {
+ @NonNull
+ public final String name;
+ @NonNull
+ public final String type;
+ /**
+ * An ordered list of the service instances in this cluster.
+ */
+ @NonNull
+ public final ImmutableList<Service> services;
+
+ public Cluster(String name, String type, List<Service> services) {
+ this.name = name;
+ this.type = type;
+ ImmutableList.Builder<Service> builder = ImmutableList.builder();
+ Service[] sortingBuffer = services.toArray(new Service[0]);
+ Arrays.sort(sortingBuffer);
+ builder.add(sortingBuffer);
+ this.services = builder.build();
+ }
+
+ @Override
+ public int compareTo(Cluster other) {
+ int nameOrder = name.compareTo(other.name);
+ if (nameOrder != 0) {
+ return nameOrder;
+ }
+ return type.compareTo(other.type);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 761;
+ int result = 1;
+ result = prime * result + name.hashCode();
+ result = prime * result + type.hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Cluster other = (Cluster) obj;
+ if (!name.equals(other.name)) {
+ return false;
+ }
+ if (!type.equals(other.type)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ final int maxLen = 3;
+ StringBuilder builder = new StringBuilder();
+ builder.append("Cluster [name=").append(name).append(", type=").append(type).append(", services=")
+ .append(services.subList(0, Math.min(services.size(), maxLen))).append("]");
+ return builder.toString();
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/ConfigServerLocation.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/ConfigServerLocation.java
new file mode 100644
index 00000000000..163743cb1bb
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/ConfigServerLocation.java
@@ -0,0 +1,25 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.serviceview;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+
+/**
+ * Wrapper for settings from the cloud.config.configserver config.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class ConfigServerLocation {
+ public final int restApiPort;
+
+ public ConfigServerLocation(ConfigserverConfig configServer) {
+ restApiPort = configServer.httpport();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("ConfigServerLocation [restApiPort=").append(restApiPort).append("]");
+ return builder.toString();
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/ProxyErrorMapper.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/ProxyErrorMapper.java
new file mode 100644
index 00000000000..60055df88df
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/ProxyErrorMapper.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.serviceview;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Convert exceptions thrown by the internal REST client into a little more helpful responses.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+@Provider
+public class ProxyErrorMapper implements ExceptionMapper<WebApplicationException> {
+
+ @Override
+ public Response toResponse(WebApplicationException exception) {
+ StringBuilder msg = new StringBuilder("Invoking (external) web service failed: ");
+ msg.append(exception.getMessage());
+ return Response.status(500).entity(msg.toString()).type("text/plain").build();
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/Service.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/Service.java
new file mode 100644
index 00000000000..4d0dc36a0c7
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/Service.java
@@ -0,0 +1,172 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.serviceview;
+
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.common.collect.ImmutableList;
+import com.yahoo.text.Utf8;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+
+/**
+ * Model a single service instance as a sortable object.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public final class Service implements Comparable<Service> {
+ @NonNull
+ public final String serviceType;
+ @NonNull
+ public final String host;
+ public final int statePort;
+ @NonNull
+ public final String configId;
+ @NonNull
+ public final List<Integer> ports;
+ @NonNull
+ public final String name;
+
+ public Service(String serviceType, String host, int statePort, String clusterName, String clusterType,
+ String configId, List<Integer> ports, String name) {
+ this.serviceType = serviceType;
+ this.host = host.toLowerCase();
+ this.statePort = statePort;
+ this.configId = configId;
+ ImmutableList.Builder<Integer> portsBuilder = new ImmutableList.Builder<>();
+ portsBuilder.addAll(ports);
+ this.ports = portsBuilder.build();
+ this.name = name;
+ }
+
+ @Override
+ public int compareTo(Service other) {
+ int serviceTypeOrder = serviceType.compareTo(other.serviceType);
+ if (serviceTypeOrder != 0) {
+ return serviceTypeOrder;
+ }
+ int hostOrder = host.compareTo(other.host);
+ if (hostOrder != 0) {
+ return hostOrder;
+ }
+ return Integer.compare(statePort, other.statePort);
+ }
+
+ /**
+ * Generate an identifier string for one of the ports of this service
+ * suitable for using in an URL.
+ *
+ * @param port
+ * port which this identifier pertains to
+ * @return an opaque identifier string for this service
+ */
+ public String getIdentifier(int port) {
+ StringBuilder b = new StringBuilder(serviceType);
+ b.append("-");
+ MessageDigest md5;
+ try {
+ md5 = MessageDigest.getInstance("MD5");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("MD5 should by definition always be available in the JVM.", e);
+ }
+ md5.update(Utf8.toBytes(serviceType));
+ md5.update(Utf8.toBytes(configId));
+ md5.update(Utf8.toBytes(host));
+ for (int i = 3; i >= 0; --i) {
+ md5.update((byte) (port >>> i));
+ }
+ byte[] digest = md5.digest();
+ BigInteger digestMarshal = new BigInteger(1, digest);
+ b.append(digestMarshal.toString(36));
+ return b.toString();
+ }
+
+ /**
+ * All valid identifiers for this object.
+ *
+ * @return a list with a unique ID for each of this service's ports
+ */
+ public List<String> getIdentifiers() {
+ List<String> ids = new ArrayList<>(ports.size());
+ for (int port : ports) {
+ ids.add(getIdentifier(port));
+ }
+ return ids;
+ }
+
+ /**
+ * Find which port number a hash code pertains to.
+ *
+ * @param identifier a string generated from {@link #getIdentifier(int)}
+ * @return a port number, or 0 if no match is found
+ */
+ public int matchIdentifierWithPort(String identifier) {
+ for (int port : ports) {
+ if (identifier.equals(getIdentifier(port))) {
+ return port;
+ }
+ }
+ throw new IllegalArgumentException("Identifier " + identifier + " matches no ports in " + this);
+ }
+
+ @Override
+ public String toString() {
+ final int maxLen = 3;
+ StringBuilder builder = new StringBuilder();
+ builder.append("Service [serviceType=").append(serviceType).append(", host=").append(host).append(", statePort=")
+ .append(statePort).append(", configId=").append(configId).append(", ports=")
+ .append(ports.subList(0, Math.min(ports.size(), maxLen))).append(", name=").append(name)
+ .append("]");
+ return builder.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 131;
+ int result = 1;
+ result = prime * result + configId.hashCode();
+ result = prime * result + host.hashCode();
+ result = prime * result + name.hashCode();
+ result = prime * result + ports.hashCode();
+ result = prime * result + serviceType.hashCode();
+ result = prime * result + statePort;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Service other = (Service) obj;
+ if (!configId.equals(other.configId)) {
+ return false;
+ }
+ if (!host.equals(other.host)) {
+ return false;
+ }
+ if (!name.equals(other.name)) {
+ return false;
+ }
+ if (!ports.equals(other.ports)) {
+ return false;
+ }
+ if (!serviceType.equals(other.serviceType)) {
+ return false;
+ }
+ if (statePort != other.statePort) {
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/ServiceModel.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/ServiceModel.java
new file mode 100644
index 00000000000..12141366811
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/ServiceModel.java
@@ -0,0 +1,236 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.serviceview;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Table;
+import com.google.common.collect.Table.Cell;
+import com.yahoo.vespa.serviceview.bindings.ApplicationView;
+import com.yahoo.vespa.serviceview.bindings.ClusterView;
+import com.yahoo.vespa.serviceview.bindings.HostService;
+import com.yahoo.vespa.serviceview.bindings.ModelResponse;
+import com.yahoo.vespa.serviceview.bindings.ServicePort;
+import com.yahoo.vespa.serviceview.bindings.ServiceView;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+
+/**
+ * A transposed view for cloud.config.model.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public final class ServiceModel {
+ private static final String CLUSTERCONTROLLER_TYPENAME = "container-clustercontroller";
+
+ private static final String CONTENT_CLUSTER_TYPENAME = "content";
+
+ private final Map<String, Service> servicesMap;
+
+ /**
+ * An ordered list of the clusters in this config model.
+ */
+ @NonNull
+ public final ImmutableList<Cluster> clusters;
+
+ ServiceModel(ModelResponse modelConfig) {
+ Table<String, String, List<Service>> services = HashBasedTable.create();
+ for (HostService h : modelConfig.hosts) {
+ String hostName = h.name;
+ for (com.yahoo.vespa.serviceview.bindings.Service s : h.services) {
+ addService(services, hostName, s);
+ }
+ }
+ List<Cluster> sortingBuffer = new ArrayList<>();
+ for (Cell<String, String, List<Service>> c : services.cellSet()) {
+ sortingBuffer.add(new Cluster(c.getRowKey(), c.getColumnKey(), c.getValue()));
+ }
+ Collections.sort(sortingBuffer);
+ ImmutableList.Builder<Cluster> clustersBuilder = new ImmutableList.Builder<>();
+ clustersBuilder.addAll(sortingBuffer);
+ clusters = clustersBuilder.build();
+ Map<String, Service> seenIdentifiers = new HashMap<>();
+ for (Cluster c : clusters) {
+ for (Service s : c.services) {
+ List<String> identifiers = s.getIdentifiers();
+ for (String identifier : identifiers) {
+ if (seenIdentifiers.containsKey(identifier)) {
+ throw new RuntimeException(
+ "Congrats, you have a publishable result. We have a very unexpected hash collision" + " between "
+ + seenIdentifiers.get(identifier) + " and " + s + ".");
+ }
+ seenIdentifiers.put(identifier, s);
+ }
+ }
+ }
+ ImmutableMap.Builder<String, Service> servicesBuilder = new ImmutableMap.Builder<>();
+ servicesBuilder.putAll(seenIdentifiers);
+ servicesMap = servicesBuilder.build();
+ }
+
+ private static void addService(Table<String, String, List<Service>> services,
+ String hostName,
+ com.yahoo.vespa.serviceview.bindings.Service s) {
+ boolean hasStateApi = false;
+ int statePort = 0;
+ List<Integer> ports = new ArrayList<>(s.ports.size());
+ for (ServicePort port : s.ports) {
+ ports.add(port.number);
+ if (!hasStateApi && port.hasTags("http", "state")) {
+ hasStateApi = true;
+ statePort = port.number;
+ }
+ }
+ // ignore hosts without state API
+ if (hasStateApi) {
+ Service service = new Service(s.type, hostName, statePort, s.clustername, s.clustertype, s.configid, ports, s.name);
+ getAndSetEntry(services, s.clustername, s.clustertype).add(service);
+ }
+ }
+
+ private static List<Service> getAndSetEntry(Table<String, String, List<Service>> services, String clusterName, String clusterType) {
+ List<Service> serviceList = services.get(clusterName, clusterType);
+ if (serviceList == null) {
+ serviceList = new ArrayList<>();
+ services.put(clusterName, clusterType, serviceList);
+ }
+ return serviceList;
+ }
+
+ /**
+ * The top level view of a given application.
+ *
+ * @return a top level view of the entire application in a form suitable for
+ * consumption by a REST API
+ */
+ public ApplicationView showAllClusters(String uriBase, String applicationIdentifier) {
+ ApplicationView response = new ApplicationView();
+ List<ClusterView> clusterViews = new ArrayList<>();
+ for (Cluster c : clusters) {
+ clusterViews.add(showCluster(c, uriBase, applicationIdentifier));
+ }
+ response.clusters = clusterViews;
+ return response;
+ }
+
+ private ClusterView showCluster(Cluster c, String uriBase, String applicationIdentifier) {
+ List<ServiceView> services = new ArrayList<>();
+ for (Service s : c.services) {
+ ServiceView service = new ServiceView();
+ StringBuilder buffer = getLinkBuilder(uriBase).append(applicationIdentifier).append('/');
+ service.url = buffer.append("service/").append(s.getIdentifier(s.statePort)).append("/state/v1/").toString();
+ service.serviceType = s.serviceType;
+ service.serviceName = s.name;
+ service.configId = s.configId;
+ service.host = s.host;
+ addLegacyLink(uriBase, applicationIdentifier, s, service);
+ services.add(service);
+ }
+ ClusterView v = new ClusterView();
+ v.services = services;
+ v.name = c.name;
+ v.type = c.type;
+ if (CONTENT_CLUSTER_TYPENAME.equals(c.type)) {
+ Service s = getFirstClusterController();
+ StringBuilder buffer = getLinkBuilder(uriBase).append(applicationIdentifier).append('/');
+ buffer.append("service/").append(s.getIdentifier(s.statePort)).append("/cluster/v2/").append(c.name);
+ v.url = buffer.toString();
+ } else {
+ v.url = null;
+ }
+ return v;
+ }
+
+ private void addLegacyLink(String uriBase, String applicationIdentifier, Service s, ServiceView service) {
+ if (s.serviceType.equals("storagenode") || s.serviceType.equals("distributor")) {
+ StringBuilder legacyBuffer = getLinkBuilder(uriBase);
+ legacyBuffer.append("legacy/").append(applicationIdentifier).append('/');
+ legacyBuffer.append("service/").append(s.getIdentifier(s.statePort)).append('/');
+ service.legacyStatusPages = legacyBuffer.toString();
+ }
+ }
+
+ private Service getFirstServiceInstanceByType(@NonNull String typeName) {
+ for (Cluster c : clusters) {
+ for (Service s : c.services) {
+ if (typeName.equals(s.serviceType)) {
+ return s;
+ }
+ }
+ }
+ throw new IllegalStateException("This installation has but no service of required type: "
+ + typeName + ".");
+ }
+
+ private Service getFirstClusterController() {
+ // This is used assuming all cluster controllers know of all fleet controllers in an application
+ return getFirstServiceInstanceByType(CLUSTERCONTROLLER_TYPENAME);
+ }
+
+ private StringBuilder getLinkBuilder(String uriBase) {
+ StringBuilder buffer = new StringBuilder(uriBase);
+ if (!uriBase.endsWith("/")) {
+ buffer.append('/');
+ }
+ return buffer;
+ }
+
+ @Override
+ public String toString() {
+ final int maxLen = 3;
+ StringBuilder builder = new StringBuilder();
+ builder.append("ServiceModel [clusters=")
+ .append(clusters.subList(0, Math.min(clusters.size(), maxLen))).append("]");
+ return builder.toString();
+ }
+
+
+ /**
+ * Match an identifier with a service for this cluster.
+ *
+ * @param identifier
+ * an opaque service identifier generated by the service
+ * @return the corresponding Service instance
+ */
+ public Service getService(String identifier) {
+ return servicesMap.get(identifier);
+ }
+
+ /**
+ * Find a service based on host and port.
+ *
+ * @param host
+ * the name of the host running the service
+ * @param port
+ * a port owned by the service
+ * @param self
+ * the service which generated the host data
+ * @return a service instance fullfilling the criteria
+ * @throws IllegalArgumentException
+ * if no matching service is found
+ */
+ public Service resolve(String host, int port, Service self) {
+ Integer portAsObject = Integer.valueOf(port);
+ String realHost;
+ if ("localhost".equals(host)) {
+ realHost = self.host;
+ } else {
+ realHost = host;
+ }
+ for (Cluster c : clusters) {
+ for (Service s : c.services) {
+ if (s.host.equals(realHost) && s.ports.contains(portAsObject)) {
+ return s;
+ }
+ }
+ }
+ throw new IllegalArgumentException("No registered service owns port " + port + " on host " + realHost + ".");
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/StateResource.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/StateResource.java
new file mode 100644
index 00000000000..c66e0c2b3ba
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/StateResource.java
@@ -0,0 +1,282 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.serviceview;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.UriInfo;
+
+import com.yahoo.container.jaxrs.annotation.Component;
+import com.yahoo.vespa.serviceview.bindings.ApplicationView;
+import com.yahoo.vespa.serviceview.bindings.ConfigClient;
+import com.yahoo.vespa.serviceview.bindings.HealthClient;
+import com.yahoo.vespa.serviceview.bindings.ModelResponse;
+import com.yahoo.vespa.serviceview.bindings.StateClient;
+
+import org.glassfish.jersey.client.proxy.WebResourceFactory;
+
+
+/**
+ * A web service to discover and proxy Vespa service state info.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+@Path("/")
+public class StateResource implements StateClient {
+ private static final String SINGLE_API_LINK = "url";
+ private final int restApiPort;
+ private final String host;
+ private final UriInfo uriInfo;
+
+ @SuppressWarnings("serial")
+ private static class GiveUpLinkRetargetingException extends Exception {
+ public GiveUpLinkRetargetingException(Throwable reason) {
+ super(reason);
+ }
+
+ public GiveUpLinkRetargetingException(String message) {
+ super(message);
+ }
+ }
+
+ public StateResource(@Component ConfigServerLocation configServer, @Context UriInfo ui) {
+ this.restApiPort = configServer.restApiPort;
+ host = "localhost";
+ this.uriInfo = ui;
+ }
+
+ @Override
+ @GET
+ @Path("v1/")
+ @Produces(MediaType.APPLICATION_JSON)
+ public ApplicationView getDefaultUserInfo() {
+ return getUserInfo("default", "default", "default", "default", "default");
+ }
+
+ @Override
+ @GET
+ @Path("v1/tenant/{tenantName}/application/{applicationName}/environment/{environmentName}/region/{regionName}/instance/{instanceName}")
+ @Produces(MediaType.APPLICATION_JSON)
+ public ApplicationView getUserInfo(@PathParam("tenantName") String tenantName,
+ @PathParam("applicationName") String applicationName,
+ @PathParam("environmentName") String environmentName,
+ @PathParam("regionName") String regionName,
+ @PathParam("instanceName") String instanceName) {
+ ServiceModel model = new ServiceModel(
+ getModelConfig(tenantName, applicationName, environmentName, regionName, instanceName));
+ return model.showAllClusters(
+ getBaseUri() + "v1/",
+ applicationIdentifier(tenantName, applicationName, environmentName, regionName, instanceName));
+ }
+
+
+ @Produces(MediaType.TEXT_HTML)
+ public interface HtmlProxyHack {
+ @GET
+ public String proxy();
+ }
+
+ @GET
+ @Path("v1/legacy/tenant/{tenantName}/application/{applicationName}/environment/{environmentName}/region/{regionName}/instance/{instanceName}/service/{serviceIdentifier}/{apiParams: .*}")
+ @Produces(MediaType.TEXT_HTML)
+ public String htmlProxy(@PathParam("tenantName") String tenantName,
+ @PathParam("applicationName") String applicationName,
+ @PathParam("environmentName") String environmentName,
+ @PathParam("regionName") String regionName,
+ @PathParam("instanceName") String instanceName,
+ @PathParam("serviceIdentifier") String identifier,
+ @PathParam("apiParams") String apiParams) {
+ ServiceModel model = new ServiceModel(getModelConfig(tenantName, applicationName, environmentName, regionName, instanceName));
+ Service s = model.getService(identifier);
+ int requestedPort = s.matchIdentifierWithPort(identifier);
+ Client client = ClientBuilder.newClient();
+ try {
+ final StringBuilder uriBuffer = new StringBuilder("http://").append(s.host).append(':').append(requestedPort).append('/')
+ .append(apiParams);
+ addQuery(uriBuffer);
+ String uri = uriBuffer.toString();
+ WebTarget target = client.target(uri);
+ HtmlProxyHack resource = WebResourceFactory.newResource(HtmlProxyHack.class, target);
+ return resource.proxy();
+ } finally {
+ if (client != null) {
+ client.close();
+ }
+ }
+ }
+
+ private String getBaseUri() {
+ String baseUri = uriInfo.getBaseUri().toString();
+ if (baseUri.endsWith("/")) {
+ return baseUri;
+ } else {
+ return baseUri + "/";
+ }
+ }
+
+ protected ModelResponse getModelConfig(String tenant, String application, String environment, String region, String instance) {
+ Client client = ClientBuilder.newClient();
+ try {
+ WebTarget target = client.target("http://" + host + ":" + restApiPort + "/");
+
+ ConfigClient resource = WebResourceFactory.newResource(ConfigClient.class, target);
+
+ return resource.getServiceModel(tenant, application, environment, region, instance);
+ } finally {
+ if (client != null) {
+ client.close();
+ }
+ }
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ @GET
+ @Path("v1/tenant/{tenantName}/application/{applicationName}/environment/{environmentName}/region/{regionName}/instance/{instanceName}/service/{serviceIdentifier}/{apiParams: .*}")
+ @Produces(MediaType.APPLICATION_JSON)
+ public HashMap singleService(@PathParam("tenantName") String tenantName,
+ @PathParam("applicationName") String applicationName,
+ @PathParam("environmentName") String environmentName,
+ @PathParam("regionName") String regionName,
+ @PathParam("instanceName") String instanceName,
+ @PathParam("serviceIdentifier") String identifier,
+ @PathParam("apiParams") String apiParams) {
+ ServiceModel model = new ServiceModel(getModelConfig(tenantName, applicationName, environmentName, regionName, instanceName));
+ Service s = model.getService(identifier);
+ int requestedPort = s.matchIdentifierWithPort(identifier);
+ Client client = ClientBuilder.newClient();
+ try {
+ HealthClient resource = getHealthClient(apiParams, s, requestedPort, client);
+ HashMap<?, ?> apiResult = resource.getHealthInfo();
+ rewriteResourceLinks(apiResult, model, s, applicationIdentifier(tenantName, applicationName, environmentName, regionName, instanceName), identifier);
+ return apiResult;
+ } finally {
+ if (client != null) {
+ client.close();
+ }
+ }
+ }
+
+ protected HealthClient getHealthClient(String apiParams, Service s, int requestedPort, Client client) {
+ final StringBuilder uriBuffer = new StringBuilder("http://").append(s.host).append(':').append(requestedPort).append('/')
+ .append(apiParams);
+ addQuery(uriBuffer);
+ WebTarget target = client.target(uriBuffer.toString());
+ return WebResourceFactory.newResource(HealthClient.class, target);
+ }
+
+ private String applicationIdentifier(String tenant, String application, String environment, String region, String instance) {
+ return new StringBuilder("tenant/").append(tenant).append("/application/").append(application).append("/environment/")
+ .append(environment).append("/region/").append(region).append("/instance/").append(instance).toString();
+ }
+
+ private void rewriteResourceLinks(Object apiResult,
+ ServiceModel model,
+ Service self,
+ String applicationIdentifier,
+ String incomingIdentifier) {
+ if (apiResult instanceof List) {
+ for (@SuppressWarnings("unchecked") ListIterator<Object> i = ((List<Object>) apiResult).listIterator(); i.hasNext();) {
+ Object resource = i.next();
+ if (resource instanceof String) {
+ try {
+ StringBuilder buffer = linkBuffer(applicationIdentifier);
+ // if it points to a port and host not part of the application, rewriting will not occur, so this is kind of safe
+ retarget(model, self, buffer, (String) resource);
+ i.set(buffer.toString());
+ } catch (GiveUpLinkRetargetingException e) {
+ break; // assume relatively homogenous lists when doing rewrites to avoid freezing up on scanning long lists
+ }
+ } else {
+ rewriteResourceLinks(resource, model, self, applicationIdentifier, incomingIdentifier);
+ }
+ }
+ } else if (apiResult instanceof Map) {
+ @SuppressWarnings("unchecked")
+ Map<Object, Object> api = (Map<Object, Object>) apiResult;
+ for (Map.Entry<Object, Object> entry : api.entrySet()) {
+ if (SINGLE_API_LINK.equals(entry.getKey()) && entry.getValue() instanceof String) {
+ try {
+ rewriteSingleLink(entry, model, self, linkBuffer(applicationIdentifier));
+ } catch (GiveUpLinkRetargetingException e) {
+ // NOP
+ }
+ } else if ("link".equals(entry.getKey()) && entry.getValue() instanceof String) {
+ buildSingleLink(entry, model, linkBuffer(applicationIdentifier), incomingIdentifier);
+ } else {
+ rewriteResourceLinks(entry.getValue(), model, self, applicationIdentifier, incomingIdentifier);
+ }
+ }
+ }
+ }
+
+ private void buildSingleLink(Map.Entry<Object, Object> entry,
+ ServiceModel model,
+ StringBuilder newUri,
+ String incomingIdentifier) {
+ newUri.append("/service/")
+ .append(incomingIdentifier);
+ newUri.append(entry.getValue());
+ entry.setValue(newUri.toString());
+ }
+
+ private void addQuery(StringBuilder newUri) {
+ String query = uriInfo.getRequestUri().getRawQuery();
+ if (query != null && query.length() > 0) {
+ newUri.append('?').append(query);
+ }
+ }
+
+ private StringBuilder linkBuffer(String applicationIdentifier) {
+ StringBuilder newUri = new StringBuilder(getBaseUri());
+ newUri.append("v1/").append(applicationIdentifier);
+ return newUri;
+ }
+
+ private void rewriteSingleLink(Map.Entry<Object, Object> entry,
+ ServiceModel model,
+ Service self,
+ StringBuilder newUri) throws GiveUpLinkRetargetingException {
+ String url = (String) entry.getValue();
+ retarget(model, self, newUri, url);
+ entry.setValue(newUri.toString());
+ }
+
+ private void retarget(ServiceModel model, Service self, StringBuilder newUri, String url) throws GiveUpLinkRetargetingException {
+ URI link;
+ try {
+ link = new URI(url);
+ } catch (URISyntaxException e) {
+ throw new GiveUpLinkRetargetingException(e);
+ }
+ if (!link.isAbsolute()) {
+ throw new GiveUpLinkRetargetingException("This rewriting only supports absolute URIs.");
+ }
+ int linkPort = link.getPort();
+ if (linkPort == -1) {
+ linkPort = 80;
+ }
+ Service s;
+ try {
+ s = model.resolve(link.getHost(), linkPort, self);
+ } catch (IllegalArgumentException e) {
+ throw new GiveUpLinkRetargetingException(e);
+ }
+ newUri.append("/service/").append(s.getIdentifier(linkPort));
+ newUri.append(link.getPath());
+ }
+
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/package-info.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/package-info.java
new file mode 100644
index 00000000000..5cc9fa85775
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/package-info.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.
+/**
+ * Home of the centralised service view implementation. The service view is a
+ * REST API for discovering and accessing the state API for any service in a
+ * Vespa cluster.
+ *
+ * <p>Do note this package is in its prototyping stage and classes <i>will</i>
+ * be renamed and moved around a little.</p>
+ */
+@ExportPackage
+package com.yahoo.vespa.serviceview;
+
+import com.yahoo.osgi.annotation.ExportPackage;